1+ import uuid
2+ import random
13import pytest
24import json
35import importlib
46from pathlib import Path
57
6- from tests .config import override_settings
78from grimp .application .graph import ImportGraph
89from grimp import PackageDependency , Route
910import grimp
1011from 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
306309def 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
358390class 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
410436def 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
415441def 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
422446def 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
429451class 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
446465class 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
502518def test_copy_graph (large_graph , benchmark ):
503- _run_benchmark ( benchmark , lambda : deepcopy (large_graph ))
519+ benchmark ( lambda : deepcopy (large_graph ))
504520
505521
506522def 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
518534def 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
531547def 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
546562def 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
553567def 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