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
1212
1313
1414@pytest .fixture (scope = "module" )
@@ -55,14 +55,16 @@ def large_graph():
5555 middle = (),
5656 tails = frozenset (
5757 {
58- "mypackage.application.7537183614.6928774480.5676105139.3275676604" # noqa:E501
58+ "mypackage.application.7537183614.6928774480.5676105139.3275676604"
59+ # noqa:E501
5960 }
6061 ),
6162 ),
6263 Route (
6364 heads = frozenset (
6465 {
65- "mypackage.domain.6928774480.5676105139.1330171288.7588443317.4661445087" # noqa:E501
66+ "mypackage.domain.6928774480.5676105139.1330171288.7588443317.4661445087"
67+ # noqa:E501
6668 }
6769 ),
6870 middle = (),
@@ -83,7 +85,8 @@ def large_graph():
8385 Route (
8486 heads = frozenset (
8587 {
86- "mypackage.domain.6928774480.1028759677.7960519247.2888779155.7486857426" # noqa:E501
88+ "mypackage.domain.6928774480.1028759677.7960519247.2888779155.7486857426"
89+ # noqa:E501
8790 }
8891 ),
8992 middle = (),
@@ -135,15 +138,19 @@ def large_graph():
135138 Route (
136139 heads = frozenset (
137140 {
138- "mypackage.application.7537183614.2538372545.1153384736.6297289996" , # noqa:E501
139- "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
140145 }
141146 ),
142147 middle = ("mypackage.6398020133.9075581450.6529869526.6297289996" ,),
143148 tails = frozenset (
144149 {
145- "mypackage.plugins.5634303718.6180716911.7582995238.1039461003.2943193489" , # noqa:E501
146- "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
147154 }
148155 ),
149156 )
@@ -319,36 +326,65 @@ def test_build_django_from_cache_no_misses(benchmark):
319326 350 , # Around half the Django codebase.
320327 ),
321328)
322- 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 ):
323330 """
324331 Benchmarks building a graph of real package - in this case Django.
325332
326333 This benchmark utilizes the cache except for a few modules, which we add.
327334 """
328- # We must use a special cache class, otherwise the cache will be populated
329- # by the first iteration. It would be better to do this using a setup function,
330- # which is supported by pytest-benchmark's pedantic mode, but not codspeed.
331- # 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.
332365
333- # Add some specially-named modules which will be treated as not in the cache.
334- django_path = Path (importlib .util .find_spec ("django" ).origin ).parent
335- extra_modules = [
336- django_path / f"{ PrefixMissingCache .MISSING_PREFIX } { i } .py" for i in range (number_of_misses )
337- ]
338- # 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
339371 module_to_copy = django_path / "forms" / "forms.py"
340372 module_contents = module_to_copy .read_text ()
341- for extra_module in extra_modules :
342- extra_module .write_text (module_contents )
343-
344- with override_settings (CACHE_CLASS = PrefixMissingCache ):
345- # Populate the cache.
346- 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 )
347382
348- benchmark (grimp .build_graph , "django" )
383+ benchmark . pedantic (grimp .build_graph , [ "django" ], rounds = 1 , iterations = 1 )
349384
350- # Clean up.
351- [module .unlink () for module in extra_modules ]
385+ # Delete the modules we just created.
386+ for module in extra_modules :
387+ module .unlink ()
352388
353389
354390class TestFindIllegalDependenciesForLayers :
0 commit comments