Skip to content

Commit db4fa7d

Browse files
committed
gh-160529: Accept synthetic profiler frames and resolve BusyBox executables
1 parent 5ca490b commit db4fa7d

3 files changed

Lines changed: 29 additions & 5 deletions

File tree

‎Lib/test/test_profiling/test_sampling_profiler/test_children.py‎

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import argparse
44
import io
55
import os
6+
import shutil
67
import signal
78
import subprocess
89
import sys
@@ -85,12 +86,11 @@ def reader():
8586
return result[0]
8687

8788

88-
def _wait_for_process_exec(proc, name, timeout):
89+
def _wait_for_process_exec(proc, executable, timeout):
8990
# Wait until a child process calls exec() to replace the forked process
9091
# executable. Raise AssertionError on timeout.
9192
for _ in sleeping_retry(timeout, max_delay=0.1):
92-
exe = os.readlink(f"/proc/{proc.pid}/exe")
93-
if os.path.basename(exe) == name:
93+
if os.path.samefile(f"/proc/{proc.pid}/exe", executable):
9494
break
9595

9696

@@ -617,15 +617,18 @@ def test_is_python_process_python_subprocess(self):
617617
@unittest.skipUnless(sys.platform == "linux", "Linux only test")
618618
def test_is_python_process_non_python_subprocess(self):
619619
"""Test that a non-Python subprocess is not detected as Python."""
620+
executable = shutil.which("sleep")
621+
if executable is None:
622+
self.skipTest("sleep command not found")
620623
# Start a non-Python subprocess (sleep command)
621624
proc = subprocess.Popen(
622-
["sleep", "60"],
625+
[executable, "60"],
623626
stdout=subprocess.DEVNULL,
624627
stderr=subprocess.DEVNULL,
625628
)
626629

627630
try:
628-
_wait_for_process_exec(proc, "sleep", SHORT_TIMEOUT)
631+
_wait_for_process_exec(proc, executable, SHORT_TIMEOUT)
629632

630633
self.assertFalse(
631634
is_python_process(proc.pid),

‎Lib/test/test_profiling/test_sampling_profiler/test_collectors.py‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -467,6 +467,24 @@ def test_collapsed_stack_collector_export(self):
467467
self.assertIn(stack1_expected, lines)
468468
self.assertIn(stack2_expected, lines)
469469

470+
def test_collapsed_stack_collector_export_synthetic_frames(self):
471+
collapsed_out = tempfile.NamedTemporaryFile(delete=False)
472+
self.addCleanup(close_and_unlink, collapsed_out)
473+
474+
for name in ("<GC>", "<native>", "<unreadable frame>"):
475+
with self.subTest(name=name):
476+
collector = CollapsedStackCollector(1000)
477+
collector.collect([
478+
MockInterpreterInfo(0, [MockThreadInfo(1, [
479+
_remote_debugging.FrameInfo(("~", None, name, None)),
480+
MockFrameInfo("app.py", 10, "main"),
481+
])])
482+
])
483+
with captured_stdout():
484+
collector.export(collapsed_out.name)
485+
with open(collapsed_out.name, encoding="utf-8") as f:
486+
self.assertEqual(f.read(), f"tid:1;app.py:main:10;{name} 1\n")
487+
470488
def test_collapsed_stack_collector_export_non_ascii_names(self):
471489
# gh-156810: frame names are written verbatim, so the output must be
472490
# opened with an encoding that can represent non-ASCII and

‎Lib/test/test_profiling/test_sampling_profiler/test_integration.py‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -508,6 +508,9 @@ def test_sampling_with_collapsed_export(self):
508508
if ";" in stack_trace:
509509
stack_parts = stack_trace.split(";")
510510
for part in stack_parts:
511+
# Synthetic frames contain only their name.
512+
if part in {"<GC>", "<native>", "<unreadable frame>"}:
513+
continue
511514
# Each part should be file:function:line
512515
self.assertIn(":", part)
513516

0 commit comments

Comments
 (0)