Skip to content

Commit 6e7b445

Browse files
authored
Add benchmark for async tree workloads (#187)
Add a benchmark for testing async workloads, specifically an async tree workload that simulates simpler versions of a typical Instagram endpoint. (See python/cpython#91121.)
1 parent 81d2ca2 commit 6e7b445

File tree

7 files changed

+187
-0
lines changed

7 files changed

+187
-0
lines changed

doc/benchmarks.rst

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,22 @@ depending on the Python version.
5757
them, and more generally to not modify them.
5858

5959

60+
async_tree
61+
----------
62+
63+
Async workload benchmark, which calls ``asyncio.gather()`` on a tree (6 levels deep,
64+
6 branches per level) with the leaf nodes simulating some [potentially] async work
65+
(depending on the benchmark variant). Available variants:
66+
67+
* ``async_tree``: no actual async work at any leaf node.
68+
* ``async_tree_io``: all leaf nodes simulate async IO workload (async sleep 50ms).
69+
* ``async_tree_memoization``: all leaf nodes simulate async IO workload with 90% of
70+
the data memoized.
71+
* ``async_tree_cpu_io_mixed``: half of the leaf nodes simulate CPU-bound workload
72+
(``math.factorial(500)``) and the other half simulate the same workload as the
73+
``async_tree_memoization`` variant.
74+
75+
6076
chameleon
6177
---------
6278

pyperformance/data-files/benchmarks/MANIFEST

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
name metafile
44
2to3 <local>
5+
async_tree <local>
6+
async_tree_cpu_io_mixed <local:async_tree>
7+
async_tree_io <local:async_tree>
8+
async_tree_memoization <local:async_tree>
59
chameleon <local>
610
chaos <local>
711
crypto_pyaes <local>
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
[tool.pyperformance]
2+
name = "async_tree_cpu_io_mixed"
3+
extra_opts = ["cpu_io_mixed"]
4+
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
[tool.pyperformance]
2+
name = "async_tree_io"
3+
extra_opts = ["io"]
4+
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
[tool.pyperformance]
2+
name = "async_tree_memoization"
3+
extra_opts = ["memoization"]
4+
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
[project]
2+
name = "pyperformance_bm_async_tree"
3+
requires-python = ">=3.8"
4+
dependencies = ["pyperf"]
5+
urls = {repository = "https://github.com/python/pyperformance"}
6+
dynamic = ["version"]
7+
8+
[tool.pyperformance]
9+
name = "async_tree"
10+
extra_opts = ["none"]
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
"""
2+
Benchmark for async tree workload, which calls asyncio.gather() on a tree
3+
(6 levels deep, 6 branches per level) with the leaf nodes simulating some
4+
(potentially) async work (depending on the benchmark variant). Benchmark
5+
variants include:
6+
7+
1) "none": No actual async work in the async tree.
8+
2) "io": All leaf nodes simulate async IO workload (async sleep 50ms).
9+
3) "memoization": All leaf nodes simulate async IO workload with 90% of
10+
the data memoized
11+
4) "cpu_io_mixed": Half of the leaf nodes simulate CPU-bound workload and
12+
the other half simulate the same workload as the
13+
"memoization" variant.
14+
"""
15+
16+
17+
import asyncio
18+
import math
19+
import random
20+
21+
import pyperf
22+
23+
24+
NUM_RECURSE_LEVELS = 6
25+
NUM_RECURSE_BRANCHES = 6
26+
RANDOM_SEED = 0
27+
IO_SLEEP_TIME = 0.05
28+
MEMOIZABLE_PERCENTAGE = 90
29+
CPU_PROBABILITY = 0.5
30+
FACTORIAL_N = 500
31+
32+
33+
class AsyncTree:
34+
def __init__(self):
35+
self.cache = {}
36+
# set to deterministic random, so that the results are reproducible
37+
random.seed(RANDOM_SEED)
38+
39+
async def mock_io_call(self):
40+
await asyncio.sleep(IO_SLEEP_TIME)
41+
42+
async def workload_func(self):
43+
raise NotImplementedError(
44+
"To be implemented by each variant's derived class."
45+
)
46+
47+
async def recurse(self, recurse_level):
48+
if recurse_level == 0:
49+
await self.workload_func()
50+
return
51+
52+
await asyncio.gather(
53+
*[self.recurse(recurse_level - 1) for _ in range(NUM_RECURSE_BRANCHES)]
54+
)
55+
56+
async def run(self):
57+
await self.recurse(NUM_RECURSE_LEVELS)
58+
59+
60+
class NoneAsyncTree(AsyncTree):
61+
async def workload_func(self):
62+
return
63+
64+
65+
class IOAsyncTree(AsyncTree):
66+
async def workload_func(self):
67+
await self.mock_io_call()
68+
69+
70+
class MemoizationAsyncTree(AsyncTree):
71+
async def workload_func(self):
72+
# deterministic random, seed set in AsyncTree.__init__()
73+
data = random.randint(1, 100)
74+
75+
if data <= MEMOIZABLE_PERCENTAGE:
76+
if self.cache.get(data):
77+
return data
78+
79+
self.cache[data] = True
80+
81+
await self.mock_io_call()
82+
return data
83+
84+
85+
class CpuIoMixedAsyncTree(MemoizationAsyncTree):
86+
async def workload_func(self):
87+
# deterministic random, seed set in AsyncTree.__init__()
88+
if random.random() < CPU_PROBABILITY:
89+
# mock cpu-bound call
90+
return math.factorial(FACTORIAL_N)
91+
else:
92+
return await MemoizationAsyncTree.workload_func(self)
93+
94+
95+
def add_metadata(runner):
96+
runner.metadata["description"] = "Async tree workloads."
97+
runner.metadata["async_tree_recurse_levels"] = NUM_RECURSE_LEVELS
98+
runner.metadata["async_tree_recurse_branches"] = NUM_RECURSE_BRANCHES
99+
runner.metadata["async_tree_random_seed"] = RANDOM_SEED
100+
runner.metadata["async_tree_io_sleep_time"] = IO_SLEEP_TIME
101+
runner.metadata["async_tree_memoizable_percentage"] = MEMOIZABLE_PERCENTAGE
102+
runner.metadata["async_tree_cpu_probability"] = CPU_PROBABILITY
103+
runner.metadata["async_tree_factorial_n"] = FACTORIAL_N
104+
105+
106+
def add_cmdline_args(cmd, args):
107+
cmd.append(args.benchmark)
108+
109+
110+
def add_parser_args(parser):
111+
parser.add_argument(
112+
"benchmark",
113+
choices=BENCHMARKS,
114+
help="""\
115+
Determines which benchmark to run. Options:
116+
1) "none": No actual async work in the async tree.
117+
2) "io": All leaf nodes simulate async IO workload (async sleep 50ms).
118+
3) "memoization": All leaf nodes simulate async IO workload with 90% of
119+
the data memoized
120+
4) "cpu_io_mixed": Half of the leaf nodes simulate CPU-bound workload and
121+
the other half simulate the same workload as the
122+
"memoization" variant.
123+
""",
124+
)
125+
126+
127+
BENCHMARKS = {
128+
"none": NoneAsyncTree,
129+
"io": IOAsyncTree,
130+
"memoization": MemoizationAsyncTree,
131+
"cpu_io_mixed": CpuIoMixedAsyncTree,
132+
}
133+
134+
135+
if __name__ == "__main__":
136+
runner = pyperf.Runner(add_cmdline_args=add_cmdline_args)
137+
add_metadata(runner)
138+
add_parser_args(runner.argparser)
139+
args = runner.parse_args()
140+
benchmark = args.benchmark
141+
142+
async_tree_class = BENCHMARKS[benchmark]
143+
async_tree = async_tree_class()
144+
runner.bench_async_func(f"async_tree_{benchmark}", async_tree.run)
145+

0 commit comments

Comments
 (0)