Skip to content

Commit d4d7098

Browse files
author
rodrigo.nogueira
committed
refactor(benchmark): use factory functions for loop isolation
Each benchmark test now creates a fresh alru_cache instance via factory functions instead of reusing global decorated functions. This ensures each test runs with its own cache bound to the test's event loop, preventing RuntimeError from the new event loop affinity check.
1 parent 54890fd commit d4d7098

1 file changed

Lines changed: 105 additions & 67 deletions

File tree

benchmark.py

Lines changed: 105 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,17 @@
1717

1818
@pytest.fixture
1919
def loop():
20-
loop = asyncio.new_event_loop()
21-
asyncio.set_event_loop(loop)
22-
yield loop
23-
loop.close()
20+
# Save current loop to restore after the test
21+
try:
22+
old_loop = asyncio.get_running_loop()
23+
except RuntimeError:
24+
old_loop = None
25+
new_loop = asyncio.new_event_loop()
26+
asyncio.set_event_loop(new_loop)
27+
yield new_loop
28+
new_loop.close()
29+
if old_loop is not None:
30+
asyncio.set_event_loop(old_loop)
2431

2532

2633
@pytest.fixture
@@ -38,54 +45,81 @@ def run_the_loop(fn, *args, **kwargs):
3845

3946

4047
# Bounded cache (LRU)
41-
@alru_cache(maxsize=128)
42-
async def cached_func(x):
48+
async def _cached_func(x):
4349
return x
4450

4551

46-
@alru_cache(maxsize=16, ttl=0.01)
47-
async def cached_func_ttl(x):
52+
def create_cached_func():
53+
return alru_cache(maxsize=128)(_cached_func)
54+
55+
56+
async def _cached_func_ttl(x):
4857
return x
4958

5059

60+
def create_cached_func_ttl():
61+
return alru_cache(maxsize=16, ttl=0.01)(_cached_func_ttl)
62+
63+
5164
# Unbounded cache (no maxsize)
52-
@alru_cache()
53-
async def cached_func_unbounded(x):
65+
async def _cached_func_unbounded(x):
5466
return x
5567

5668

57-
@alru_cache(ttl=0.01)
58-
async def cached_func_unbounded_ttl(x):
69+
def create_cached_func_unbounded():
70+
return alru_cache()(_cached_func_unbounded)
71+
72+
73+
async def _cached_func_unbounded_ttl(x):
5974
return x
6075

6176

62-
class Methods:
63-
@alru_cache(maxsize=128)
64-
async def cached_meth(self, x):
65-
return x
77+
def create_cached_func_unbounded_ttl():
78+
return alru_cache(ttl=0.01)(_cached_func_unbounded_ttl)
79+
6680

67-
@alru_cache(maxsize=16, ttl=0.01)
68-
async def cached_meth_ttl(self, x):
69-
return x
7081

71-
@alru_cache()
72-
async def cached_meth_unbounded(self, x):
73-
return x
82+
def create_cached_meth():
83+
class MethodsInstance:
84+
@alru_cache(maxsize=128)
85+
async def cached_meth(self, x):
86+
return x
87+
return MethodsInstance().cached_meth
7488

75-
@alru_cache(ttl=0.01)
76-
async def cached_meth_unbounded_ttl(self, x):
77-
return x
89+
90+
def create_cached_meth_ttl():
91+
class MethodsInstance:
92+
@alru_cache(maxsize=16, ttl=0.01)
93+
async def cached_meth_ttl(self, x):
94+
return x
95+
return MethodsInstance().cached_meth_ttl
96+
97+
98+
def create_cached_meth_unbounded():
99+
class MethodsInstance:
100+
@alru_cache()
101+
async def cached_meth_unbounded(self, x):
102+
return x
103+
return MethodsInstance().cached_meth_unbounded
104+
105+
106+
def create_cached_meth_unbounded_ttl():
107+
class MethodsInstance:
108+
@alru_cache(ttl=0.01)
109+
async def cached_meth_unbounded_ttl(self, x):
110+
return x
111+
return MethodsInstance().cached_meth_unbounded_ttl
78112

79113

80114
async def uncached_func(x):
81115
return x
82116

83117

84118
funcs_no_ttl = [
85-
cached_func,
86-
cached_func_unbounded,
87-
Methods.cached_meth,
88-
Methods.cached_meth_unbounded,
119+
create_cached_func,
120+
create_cached_func_unbounded,
121+
create_cached_meth,
122+
create_cached_meth_unbounded,
89123
]
90124
no_ttl_ids = [
91125
"func-bounded",
@@ -95,10 +129,10 @@ async def uncached_func(x):
95129
]
96130

