@@ -1840,6 +1840,123 @@ def test_diff_flamegraph_function_matched_despite_line_change(self):
18401840 self .assertAlmostEqual (child ["diff" ], 0.0 , places = 1 )
18411841 self .assertAlmostEqual (child ["diff_pct" ], 0.0 , places = 1 )
18421842
1843+ def test_diff_flamegraph_does_not_duplicate_line_values (self ):
1844+ """Function aggregates are apportioned across line nodes."""
1845+ def sample (line ):
1846+ return [
1847+ MockInterpreterInfo (0 , [
1848+ MockThreadInfo (1 , [
1849+ MockFrameInfo ("file.py" , line , "func" ),
1850+ MockFrameInfo ("file.py" , 1 , "caller" ),
1851+ ])
1852+ ])
1853+ ]
1854+
1855+ diff = make_diff_collector_with_mock_baseline (
1856+ [sample (10 ), sample (20 )]
1857+ )
1858+ diff .collect (sample (10 ))
1859+ diff .collect (sample (20 ))
1860+
1861+ data = diff ._convert_to_flamegraph_format ()
1862+ children = data ["children" ]
1863+ self .assertEqual (sum (node ["self" ] for node in children ), 2 )
1864+ self .assertEqual (sum (node ["self_time" ] for node in children ), 2 )
1865+ self .assertEqual (sum (node ["baseline" ] for node in children ), 2 )
1866+ for node in children :
1867+ self .assertEqual (node ["self" ], 1 )
1868+ self .assertEqual (node ["self_time" ], 1 )
1869+ self .assertAlmostEqual (node ["baseline" ], 1.0 )
1870+ self .assertAlmostEqual (node ["diff" ], 0.0 )
1871+
1872+ def test_diff_flamegraph_line_totals_include_allocated_self (self ):
1873+ """A line's baseline self time cannot exceed its inclusive time."""
1874+ def sample (* frames ):
1875+ return [
1876+ MockInterpreterInfo (0 , [MockThreadInfo (1 , list (frames ))])
1877+ ]
1878+
1879+ target_10 = MockFrameInfo ("file.py" , 10 , "target" )
1880+ target_20 = MockFrameInfo ("file.py" , 20 , "target" )
1881+ child = MockFrameInfo ("file.py" , 30 , "child" )
1882+
1883+ diff = make_diff_collector_with_mock_baseline (
1884+ [sample (target_10 )] * 100
1885+ )
1886+ for _ in range (10 ):
1887+ diff .collect (sample (target_10 ))
1888+ for _ in range (90 ):
1889+ diff .collect (sample (child , target_20 ))
1890+
1891+ data = diff ._convert_to_flamegraph_format ()
1892+ nodes = data ["children" ]
1893+ self .assertEqual (sum (node ["baseline" ] for node in nodes ), 100 )
1894+ self .assertEqual (sum (node ["baseline_total" ] for node in nodes ), 100 )
1895+ for node in nodes :
1896+ self .assertGreaterEqual (node ["baseline" ], 0 )
1897+ self .assertLessEqual (node ["baseline" ], node ["baseline_total" ])
1898+
1899+ def test_diff_flamegraph_does_not_duplicate_elided_line_values (self ):
1900+ """Elided metadata uses each rendered line node's samples."""
1901+ def sample (line , funcname = "old_func" ):
1902+ return [
1903+ MockInterpreterInfo (0 , [
1904+ MockThreadInfo (1 , [
1905+ MockFrameInfo ("file.py" , line , funcname ),
1906+ MockFrameInfo ("file.py" , 1 , "caller" ),
1907+ ])
1908+ ])
1909+ ]
1910+
1911+ diff = make_diff_collector_with_mock_baseline (
1912+ [sample (10 ), sample (20 )]
1913+ )
1914+ diff .collect (sample (30 , "new_func" ))
1915+
1916+ data = diff ._convert_to_flamegraph_format ()
1917+ elided = data ["stats" ]["elided_flamegraph" ]
1918+ children = elided ["children" ]
1919+ scale = data ["stats" ]["baseline_scale" ]
1920+ self .assertEqual (sum (node ["self" ] for node in children ), 2 )
1921+ self .assertEqual (sum (node ["baseline" ] for node in children ), 2 * scale )
1922+ for node in children :
1923+ self .assertEqual (node ["self" ], 1 )
1924+ self .assertAlmostEqual (node ["baseline" ], scale )
1925+ self .assertAlmostEqual (node ["diff" ], - scale )
1926+
1927+ def test_diff_flamegraph_elided_ancestors_have_no_lost_self_time (self ):
1928+ """Matched ancestors only carry inclusive elided geometry."""
1929+ root = MockFrameInfo ("file.py" , 10 , "root" )
1930+ common = MockFrameInfo ("file.py" , 20 , "common" , opcode = 100 )
1931+ old = MockFrameInfo ("file.py" , 30 , "old" )
1932+
1933+ common_sample = [
1934+ MockInterpreterInfo (0 , [MockThreadInfo (1 , [common , root ])])
1935+ ]
1936+ old_sample = [
1937+ MockInterpreterInfo (0 , [MockThreadInfo (1 , [old , common , root ])])
1938+ ]
1939+
1940+ diff = make_diff_collector_with_mock_baseline (
1941+ [common_sample ] * 3 + [old_sample ]
1942+ )
1943+ diff .collect (common_sample )
1944+
1945+ data = diff ._convert_to_flamegraph_format ()
1946+ elided_root = data ["stats" ]["elided_flamegraph" ]
1947+ common_node = elided_root ["children" ][0 ]
1948+ old_node = common_node ["children" ][0 ]
1949+
1950+ for ancestor in (elided_root , common_node ):
1951+ self .assertEqual (ancestor ["self" ], 0 )
1952+ self .assertEqual (ancestor ["baseline" ], 0 )
1953+ self .assertNotIn ("opcodes" , ancestor )
1954+ self .assertLessEqual (
1955+ ancestor ["baseline" ], ancestor ["baseline_total" ]
1956+ )
1957+ self .assertEqual (old_node ["self" ], 1 )
1958+ self .assertEqual (old_node ["baseline" ], old_node ["baseline_total" ])
1959+
18431960 def test_diff_flamegraph_empty_current (self ):
18441961 """Empty current profile still produces differential metadata and elided paths."""
18451962 baseline_frames = [
0 commit comments