Skip to content

Commit 3b125e3

Browse files
annatischmikeharderpvaneck
authored
[Perfstress] Multiprocess support (Azure#22997)
* Draft multiproc support * Improve barrier reliability * Spelling * Run global setup per proc * Update cspell * Add parameters to SleepTest - Aligns with .NET and supports more scenarios * Add LogTest to help observe and debug the framework * Subprocess spawn instead of fork * Fix status reporting * Improved error handling * Updated docs * Apply suggestions from code review Co-authored-by: Paul Van Eck <[email protected]> * Cspell fixes * Typo Co-authored-by: Mike Harder <[email protected]> Co-authored-by: Paul Van Eck <[email protected]>
1 parent ba11784 commit 3b125e3

File tree

7 files changed

+504
-150
lines changed

7 files changed

+504
-150
lines changed

doc/dev/perfstress_tests.md

Lines changed: 123 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
# Table of Contents
22
1. [The perfstress framework](#the-perfstress-framework)
3-
- [The PerfStressTest base](#the-perfstresstest-base)
3+
- [The framework baseclasses](#the-provided-baseclasses)
4+
- [PerfStressTest](#the-perfstresstest-baseclass)
5+
- [BatchPerfTest](#the-batchperftest-baseclass)
6+
- [EventPerfTest](#the-eventperftest-baseclass)
47
- [Default command options](#default-command-options)
58
- [Running with test proxy](#running-with-the-test-proxy)
6-
- [The BatchPerfTest base](#the-batchperftest-base)
79
2. [Adding performance tests to an SDK](#adding-performance-tests-to-an-sdk)
810
- [Writing a test](#writing-a-test)
911
- [Writing a batch test](#writing-a-batch-test)
@@ -12,6 +14,8 @@
1214
- [Running the system tests](#running-the-system-tests)
1315
4. [Readme](#readme)
1416

17+
[comment]: # ( cspell:ignore perfstresstest batchperftest )
18+
1519
# The perfstress framework
1620

1721
The perfstress framework has been added to azure-devtools module. The code can be found [here](https://github.com/Azure/azure-sdk-for-python/tree/main/tools/azure-devtools/src/azure_devtools/perfstress_tests).
@@ -22,52 +26,69 @@ the tests. To start using the framework, make sure that `azure-devtools` is incl
2226
```
2327
The perfstress framework offers the following:
2428
- The `perfstress` commandline tool.
25-
- The `PerfStressTest` baseclass.
26-
- The `BatchPerfTest` baseclass.
29+
- The `PerfStressTest` baseclass (each test run counted as a single data point).
30+
- The `BatchPerfTest` baseclass (each test run counted as 1 or more data points).
31+
- The `EventPerfTest` baseclass (supports a callback based event handler).
2732
- Stream utilities for uploading/downloading without storing in memory: `RandomStream`, `AsyncRandomStream`, `WriteStream`.
2833
- A `get_random_bytes` utility for returning randomly generated data.
2934
- A series of "system tests" to test the perfstress framework along with the performance of the raw transport layers (requests, aiohttp, etc).
3035

31-
## The PerfStressTest base
32-
The `PerfStressTest` base class is what will be used for all perf test implementations. It provides the following API:
36+
## The provided baseclasses:
37+
The perf framework provides three baseclasses to accommodate testing different SDK scenarios. Depending on which baseclass you select, different
38+
methods will need to be implemented. All of them have a common base API (`_PerfTestBase`), defined below:
39+
3340
```python
34-
class PerfStressTest:
41+
class _PerfTestBase:
3542
args = {} # Command line arguments
3643

44+
@property
45+
def completed_operations(self) -> int:
46+
# Total number of operations completed by run_all(). Reset after warmup.
47+
48+
@property
49+
def last_completion_time(self) -> float:
50+
# Elapsed time between start of warmup/run and last completed operation. Reset after warmup.
51+
3752
def __init__(self, arguments):
3853
# The command line args can be accessed on construction.
3954

40-
async def global_setup(self):
41-
# Can be optionally defined. Only run once, regardless of parallelism.
55+
async def global_setup(self) -> None:
56+
# Can be optionally defined. Only run once per process, regardless of multi-threading.
57+
# The baseclasses will also define logic here, so if you override this method, make sure you include a call to super().
4258

43-
async def global_cleanup(self):
44-
# Can be optionally defined. Only run once, regardless of parallelism.
59+
async def global_cleanup(self) -> None:
60+
# Can be optionally defined. Only run once per process, regardless of multi-threading.
61+
# The baseclasses will also define logic here, so if you override this method, make sure you include a call to super().
4562

46-
async def post_setup(self):
63+
async def post_setup(self) -> None:
4764
# Post-setup called once per parallel test instance.
4865
# Used by base classes to setup state (like test-proxy) after all derived class setup is complete.
4966
# There should be no need to overwrite this function.
5067

51-
async def pre_cleanup(self):
68+
async def pre_cleanup(self) -> None:
5269
# Pre-cleanup called once per parallel test instance.
5370
# Used by base classes to cleanup state (like test-proxy) before all derived class cleanup runs.
5471
# There should be no need to overwrite this function.
5572

56-
async def setup(self):
73+
async def setup(self) -> None:
5774
# Can be optionally defined. Run once per test instance, after global_setup.
75+
# The baseclasses will also define logic here, so if you override this method, make sure you include a call to super().
5876

59-
async def cleanup(self):
77+
async def cleanup(self) -> None:
6078
# Can be optionally defined. Run once per test instance, before global_cleanup.
79+
# The baseclasses will also define logic here, so if you override this method, make sure you include a call to super().
6180

62-
async def close(self):
81+
async def close(self) -> None:
6382
# Can be optionally defined. Run once per test instance, after cleanup and global_cleanup.
83+
# The baseclasses will also define logic here, so if you override this method, make sure you include a call to super().
6484

65-
def run_sync(self):
66-
# Must be implemented. This will be the perf test to be run synchronously.
85+
def run_all_sync(self, duration: int) -> None:
86+
# Run all sync tests, including both warmup and duration. This method is implemented by the provided base
87+
# classes, there should be no need to overwrite this function.
6788

68-
async def run_async(self):
69-
# Must be implemented. This will be the perf test to be run asynchronously.
70-
# If writing a test for a T1 legacy SDK with no async, implement this method and raise an exception.
89+
async def run_all_async(self, duration: int) -> None:
90+
# Run all async tests, including both warmup and duration. This method is implemented by the provided base
91+
# classes, there should be no need to overwrite this function.
7192

7293
@staticmethod
7394
def add_arguments(parser):
@@ -78,17 +99,92 @@ class PerfStressTest:
7899
def get_from_env(variable):
79100
# Get the value of an env var. If empty or not found, a ValueError will be raised.
80101
```
102+
### The PerfStressTest baseclass
103+
This is probably the most common test baseclass, and should be used where each test run represents 1 logical successful result.
104+
For example, 1 successful service request, 1 file uploaded, 1 output downloaded, etc.
105+
Along with the above base API, the following methods will need to be implemented:
106+
107+
```python
108+
class PerfStressTest:
109+
def run_sync(self) -> None:
110+
# Must be implemented. This will be the perf test to be run synchronously.
111+
112+
async def run_async(self) -> None:
113+
# Must be implemented. This will be the perf test to be run asynchronously.
114+
# If writing a test for an SDK without async support (e.g. a T1 legacy SDK), implement this method and raise an exception.
115+
116+
```
117+
### The BatchPerfTest baseclass
118+
The `BatchPerfTest` class is the parent class of the above `PerfStressTest` class that is further abstracted to allow for more flexible testing of SDKs that don't conform to a 1:1 ratio of operations to results.
119+
This baseclass should be used where each test run represent a more than a single result. For example, results that are uploaded
120+
or downloaded in batches.
121+
Along with the above base API, the following methods will need to be implemented:
122+
123+
```python
124+
class BatchPerfTest:
125+
def run_batch_sync(self) -> int:
126+
# Run cumulative operation(s) synchronously - i.e. an operation that results in more than a single logical result.
127+
# When inheriting from BatchPerfTest, this method will need to be implemented.
128+
# Must return the number of completed results represented by a single successful test run.
129+
130+
async def run_batch_async(self) -> int:
131+
# Run cumulative operation(s) asynchronously - i.e. an operation that results in more than a single logical result.
132+
# When inheriting from BatchPerfTest, this method will need to be implemented.
133+
# Must return the number of completed results represented by a single successful test run.
134+
# If writing a test for an SDK without async support (e.g. a T1 legacy SDK), implement this method and raise an exception.
135+
136+
```
137+
### The EventPerfTest baseclass
138+
This baseclass should be used when SDK operation to be tested requires starting up a process that acts on events via callback.
139+
Along with the above base API, the following methods will need to be implemented:
140+
```python
141+
class EventPerfTest:
142+
def event_raised_sync(self) -> None:
143+
# This method should not be overwritten, instead it should be called in your sync callback implementation
144+
# to register a single successful event.
145+
146+
def error_raised_sync(self, error):
147+
# This method should not be overwritten, instead it should be called in your sync callback implementation
148+
# to register a failure in the event handler. This will result in the test being shutdown.
149+
150+
async def event_raised_async(self):
151+
# This method should not be overwritten, instead it should be called in your async callback implementation
152+
# to register a single successful event.
153+
154+
async def error_raised_async(self, error):
155+
# This method should not be overwritten, instead it should be called in your async callback implementation
156+
# to register a failure in the event handler. This will result in the test being shutdown.
157+
158+
def start_events_sync(self) -> None:
159+
# Must be implemented - starts the synchronous process for receiving events.
160+
# This can be blocking for the duration of the test as it will be run during setup() in a thread.
161+
162+
def stop_events_sync(self) -> None:
163+
# Stop the synchronous process for receiving events. Must be implemented. Will be called during cleanup.
164+
165+
async def start_events_async(self) -> None:
166+
# Must be implemented - starts the asynchronous process for receiving events.
167+
# This can be blocking for the duration of the test as it will be scheduled in the eventloop during setup().
168+
169+
async def stop_events_async(self) -> None:
170+
# Stop the asynchronous process for receiving events. Must be implemented. Will be called during cleanup.
171+
172+
```
173+
81174
## Default command options
82175
The framework has a series of common command line options built in:
83176
- `-d --duration=10` Number of seconds to run as many operations (the "run" function) as possible. Default is 10.
84-
- `-i --iterations=1` Number of test iterations to run. Default is 1.
85177
- `-p --parallel=1` Number of tests to run in parallel. Default is 1.
178+
- `--processes=multiprocessing.cpu_count()` Number of concurrent processes that the parallel test runs should be distributed over. This is used
179+
together with `--parallel` to distribute the number of concurrent tests first between available processes, then between threads within each
180+
process. For example if `--parallel=16 --processes=4`, 4 processes will be started, each running 4 concurrent threaded test instances.
181+
Best effort will be made to distribute evenly, for example if `--parallel=10 --processes=4`, 4 processes will be start, two of which run 3 threads, and two that run 2 threads. It's therefore recommended that the value of `parallel` be less than, or a multiple of, the value of `processes`.
86182
- `-w --warm-up=5` Number of seconds to spend warming up the connection before measuring begins. Default is 5.
87183
- `--sync` Whether to run the tests in sync or async. Default is False (async).
88184
- `--no-cleanup` Whether to keep newly created resources after test run. Default is False (resources will be deleted).
89185
- `--insecure` Whether to run without SSL validation. Default is False.
90186
- `-x --test-proxies` Whether to run the tests against the test proxy server. Specify the URL(s) for the proxy endpoint(s) (e.g. "https://localhost:5001"). Multiple values should be semi-colon-separated.
91-
- `--profile` Whether to run the perftest with cProfile. If enabled (default is False), the output file of a single iteration will be written to the current working directory in the format `"cProfile-<TestClassName>-<TestID>-<sync|async>.pstats"`.
187+
- `--profile` Whether to run the perftest with cProfile. If enabled (default is False), the output file of a single iteration will be written to the current working directory in the format `"cProfile-<TestClassName>-<TestID>-<sync|async>.pstats"`. **Note:** The profiler is not currently supported for the `EventPerfTest` baseclass.
92188

93189
## Running with the test proxy
94190
Follow the instructions here to install and run the test proxy server:
@@ -98,29 +194,6 @@ Once running, in a separate process run the perf test in question, combined with
98194
```cmd
99195
(env) ~/azure-storage-blob/tests> perfstress DownloadTest -x "https://localhost:5001"
100196
```
101-
## The BatchPerfTest base
102-
The `BatchPerfTest` class is the parent class of the above `PerfStressTest` class that is further abstracted to allow for more flexible testing of SDKs that don't conform to a 1:1 ratio of operations to results.
103-
An example of this is a messaging SDK that streams multiple messages for a period of time.
104-
This base class uses the same setup/cleanup/close functions described above, however instead of `run_sync` and `run_async`, it has `run_batch_sync` and `run_batch_async`:
105-
```python
106-
class BatchPerfTest:
107-
108-
def run_batch_sync(self) -> int:
109-
"""
110-
Run cumultive operation(s) - i.e. an operation that results in more than a single logical result.
111-
:returns: The number of completed results.
112-
:rtype: int
113-
"""
114-
115-
async def run_batch_async(self) -> int:
116-
"""
117-
Run cumultive operation(s) - i.e. an operation that results in more than a single logical result.
118-
:returns: The number of completed results.
119-
:rtype: int
120-
"""
121-
122-
```
123-
An example test case using the `BatchPerfTest` base can be found below.
124197

125198
# Adding performance tests to an SDK
126199
The performance tests will be in a submodule called `perfstress_tests` within the `tests` directory in an SDK project.
@@ -131,7 +204,7 @@ sdk/storage/azure-storage-blob/tests/perfstress_tests
131204
This `perfstress_tests` directory is a module, and so must contain an `__init__.py` file. This can be empty.
132205

133206
## Writing a test
134-
To add a test, import and inherit from `PerfStressTest` and populate the functions as needed.
207+
To add a test, import and inherit from one of the provided baseclasses and populate the functions as needed.
135208
The name of the class will be the name of the perf test, and is what will be passed into the command line to execute that test.
136209
```python
137210
from azure_devtools.perfstress_tests import PerfStressTest
@@ -182,7 +255,7 @@ class ListContainersTest(PerfStressTest):
182255
"""The synchronous perf test.
183256
184257
Try to keep this minimal and focused. Using only a single client API.
185-
Avoid putting any ancilliary logic (e.g. generating UUIDs), and put this in the setup/init instead
258+
Avoid putting any ancillary logic (e.g. generating UUIDs), and put this in the setup/init instead
186259
so that we're only measuring the client API call.
187260
"""
188261
for _ in self.client.list_containers():
@@ -192,7 +265,7 @@ class ListContainersTest(PerfStressTest):
192265
"""The asynchronous perf test.
193266
194267
Try to keep this minimal and focused. Using only a single client API.
195-
Avoid putting any ancilliary logic (e.g. generating UUIDs), and put this in the setup/init instead
268+
Avoid putting any ancillary logic (e.g. generating UUIDs), and put this in the setup/init instead
196269
so that we're only measuring the client API call.
197270
"""
198271
async for _ in self.async_client.list_containers():
@@ -218,7 +291,7 @@ class _StorageStreamTestBase(PerfStressTest):
218291
super().__init__(arguments)
219292

220293
# Any common attributes
221-
self.container_name = 'streamperftests'
294+
self.container_name = 'stream-perf-tests'
222295

223296
# Auth configuration
224297
connection_string = self.get_from_env("AZURE_STORAGE_CONNECTION_STRING")

tools/azure-devtools/src/azure_devtools/perfstress_tests/_perf_stress_base.py

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
import os
77
import abc
8-
import threading
8+
import multiprocessing
99
import argparse
1010

1111

@@ -106,17 +106,15 @@ class _PerfTestBase(_PerfTestABC):
106106
"""Base class for implementing a python perf test."""
107107

108108
args = {}
109-
_global_parallel_index_lock = threading.Lock()
109+
_global_parallel_index_lock = multiprocessing.Lock()
110110
_global_parallel_index = 0
111111

112112
def __init__(self, arguments):
113113
self.args = arguments
114114
self._completed_operations = 0
115115
self._last_completion_time = 0.0
116-
117-
with _PerfTestBase._global_parallel_index_lock:
118-
self._parallel_index = _PerfTestBase._global_parallel_index
119-
_PerfTestBase._global_parallel_index += 1
116+
self._parallel_index = _PerfTestBase._global_parallel_index
117+
_PerfTestBase._global_parallel_index += 1
120118

121119
@property
122120
def completed_operations(self) -> int:
@@ -136,14 +134,14 @@ def last_completion_time(self) -> float:
136134

137135
async def global_setup(self) -> None:
138136
"""
139-
Setup called once across all parallel test instances.
137+
Setup called once per process across all threaded test instances.
140138
Used to setup state that can be used by all test instances.
141139
"""
142140
return
143141

144142
async def global_cleanup(self) -> None:
145143
"""
146-
Cleanup called once across all parallel test instances.
144+
Cleanup called once per process across all threaded test instances.
147145
Used to cleanup state that can be used by all test instances.
148146
"""
149147
return

0 commit comments

Comments
 (0)