Skip to content

Commit e239dcd

Browse files
authored
Merge pull request #255 from seddonym/update-benchmarks
Update benchmarks
2 parents f6d1c4b + 78ac056 commit e239dcd

File tree

3 files changed

+86
-93
lines changed

3 files changed

+86
-93
lines changed

justfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ benchmark-local:
138138
# Show recent local benchmark results.
139139
[group('benchmarking')]
140140
show-benchmark-results:
141-
@uv run --group=benchmark-local pytest-benchmark compare --group-by=fullname --sort=name --columns=mean
141+
@uv run pytest-benchmark compare --group-by=fullname --sort=name --columns=mean
142142

143143
# Run benchmarks using Codspeed. This only works in CI.
144144
[group('benchmarking')]

tests/benchmarking/adaptors.py

Lines changed: 0 additions & 20 deletions
This file was deleted.

tests/benchmarking/test_benchmarking.py

Lines changed: 85 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,14 @@
1+
import uuid
2+
import random
13
import pytest
24
import json
35
import importlib
46
from pathlib import Path
57

6-
from tests.config import override_settings
78
from grimp.application.graph import ImportGraph
89
from grimp import PackageDependency, Route
910
import grimp
1011
from copy import deepcopy
11-
from .adaptors import PrefixMissingCache
12-
13-
14-
def _run_benchmark(benchmark, fn, *args, **kwargs):
15-
return benchmark(fn, *args, **kwargs)
1612

1713