97131
funcs_ttl = [
98-
cached_func_ttl,
99-
cached_func_unbounded_ttl,
100-
Methods.cached_meth_ttl,
101-
Methods.cached_meth_unbounded_ttl,
132+
create_cached_func_ttl,
133+
create_cached_func_unbounded_ttl,
134+
create_cached_meth_ttl,
135+
create_cached_meth_unbounded_ttl,
102136
]
103137
ttl_ids = [
104138
"func-bounded-ttl",
@@ -111,13 +145,13 @@ async def uncached_func(x):
111145
all_ids = [*no_ttl_ids, *ttl_ids]
112146

113147

114-
@pytest.mark.parametrize("func", all_funcs, ids=all_ids)
148+
@pytest.mark.parametrize("factory", all_funcs, ids=all_ids)
115149
def test_cache_hit_benchmark(
116150
benchmark: BenchmarkFixture,
117151
run_loop: Callable[..., Any],
118-
func: _LRUCacheWrapper[Any],
152+
factory: Callable[[], _LRUCacheWrapper[Any]],
119153
) -> None:
120-
# Populate cache
154+
func = factory()
121155
keys = list(range(10))
122156
for key in keys:
123157
run_loop(func, key)
@@ -130,14 +164,15 @@ async def run() -> None:
130164
benchmark(run_loop, run)
131165

132166

133-
@pytest.mark.parametrize("func", all_funcs, ids=all_ids)
167+
@pytest.mark.parametrize("factory", all_funcs, ids=all_ids)
134168
def test_cache_miss_benchmark(
135169
benchmark: BenchmarkFixture,
136170
run_loop: Callable[..., Any],
137-
func: _LRUCacheWrapper[Any],
171+
factory: Callable[[], _LRUCacheWrapper[Any]],
138172
) -> None:
139-
unique_objects = [object() for _ in range(128)]
140-
func.cache_clear()
173+
func = factory()
174+
# Use 2048 objects (16x maxsize=128) to force evictions and measure actual misses
175+
unique_objects = [object() for _ in range(2048)]
141176

142177
async def run() -> None:
143178
for obj in unique_objects:
@@ -146,37 +181,39 @@ async def run() -> None:
146181
benchmark(run_loop, run)
147182

148183

149-
@pytest.mark.parametrize("func", all_funcs, ids=all_ids)
184+
@pytest.mark.parametrize("factory", all_funcs, ids=all_ids)
150185
def test_cache_clear_benchmark(
151186
benchmark: BenchmarkFixture,
152187
run_loop: Callable[..., Any],
153-
func: _LRUCacheWrapper[Any],
188+
factory: Callable[[], _LRUCacheWrapper[Any]],
154189
) -> None:
190+
func = factory()
155191
for i in range(100):
156192
run_loop(func, i)
157193

158194
benchmark(func.cache_clear)
159195

160196

161-
@pytest.mark.parametrize("func_ttl", funcs_ttl, ids=ttl_ids)
197+
@pytest.mark.parametrize("factory", funcs_ttl, ids=ttl_ids)
162198
def test_cache_ttl_expiry_benchmark(
163199
benchmark: BenchmarkFixture,
164200
run_loop: Callable[..., Any],
165-
func_ttl: _LRUCacheWrapper[Any],
201+
factory: Callable[[], _LRUCacheWrapper[Any]],
166202
) -> None:
203+
func_ttl = factory()
167204
run_loop(func_ttl, 99)
168205
run_loop(asyncio.sleep, 0.02)
169206

170207
benchmark(run_loop, func_ttl, 99)
171208

172209

173-
@pytest.mark.parametrize("func", all_funcs, ids=all_ids)
210+
@pytest.mark.parametrize("factory", all_funcs, ids=all_ids)
174211
def test_cache_invalidate_benchmark(
175212
benchmark: BenchmarkFixture,
176213
run_loop: Callable[..., Any],
177-
func: _LRUCacheWrapper[Any],
214+
factory: Callable[[], _LRUCacheWrapper[Any]],
178215
) -> None:
179-
# Populate cache
216+
func = factory()
180217
keys = list(range(123, 321))
181218
for i in keys:
182219
run_loop(func, i)
@@ -189,13 +226,13 @@ def run() -> None:
189226
invalidate(i)
190227

191228

192-
@pytest.mark.parametrize("func", all_funcs, ids=all_ids)
229+
@pytest.mark.parametrize("factory", all_funcs, ids=all_ids)
193230
def test_cache_info_benchmark(
194231
benchmark: BenchmarkFixture,
195232
run_loop: Callable[..., Any],
196-
func: _LRUCacheWrapper[Any],
233+
factory: Callable[[], _LRUCacheWrapper[Any]],
197234
) -> None:
198-
# Populate cache
235+
func = factory()
199236
keys = list(range(1000))
200237
for i in keys:
201238
run_loop(func, i)
@@ -208,37 +245,37 @@ def run() -> None:
208245
cache_info()
209246

210247

211-
@pytest.mark.parametrize("func", all_funcs, ids=all_ids)
248+
@pytest.mark.parametrize("factory", all_funcs, ids=all_ids)
212249
def test_concurrent_cache_hit_benchmark(
213250
benchmark: BenchmarkFixture,
214251
run_loop: Callable[..., Any],
215-
func: _LRUCacheWrapper[Any],
252+
factory: Callable[[], _LRUCacheWrapper[Any]],
216253
) -> None:
217-
# Populate cache
254+
func = factory()
218255
keys = list(range(600, 700))
219256
for key in keys:
220257
run_loop(func, key)
221258

222259
async def gather_coros():
223260
gather = asyncio.gather
224261
for _ in range(10):
225-
return await gather(*map(func, keys))
262+
_ = await gather(*map(func, keys))
226263

227264
benchmark(run_loop, gather_coros)
228265

229266

230267
def test_cache_fill_eviction_benchmark(
231268
benchmark: BenchmarkFixture, run_loop: Callable[..., Any]
232269
) -> None:
233-
# Populate cache
270+
func = create_cached_func()
234271
for i in range(-128, 0):
235-
run_loop(cached_func, i)
272+
run_loop(func, i)
236273

237274
keys = list(range(5000))
238275

239276
async def fill():
240277
for k in keys:
241-
await cached_func(k)
278+
await func(k)
242279

243280
benchmark(run_loop, fill)
244281

@@ -252,20 +289,20 @@ async def fill():
252289
# The relevant internal methods do not exist on _LRUCacheWrapperInstanceMethod,
253290
# so we can skip methods for this part of the benchmark suite.
254291
# We also skip wrappers with ttl because it raises KeyError.
255-
only_funcs_no_ttl = all_funcs[:2]
256-
func_ids_no_ttl = all_ids[:2]
292+
only_funcs_no_ttl = funcs_no_ttl[:2]
293+
func_ids_no_ttl = no_ttl_ids[:2]
257294

258295

259-
@pytest.mark.parametrize("func", only_funcs_no_ttl, ids=func_ids_no_ttl)
296+
@pytest.mark.parametrize("factory", only_funcs_no_ttl, ids=func_ids_no_ttl)
260297
def test_internal_cache_hit_microbenchmark(
261298
benchmark: BenchmarkFixture,
262299
run_loop: Callable[..., Any],
263-
func: _LRUCacheWrapper[Any],
300+
factory: Callable[[], _LRUCacheWrapper[Any]],
264301
) -> None:
265302
"""Directly benchmark _cache_hit (internal, sync) using parameterized funcs."""
303+
func = factory()
266304
cache_hit = func._cache_hit
267305

268-
# Populate cache
269306
keys = list(range(128))
270307
for i in keys:
271308
run_loop(func, i)
@@ -276,11 +313,12 @@ def run() -> None:
276313
cache_hit(i)
277314

278315

279-
@pytest.mark.parametrize("func", only_funcs_no_ttl, ids=func_ids_no_ttl)
316+
@pytest.mark.parametrize("factory", only_funcs_no_ttl, ids=func_ids_no_ttl)
280317
def test_internal_cache_miss_microbenchmark(
281-
benchmark: BenchmarkFixture, func: _LRUCacheWrapper[Any]
318+
benchmark: BenchmarkFixture, factory: Callable[[], _LRUCacheWrapper[Any]]
282319
) -> None:
283320
"""Directly benchmark _cache_miss (internal, sync) using parameterized funcs."""
321+
func = factory()
284322
cache_miss = func._cache_miss
285323

286324
@benchmark
@@ -289,17 +327,17 @@ def run() -> None:
289327
cache_miss(i)
290328

291329

292-
@pytest.mark.parametrize("func", only_funcs_no_ttl, ids=func_ids_no_ttl)
330+
@pytest.mark.parametrize("factory", only_funcs_no_ttl, ids=func_ids_no_ttl)
293331
@pytest.mark.parametrize("task_state", ["finished", "cancelled", "exception"])
294332
def test_internal_task_done_callback_microbenchmark(
295333
benchmark: BenchmarkFixture,
296334
loop: asyncio.BaseEventLoop,
297-
func: _LRUCacheWrapper[Any],
335+
factory: Callable[[], _LRUCacheWrapper[Any]],
298336
task_state: str,
299337
) -> None:
300338
"""Directly benchmark _task_done_callback (internal, sync) using parameterized funcs and task states."""
339+
func = factory()
301340

302-
# Create a dummy coroutine and task
303341
async def dummy_coro():
304342
if task_state == "exception":
305343
raise ValueError("test exception")

0 commit comments

Comments
 (0)