@@ -623,3 +623,158 @@ def test_profile_generated_code():
623623
624624 # .. as well as the generated code name
625625 assert generated_code_name in output
626+
627+
628+ def test_multiple_profilers_metadata ():
629+ """
630+ Test the curation of profiler metadata (e.g. `.code_hash_map`,
631+ `.dupes_map`, `.code_map`) from the underlying C-level profiler.
632+ """
633+ from copy import deepcopy
634+ from operator import attrgetter
635+ from warnings import warn
636+
637+ prof1 = LineProfiler ()
638+ prof2 = LineProfiler ()
639+ cprof = prof1 ._get_c_profiler ()
640+ assert prof2 ._get_c_profiler () is cprof
641+
642+ @prof1
643+ @prof2
644+ def f (c = False ):
645+ get_time = attrgetter ('c_last_time' if c else 'last_time' )
646+ t1 = get_time (prof1 )
647+ t2 = get_time (prof2 )
648+ return [t1 , t2 , get_time (cprof )]
649+
650+ @prof1
651+ def g ():
652+ return [prof1 .enable_count , prof2 .enable_count ]
653+
654+ @prof2
655+ def h (): # Same bytecode as `g()`
656+ return [prof1 .enable_count , prof2 .enable_count ]
657+
658+ get_code = attrgetter ('__wrapped__.__code__' )
659+
660+ # `.functions`
661+ assert prof1 .functions == [f .__wrapped__ , g .__wrapped__ ]
662+ assert prof2 .functions == [f .__wrapped__ , h .__wrapped__ ]
663+ # `.enable_count`
664+ # (Note: `.enable_count` is automatically in-/de-cremented in
665+ # decorated functions, so we need to access it within a called
666+ # function)
667+ assert g () == [1 , 0 ]
668+ assert h () == [0 , 1 ]
669+ assert prof1 .enable_count == prof2 .enable_count == cprof .enable_count == 0
670+ # `.timer_unit`
671+ assert prof1 .timer_unit == prof2 .timer_unit == cprof .timer_unit
672+ # `.code_hash_map`
673+ assert set (prof1 .code_hash_map ) == {get_code (f ), get_code (g )}
674+ assert set (prof2 .code_hash_map ) == {get_code (f ), get_code (h )}
675+
676+ # `.c_code_map`
677+ prof1_line_hashes = {h for hashes in prof1 .code_hash_map .values ()
678+ for h in hashes }
679+ assert set (prof1 .c_code_map ) == prof1_line_hashes
680+ prof2_line_hashes = {h for hashes in prof2 .code_hash_map .values ()
681+ for h in hashes }
682+ assert set (prof2 .c_code_map ) == prof2_line_hashes
683+ # `.code_map`
684+ assert set (prof1 .code_map ) == {get_code (f ), get_code (g )}
685+ assert len (prof1 .code_map [get_code (f )]) == 0
686+ assert len (prof1 .code_map [get_code (g )]) == 1
687+ assert set (prof2 .code_map ) == {get_code (f ), get_code (h )}
688+ assert len (prof2 .code_map [get_code (f )]) == 0
689+ assert len (prof2 .code_map [get_code (h )]) == 1
690+ t1 , t2 , _ = f () # Timing info gathered after calling the function
691+ assert len (prof1 .code_map [get_code (f )]) == 4 # 4 real lines
692+ assert len (prof2 .code_map [get_code (f )]) == 4
693+
694+ # `.c_last_time`
695+ # (Note: `.c_last_time` is transient, so we need to access it within
696+ # a called function)
697+ ct1 , ct2 , _ = f (c = True )
698+ assert set (ct1 ) == set (ct2 ) == {hash (get_code (f ).co_code )}
699+ # `.last_time`
700+ # (Note: `.last_time` is currently bugged; since `.c_last_time`
701+ # stores code-block hashes and `.code_hash_map` line hashes,
702+ # `line_profiler._line_profiler.LineProfiler.last_time` never gets a
703+ # hash match and is thus always empty)
704+ t1 , t2 , tc = f (c = False )
705+ if tc :
706+ expected = {get_code (f )}
707+ else :
708+ msg = ('`line_profiler/_line_profiler.pyx::LineProfiler.last_time` '
709+ 'is always empty because `.c_last_time` and `.code_hash_map` '
710+ 'use different types of hashes (see PR #344)' )
711+ warn (msg , DeprecationWarning )
712+ expected = set ()
713+ assert set (t1 ) == set (t2 ) == set (tc ) == expected
714+
715+ # `.dupes_map` (introduce a dupe for this)
716+ # Note: `h.__wrapped__.__code__` is padded but the `.dupes_map`
717+ # entries are not
718+ assert prof1 .dupes_map == {get_code (f ).co_code : [get_code (f )],
719+ get_code (g ).co_code : [get_code (g )]}
720+ h = prof1 (h )
721+ dupes = deepcopy (prof1 .dupes_map )
722+ h_code = dupes [get_code (g ).co_code ][- 1 ]
723+ assert get_code (h ).co_code .startswith (h_code .co_code )
724+ dupes [get_code (g ).co_code ][- 1 ] = (h_code
725+ .replace (co_code = get_code (h ).co_code ))
726+ assert dupes == {get_code (f ).co_code : [get_code (f )],
727+ get_code (g ).co_code : [get_code (g ), get_code (h )]}
728+
729+
730+ def test_multiple_profilers_usage ():
731+ """
732+ Test using more than one profilers simultaneously.
733+ """
734+ prof1 = LineProfiler ()
735+ prof2 = LineProfiler ()
736+
737+ def sum_n (n ):
738+ x = 0
739+ for n in range (1 , n + 1 ):
740+ x += n
741+ return x
742+
743+ @prof1
744+ def sum_n_sq (n ):
745+ x = 0
746+ for n in range (1 , n + 1 ):
747+ x += n ** 2
748+ return x
749+
750+ @prof2
751+ def sum_n_cb (n ):
752+ x = 0
753+ for n in range (1 , n + 1 ):
754+ x += n ** 3
755+ return x
756+
757+ # If we decorate a wrapper, just "register" the profiler with the
758+ # existing wrapper and add the wrapped function
759+ sum_n_wrapper = prof1 (sum_n )
760+ assert prof1 .functions == [sum_n_sq .__wrapped__ , sum_n ]
761+ sum_n_wrapper_2 = prof2 (sum_n_wrapper )
762+ assert prof2 .functions == [sum_n_cb .__wrapped__ , sum_n ]
763+ assert sum_n_wrapper_2 is sum_n_wrapper
764+
765+ # Call the functions
766+ n = 400
767+ assert sum_n_wrapper (n ) == .5 * n * (n + 1 )
768+ assert 6 * sum_n_sq (n ) == n * (n + 1 ) * (2 * n + 1 )
769+ assert sum_n_cb (n ) == .25 * (n * (n + 1 )) ** 2
770+
771+ # Inspect the timings
772+ t1 = {fname : entries
773+ for (* _ , fname ), entries in prof1 .get_stats ().timings .items ()}
774+ t2 = {fname : entries
775+ for (* _ , fname ), entries in prof2 .get_stats ().timings .items ()}
776+ assert set (t1 ) == {'sum_n_sq' , 'sum_n' }
777+ assert set (t2 ) == {'sum_n_cb' , 'sum_n' }
778+ assert t1 ['sum_n' ][2 ][1 ] == t2 ['sum_n' ][2 ][1 ] == n
779+ assert t1 ['sum_n_sq' ][2 ][1 ] == n
780+ assert t2 ['sum_n_cb' ][2 ][1 ] == n
0 commit comments