Skip to content

Commit c2203d6

Browse files
Improve tests
Clean up tests by extracting repeated logic (resolving function names and finding child nodes by name) into helper functions. Also add a test that doesn’t mock BinaryCollector to cover the full round trip.
1 parent edaac9f commit c2203d6

File tree

1 file changed

+159
-63
lines changed

1 file changed

+159
-63
lines changed

Lib/test/test_profiling/test_sampling_profiler/test_collectors.py

Lines changed: 159 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,22 @@
4040
from .helpers import close_and_unlink
4141

4242

43+
def resolve_name(node, strings):
44+
"""Resolve a flamegraph node's name from the string table."""
45+
idx = node.get("name", 0)
46+
if isinstance(idx, int) and 0 <= idx < len(strings):
47+
return strings[idx]
48+
return str(idx)
49+
50+
51+
def find_child_by_name(children, strings, substr):
52+
"""Find a child node whose resolved name contains substr."""
53+
for child in children:
54+
if substr in resolve_name(child, strings):
55+
return child
56+
return None
57+
58+
4359
class TestSampleProfilerComponents(unittest.TestCase):
4460
"""Unit tests for individual profiler components."""
4561

@@ -397,13 +413,7 @@ def test_flamegraph_collector_basic(self):
397413
data = collector._convert_to_flamegraph_format()
398414
# With string table, name is now an index - resolve it using the strings array
399415
strings = data.get("strings", [])
400-
name_index = data.get("name", 0)
401-
resolved_name = (
402-
strings[name_index]
403-
if isinstance(name_index, int) and 0 <= name_index < len(strings)
404-
else str(name_index)
405-
)
406-
self.assertIn(resolved_name, ("No Data", "No significant data"))
416+
self.assertIn(resolve_name(data, strings), ("No Data", "No significant data"))
407417