1814
@pytest.fixture(scope="module")
@@ -59,14 +55,16 @@ def large_graph():
5955
middle=(),
6056
tails=frozenset(
6157
{
62-
"mypackage.application.7537183614.6928774480.5676105139.3275676604" # noqa:E501
58+
"mypackage.application.7537183614.6928774480.5676105139.3275676604"
59+
# noqa:E501
6360
}
6461
),
6562
),
6663
Route(
6764
heads=frozenset(
6865
{
69-
"mypackage.domain.6928774480.5676105139.1330171288.7588443317.4661445087" # noqa:E501
66+
"mypackage.domain.6928774480.5676105139.1330171288.7588443317.4661445087"
67+
# noqa:E501
7068
}
7169
),
7270
middle=(),
@@ -87,7 +85,8 @@ def large_graph():
8785
Route(
8886
heads=frozenset(
8987
{
90-
"mypackage.domain.6928774480.1028759677.7960519247.2888779155.7486857426" # noqa:E501
88+
"mypackage.domain.6928774480.1028759677.7960519247.2888779155.7486857426"
89+
# noqa:E501
9190
}
9291
),
9392
middle=(),
@@ -139,15 +138,19 @@ def large_graph():
139138
Route(
140139
heads=frozenset(
141140
{
142-
"mypackage.application.7537183614.2538372545.1153384736.6297289996", # noqa:E501
143-
"mypackage.application.7537183614.2538372545.1153384736.6404547812.6297289996", # noqa:E501
141+
"mypackage.application.7537183614.2538372545.1153384736.6297289996",
142+
# noqa:E501
143+
"mypackage.application.7537183614.2538372545.1153384736.6404547812.6297289996",
144+
# noqa:E501
144145
}
145146
),
146147
middle=("mypackage.6398020133.9075581450.6529869526.6297289996",),
147148
tails=frozenset(
148149
{
149-
"mypackage.plugins.5634303718.6180716911.7582995238.1039461003.2943193489", # noqa:E501
150-
"mypackage.plugins.5634303718.6180716911.7582995238.1039461003.6322703811", # noqa:E501
150+
"mypackage.plugins.5634303718.6180716911.7582995238.1039461003.2943193489",
151+
# noqa:E501
152+
"mypackage.plugins.5634303718.6180716911.7582995238.1039461003.6322703811",
153+
# noqa:E501
151154
}
152155
),
153156
)
@@ -300,7 +303,7 @@ def test_build_django_uncached(benchmark):
300303
301304
In this benchmark, the cache is turned off.
302305
"""
303-
_run_benchmark(benchmark, grimp.build_graph, "django", cache_dir=None)
306+
benchmark(grimp.build_graph, "django", cache_dir=None)
304307

305308

306309
def test_build_django_from_cache_no_misses(benchmark):
@@ -312,7 +315,7 @@ def test_build_django_from_cache_no_misses(benchmark):
312315
# Populate the cache first, before beginning the benchmark.
313316
grimp.build_graph("django")
314317

315-
_run_benchmark(benchmark, grimp.build_graph, "django")
318+
benchmark(grimp.build_graph, "django")
316319

317320

318321
@pytest.mark.parametrize(
@@ -323,36 +326,65 @@ def test_build_django_from_cache_no_misses(benchmark):
323326
350, # Around half the Django codebase.
324327
),
325328
)
326-
def test_build_django_from_cache_a_few_misses(benchmark, number_of_misses):
329+
def test_build_django_from_cache_a_few_misses(benchmark, number_of_misses: int):
327330
"""
328331
Benchmarks building a graph of real package - in this case Django.
329332
330333
This benchmark utilizes the cache except for a few modules, which we add.
331334
"""
332-
# We must use a special cache class, otherwise the cache will be populated
333-
# by the first iteration. It would be better to do this using a setup function,
334-
# which is supported by pytest-benchmark's pedantic mode, but not codspeed.
335-
# This won't give us a truly accurate picture, but it's better than nothing.
335+
# We need to take care in benchmarking partially populated caches, because
336+
# the benchmark may run multiple times, depending on the context in which it's run.
337+
# If we're not careful, the cache will be populated the first time and not reset
338+
# in subsequent runs.
339+
#
340+
# The benchmark fixture available here is either from pytest-benchmark (used locally)
341+
# or pytest-codspeed (used in CI). Here's how they both behave:
342+
#
343+
# - pytest-benchmark: By default, dynamically decides how many times to run the benchmark.
344+
# It does this to improve benchmarking of very fast single runs: it will run
345+
# the code many times and average the total time. That's not so important
346+
# for code that takes orders of magnitude longer than the timer resolution.
347+
#
348+
# We can override this by using benchmark.pedantic, where we specify the
349+
# number of rounds and iterations. Each iteration contains multiple rounds.
350+
#
351+
# It's also possible to provide a setup function that runs
352+
# between each iteration. A teardown function will be in the next release
353+
# but isn't in pytest-benchmark<=5.1.0.
354+
#
355+
# - pytest-codspeed: This can run in two modes, CPU instrumentation and wall time. Currently
356+
# we use CPU instrumentation as wall time is only available to
357+
# Github organizations. CPU mode will always run the benchmark once,
358+
# regardless of what rounds and iterations are specified in pedantic mode.
359+
# This mode measures the speed of the benchmark by simulating the CPU,
360+
# rather than timing how long it actually takes, so there is no point in
361+
# running it multiple times and taking an average.
362+
#
363+
# So - in this case, because we are benchmarking a relatively slow piece of code, we explicitly
364+
# turn off multiple runs, which could potentially be misleading when running locally.
336365

337-
# Add some specially-named modules which will be treated as not in the cache.
338-
django_path = Path(importlib.util.find_spec("django").origin).parent
339-
extra_modules = [
340-
django_path / f"{PrefixMissingCache.MISSING_PREFIX}{i}.py" for i in range(number_of_misses)
341-
]
342-
# Use some real python, which will take time to parse.
366+
# Populate the cache first, before beginning the benchmark.
367+
grimp.build_graph("django")
368+
# Add some modules which won't be in the cache.
369+
# (Use some real python, which will take time to parse.)
370+
django_path = Path(importlib.util.find_spec("django").origin).parent # type: ignore
343371
module_to_copy = django_path / "forms" / "forms.py"
344372
module_contents = module_to_copy.read_text()
345-
for extra_module in extra_modules:
346-
extra_module.write_text(module_contents)
347-
348-
with override_settings(CACHE_CLASS=PrefixMissingCache):
349-
# Populate the cache.
350-
grimp.build_graph("django")
373+
extra_modules = [
374+
django_path / f"module_{i}_{random.randint(100000, 999999)}.py"
375+
for i in range(number_of_misses)
376+
]
377+
for new_module in extra_modules:
378+
# Make sure the module contents aren't identical. Depending on how the cache is implemented,
379+
# perhaps this could make a difference.
380+
hash_buster = f"\n# Hash busting comment: {uuid.uuid4()}"
381+
new_module.write_text(module_contents + hash_buster)
351382

352-
_run_benchmark(benchmark, grimp.build_graph, "django")
383+
benchmark.pedantic(grimp.build_graph, ["django"], rounds=1, iterations=1)
353384

354-
# Clean up.
355-
[module.unlink() for module in extra_modules]
385+
# Delete the modules we just created.
386+
for module in extra_modules:
387+
module.unlink()
356388

357389

358390
class TestFindIllegalDependenciesForLayers:
@@ -371,8 +403,7 @@ def _remove_package_dependencies(graph, package_dependencies):
371403
return graph
372404

373405
def test_top_level_large_graph_violated(self, large_graph, benchmark):
374-
result = _run_benchmark(
375-
benchmark,
406+
result = benchmark(
376407
large_graph.find_illegal_dependencies_for_layers,
377408
layers=TOP_LEVEL_LAYERS,
378409
containers=("mypackage",),
@@ -383,59 +414,47 @@ def test_top_level_large_graph_kept(self, large_graph, benchmark):
383414
large_graph = self._remove_package_dependencies(
384415
large_graph, TOP_LEVEL_PACKAGE_DEPENDENCIES
385416
)
386-
result = _run_benchmark(
387-
benchmark,
417+
result = benchmark(
388418
large_graph.find_illegal_dependencies_for_layers,
389419
layers=TOP_LEVEL_LAYERS,
390420
containers=("mypackage",),
391421
)
392422
assert result == set()
393423

394424
def test_deep_layers_large_graph_violated(self, large_graph, benchmark):
395-
result = _run_benchmark(
396-
benchmark, large_graph.find_illegal_dependencies_for_layers, layers=DEEP_LAYERS
397-
)
425+
result = benchmark(large_graph.find_illegal_dependencies_for_layers, layers=DEEP_LAYERS)
398426
assert result == DEEP_LAYER_PACKAGE_DEPENDENCIES
399427

400428
def test_deep_layers_large_graph_kept(self, large_graph, benchmark):
401429
large_graph = self._remove_package_dependencies(
402430
large_graph, DEEP_LAYER_PACKAGE_DEPENDENCIES
403431
)
404-
result = _run_benchmark(
405-
benchmark, large_graph.find_illegal_dependencies_for_layers, layers=DEEP_LAYERS
406-
)
432+
result = benchmark(large_graph.find_illegal_dependencies_for_layers, layers=DEEP_LAYERS)
407433
assert result == set()
408434

409435

410436
def test_find_descendants(large_graph, benchmark):
411-
result = _run_benchmark(benchmark, large_graph.find_descendants, "mypackage")
437+
result = benchmark(large_graph.find_descendants, "mypackage")
412438
assert len(result) == 28222
413439

414440

415441
def test_find_downstream_modules(large_graph, benchmark):
416-
result = _run_benchmark(
417-
benchmark, large_graph.find_downstream_modules, DEEP_LAYERS[0], as_package=True
418-
)
442+
result = benchmark(large_graph.find_downstream_modules, DEEP_LAYERS[0], as_package=True)
419443
assert len(result) == 80
420444

421445

422446
def test_find_upstream_modules(large_graph, benchmark):
423-
result = _run_benchmark(
424-
benchmark, large_graph.find_upstream_modules, DEEP_LAYERS[0], as_package=True
425-
)
447+
result = benchmark(large_graph.find_upstream_modules, DEEP_LAYERS[0], as_package=True)
426448
assert len(result) == 2159
427449

428450

429451
class TestFindShortestChain:
430452
def test_chain_found(self, large_graph, benchmark):
431-
result = _run_benchmark(
432-
benchmark, large_graph.find_shortest_chain, DEEP_LAYERS[0], DEEP_LAYERS[1]
433-
)
453+
result = benchmark(large_graph.find_shortest_chain, DEEP_LAYERS[0], DEEP_LAYERS[1])
434454
assert result is not None
435455

436456
def test_no_chain(self, large_graph, benchmark):
437-
result = _run_benchmark(
438-
benchmark,
457+
result = benchmark(
439458
large_graph.find_shortest_chain,
440459
DEEP_LAYERS[0],
441460
"mypackage.data.vendors.4053192739.6373932949",
@@ -445,8 +464,7 @@ def test_no_chain(self, large_graph, benchmark):
445464

446465
class TestFindShortestChains:
447466
def test_chains_found(self, large_graph, benchmark):
448-
result = _run_benchmark(
449-
benchmark,
467+
result = benchmark(
450468
large_graph.find_shortest_chains,
451469
DEEP_LAYERS[0],
452470
DEEP_LAYERS[1],
@@ -455,8 +473,7 @@ def test_chains_found(self, large_graph, benchmark):
455473
assert len(result) > 0
456474

457475
def test_no_chains(self, large_graph, benchmark):
458-
result = _run_benchmark(
459-
benchmark,
476+
result = benchmark(
460477
large_graph.find_shortest_chains,
461478
DEEP_LAYERS[0],
462479
"mypackage.data.vendors.4053192739.6373932949",
@@ -489,8 +506,7 @@ def test_chains_found_sparse_imports(self, benchmark):
489506
graph.add_import(importer=f"a.m{i}", imported=f"b.m{i}")
490507
graph.add_import(importer=f"b.m{i}", imported=f"c.m{i}")
491508

492-
result = _run_benchmark(
493-
benchmark,
509+
result = benchmark(
494510
graph.find_shortest_chains,
495511
"a",
496512
"c",
@@ -500,7 +516,7 @@ def test_chains_found_sparse_imports(self, benchmark):
500516

501517

502518
def test_copy_graph(large_graph, benchmark):
503-
_run_benchmark(benchmark, lambda: deepcopy(large_graph))
519+
benchmark(lambda: deepcopy(large_graph))
504520

505521

506522
def test_modules_property_first_access(large_graph, benchmark):
@@ -512,7 +528,7 @@ def f():
512528
# Accessing the modules property is what we're benchmarking.
513529
_ = large_graph.modules
514530

515-
_run_benchmark(benchmark, f)
531+
benchmark(f)
516532

517533

518534
def test_modules_property_many_accesses(large_graph, benchmark):
@@ -525,7 +541,7 @@ def f():
525541
for i in range(1000):
526542
_ = large_graph.modules
527543

528-
_run_benchmark(benchmark, f)
544+
benchmark(f)
529545

530546

531547
def test_get_import_details(benchmark):
@@ -540,19 +556,16 @@ def f():
540556
for i in range(iterations):
541557
graph.get_import_details(importer=f"blue_{i}", imported=f"green_{i}")
542558

543-
_run_benchmark(benchmark, f)
559+
benchmark(f)
544560

545561

546562
def test_find_matching_modules(benchmark, large_graph):
547-
matching_modules = _run_benchmark(
548-
benchmark, lambda: large_graph.find_matching_modules("mypackage.domain.**")
549-
)
563+
matching_modules = benchmark(lambda: large_graph.find_matching_modules("mypackage.domain.**"))
550564
assert len(matching_modules) == 2519
551565

552566

553567
def test_find_matching_direct_imports(benchmark, large_graph):
554-
matching_imports = _run_benchmark(
555-
benchmark,
568+
matching_imports = benchmark(
556569
lambda: large_graph.find_matching_direct_imports(
557570
"mypackage.domain.** -> mypackage.data.**"
558571
),

0 commit comments

Comments
 (0)