408418
# Test collecting sample data
409419
test_frames = [
@@ -422,26 +432,13 @@ def test_flamegraph_collector_basic(self):
422432
data = collector._convert_to_flamegraph_format()
423433
# Expect promotion: root is the single child (func2), with func1 as its only child
424434
strings = data.get("strings", [])
425-
name_index = data.get("name", 0)
426-
name = (
427-
strings[name_index]
428-
if isinstance(name_index, int) and 0 <= name_index < len(strings)
429-
else str(name_index)
430-
)
431-
self.assertIsInstance(name, str)
435+
name = resolve_name(data, strings)
432436
self.assertTrue(name.startswith("Program Root: "))
433437
self.assertIn("func2 (file.py:20)", name) # formatted name
434438
children = data.get("children", [])
435439
self.assertEqual(len(children), 1)
436440
child = children[0]
437-
child_name_index = child.get("name", 0)
438-
child_name = (
439-
strings[child_name_index]
440-
if isinstance(child_name_index, int)
441-
and 0 <= child_name_index < len(strings)
442-
else str(child_name_index)
443-
)
444-
self.assertIn("func1 (file.py:10)", child_name) # formatted name
441+
self.assertIn("func1 (file.py:10)", resolve_name(child, strings))
445442
self.assertEqual(child["value"], 1)
446443

447444
def test_flamegraph_collector_export(self):
@@ -1234,14 +1231,9 @@ def test_diff_flamegraph_identical_profiles(self):
12341231
children = data.get("children", [])
12351232
self.assertEqual(len(children), 1)
12361233
child = children[0]
1237-
child_name_index = child.get("name", 0)
1238-
child_name = (
1239-
strings[child_name_index]
1240-
if isinstance(child_name_index, int)
1241-
and 0 <= child_name_index < len(strings)
1242-
else str(child_name_index)
1243-
)
1244-
self.assertIn("func1", child_name)
1234+
self.assertIn("func1", resolve_name(child, strings))
1235+
self.assertEqual(child["self_time"], 3)
1236+
self.assertAlmostEqual(child["baseline"], 3.0)
12451237
self.assertAlmostEqual(child["diff"], 0.0, places=1)
12461238
self.assertAlmostEqual(child["diff_pct"], 0.0, places=1)
12471239

@@ -1276,30 +1268,66 @@ def test_diff_flamegraph_new_function(self):
12761268
children = data.get("children", [])
12771269
self.assertEqual(len(children), 1)
12781270
func1_node = children[0]
1279-
func1_name_index = func1_node.get("name", 0)
1280-
func1_name = (
1281-
strings[func1_name_index]
1282-
if isinstance(func1_name_index, int)
1283-
and 0 <= func1_name_index < len(strings)
1284-
else str(func1_name_index)
1285-
)
1286-
self.assertIn("func1", func1_name)
1271+
self.assertIn("func1", resolve_name(func1_node, strings))
12871272

12881273
func1_children = func1_node.get("children", [])
12891274
self.assertEqual(len(func1_children), 1)
12901275
new_func_node = func1_children[0]
1291-
new_func_name_index = new_func_node.get("name", 0)
1292-
new_func_name = (
1293-
strings[new_func_name_index]
1294-
if isinstance(new_func_name_index, int)
1295-
and 0 <= new_func_name_index < len(strings)
1296-
else str(new_func_name_index)
1297-
)
1298-
self.assertIn("new_func", new_func_name)
1276+
self.assertIn("new_func", resolve_name(new_func_node, strings))
12991277
self.assertEqual(new_func_node["baseline"], 0)
13001278
self.assertGreater(new_func_node["self_time"], 0)
1279+
self.assertEqual(new_func_node["diff"], new_func_node["self_time"])
13011280
self.assertAlmostEqual(new_func_node["diff_pct"], 100.0)
13021281

1282+
def test_diff_flamegraph_changed_functions(self):
1283+
"""Functions with different sample counts should have correct diff and diff_pct."""
1284+
hot_leaf_sample = [
1285+
MockInterpreterInfo(0, [
1286+
MockThreadInfo(1, [
1287+
MockFrameInfo("file.py", 10, "hot_leaf"),
1288+
MockFrameInfo("file.py", 20, "caller"),
1289+
])
1290+
])
1291+
]
1292+
cold_leaf_sample = [
1293+
MockInterpreterInfo(0, [
1294+
MockThreadInfo(1, [
1295+
MockFrameInfo("file.py", 30, "cold_leaf"),
1296+
MockFrameInfo("file.py", 20, "caller"),
1297+
])
1298+
])
1299+
]
1300+
1301+
# Baseline: 2 samples, current: 4, scale = 2.0
1302+
diff = make_diff_collector_with_mock_baseline(
1303+
[hot_leaf_sample, cold_leaf_sample]
1304+
)
1305+
for _ in range(3):
1306+
diff.collect(hot_leaf_sample)
1307+
diff.collect(cold_leaf_sample)
1308+
1309+
data = diff._convert_to_flamegraph_format()
1310+
strings = data.get("strings", [])
1311+
self.assertAlmostEqual(data["stats"]["baseline_scale"], 2.0)
1312+
1313+
children = data.get("children", [])
1314+
hot_node = find_child_by_name(children, strings, "hot_leaf")
1315+
cold_node = find_child_by_name(children, strings, "cold_leaf")
1316+
self.assertIsNotNone(hot_node)
1317+
self.assertIsNotNone(cold_node)
1318+
1319+
# hot_leaf regressed (+50%)
1320+
self.assertAlmostEqual(hot_node["baseline"], 2.0)
1321+
self.assertEqual(hot_node["self_time"], 3)
1322+
self.assertAlmostEqual(hot_node["diff"], 1.0)
1323+
self.assertAlmostEqual(hot_node["diff_pct"], 50.0)
1324+
1325+
# cold_leaf improved (-50%)
1326+
self.assertAlmostEqual(cold_node["baseline"], 2.0)
1327+
self.assertEqual(cold_node["self_time"], 1)
1328+
self.assertAlmostEqual(cold_node["diff"], -1.0)
1329+
self.assertAlmostEqual(cold_node["diff_pct"], -50.0)
1330+
13031331
def test_diff_flamegraph_scale_factor(self):
13041332
"""Scale factor adjusts when sample counts differ."""
13051333
baseline_frames = [
@@ -1318,6 +1346,14 @@ def test_diff_flamegraph_scale_factor(self):
13181346
data = diff._convert_to_flamegraph_format()
13191347
self.assertAlmostEqual(data["stats"]["baseline_scale"], 4.0)
13201348

1349+
children = data.get("children", [])
1350+
self.assertEqual(len(children), 1)
1351+
func1_node = children[0]
1352+
self.assertEqual(func1_node["self_time"], 4)
1353+
self.assertAlmostEqual(func1_node["baseline"], 4.0)
1354+
self.assertAlmostEqual(func1_node["diff"], 0.0)
1355+
self.assertAlmostEqual(func1_node["diff_pct"], 0.0)
1356+
13211357
def test_diff_flamegraph_elided_stacks(self):
13221358
"""Paths in baseline but not current produce elided stacks."""
13231359
baseline_frames_1 = [
@@ -1353,14 +1389,11 @@ def test_diff_flamegraph_elided_stacks(self):
13531389
children = elided.get("children", [])
13541390
self.assertEqual(len(children), 1)
13551391
child = children[0]
1356-
child_name_index = child.get("name", 0)
1357-
child_name = (
1358-
elided_strings[child_name_index]
1359-
if isinstance(child_name_index, int)
1360-
and 0 <= child_name_index < len(elided_strings)
1361-
else str(child_name_index)
1362-
)
1363-
self.assertIn("old_func", child_name)
1392+
self.assertIn("old_func", resolve_name(child, elided_strings))
1393+
self.assertEqual(child["self_time"], 0)
1394+
self.assertAlmostEqual(child["diff_pct"], -100.0)
1395+
self.assertGreater(child["baseline"], 0)
1396+
self.assertAlmostEqual(child["diff"], -child["baseline"])
13641397

13651398
def test_diff_flamegraph_function_matched_despite_line_change(self):
13661399
"""Functions match by (filename, funcname), ignoring lineno."""
@@ -1387,18 +1420,10 @@ def test_diff_flamegraph_function_matched_despite_line_change(self):
13871420
data = diff._convert_to_flamegraph_format()
13881421
strings = data.get("strings", [])
13891422

1390-
# func1 is child of promoted root func2
13911423
children = data.get("children", [])
13921424
self.assertEqual(len(children), 1)
13931425
child = children[0]
1394-
child_name_index = child.get("name", 0)
1395-
child_name = (
1396-
strings[child_name_index]
1397-
if isinstance(child_name_index, int)
1398-
and 0 <= child_name_index < len(strings)
1399-
else str(child_name_index)
1400-
)
1401-
self.assertIn("func1", child_name)
1426+
self.assertIn("func1", resolve_name(child, strings))
14021427
self.assertGreater(child["baseline"], 0)
14031428
self.assertGreater(child["self_time"], 0)
14041429
self.assertAlmostEqual(child["diff"], 0.0, places=1)
@@ -1522,15 +1547,86 @@ def test_diff_flamegraph_elided_preserves_metadata(self):
15221547
data = diff._convert_to_flamegraph_format()
15231548
elided = data["stats"]["elided_flamegraph"]
15241549

1550+
self.assertTrue(elided["stats"]["is_differential"])
15251551
self.assertIn("thread_stats", elided["stats"])
15261552
self.assertIn("per_thread_stats", elided["stats"])
1553+
self.assertIn("baseline_samples", elided["stats"])
1554+
self.assertIn("current_samples", elided["stats"])
1555+
self.assertIn("baseline_strings", elided)
15271556

15281557
elided_strings = elided.get("strings", [])
15291558
children = elided.get("children", [])
15301559
self.assertEqual(len(children), 1)
15311560
old_func_node = children[0]
15321561
if "opcodes" in old_func_node:
15331562
self.assertIn(200, old_func_node["opcodes"])
1563+
self.assertEqual(old_func_node["self_time"], 0)
1564+
self.assertAlmostEqual(old_func_node["diff_pct"], -100.0)
1565+
1566+
def test_diff_flamegraph_load_baseline(self):
1567+
"""Diff annotations work when baseline is loaded from a binary file."""
1568+
from profiling.sampling.binary_collector import BinaryCollector
1569+
from profiling.sampling.stack_collector import DiffFlamegraphCollector
1570+
from .test_binary_format import make_frame, make_thread, make_interpreter
1571+
1572+
hot_sample = [make_interpreter(0, [make_thread(1, [
1573+
make_frame("file.py", 10, "hot_leaf"),
1574+
make_frame("file.py", 20, "caller"),
1575+
])])]
1576+
cold_sample = [make_interpreter(0, [make_thread(1, [
1577+
make_frame("file.py", 30, "cold_leaf"),
1578+
make_frame("file.py", 20, "caller"),
1579+
])])]
1580+
1581+
# Baseline: 2 samples, current: 4, scale = 2.0
1582+
bin_file = tempfile.NamedTemporaryFile(suffix=".bin", delete=False)
1583+
self.addCleanup(close_and_unlink, bin_file)
1584+
1585+
writer = BinaryCollector(
1586+
bin_file.name, sample_interval_usec=1000, compression='none'
1587+
)
1588+
writer.collect(hot_sample)
1589+
writer.collect(cold_sample)
1590+
writer.export(None)
1591+
1592+
diff = DiffFlamegraphCollector(
1593+
1000, baseline_binary_path=bin_file.name
1594+
)
1595+
hot_mock = [MockInterpreterInfo(0, [MockThreadInfo(1, [
1596+
MockFrameInfo("file.py", 10, "hot_leaf"),
1597+
MockFrameInfo("file.py", 20, "caller"),
1598+
])])]
1599+
cold_mock = [MockInterpreterInfo(0, [MockThreadInfo(1, [
1600+
MockFrameInfo("file.py", 30, "cold_leaf"),
1601+
MockFrameInfo("file.py", 20, "caller"),
1602+
])])]
1603+
for _ in range(3):
1604+
diff.collect(hot_mock)
1605+
diff.collect(cold_mock)
1606+
1607+
data = diff._convert_to_flamegraph_format()
1608+
strings = data.get("strings", [])
1609+
1610+
self.assertTrue(data["stats"]["is_differential"])
1611+
self.assertAlmostEqual(data["stats"]["baseline_scale"], 2.0)
1612+
1613+
children = data.get("children", [])
1614+
hot_node = find_child_by_name(children, strings, "hot_leaf")
1615+
cold_node = find_child_by_name(children, strings, "cold_leaf")
1616+
self.assertIsNotNone(hot_node)
1617+
self.assertIsNotNone(cold_node)
1618+
1619+
# hot_leaf regressed (+50%)
1620+
self.assertAlmostEqual(hot_node["baseline"], 2.0)
1621+
self.assertEqual(hot_node["self_time"], 3)
1622+
self.assertAlmostEqual(hot_node["diff"], 1.0)
1623+
self.assertAlmostEqual(hot_node["diff_pct"], 50.0)
1624+
1625+
# cold_leaf improved (-50%)
1626+
self.assertAlmostEqual(cold_node["baseline"], 2.0)
1627+
self.assertEqual(cold_node["self_time"], 1)
1628+
self.assertAlmostEqual(cold_node["diff"], -1.0)
1629+
self.assertAlmostEqual(cold_node["diff_pct"], -50.0)
15341630

15351631

15361632
class TestRecursiveFunctionHandling(unittest.TestCase):

0 commit comments

Comments
 (0)