cpython/Lib/test/test_profiling/test_sampling_profiler/test_blocking.py
Pablo Galindo Salgado 4ceb3faf2c
[3.15] Restore deferred backports for 3.15.1, including GH-158874 (#158882)
* [3.15] gh-153364: Make frame, coroutine, and task-waiter chain walks iterative and bounded (GH-153365) (#158813)

gh-153364: Make frame, coroutine, and task-waiter chain walks iterative and bounded (GH-153365)

* let me declare single limit

* use our new limit in process_frame_chain()

* add it in parse_async_frame_chain()

* parse_coro_chain()

* NEWS

* async in the message?

* test

* no race

* process_task_awaited_by

* process_task_awaited_by limit test

* NEWS

* MAX_TASK_WAITER_CHAIN_DEPTH

* TASK_WAITER_CHAIN_DEPTH in test

* TASK_WAITER_CHAIN_DEPTH 256

* prevent the drift with the comment

* better naming, better style

* MAX_TASK_WAITER_CHAIN_DEPTH comment

* task-waiter iterative bfs walk

* iterative coro-walk

* nicer news

* 1 << 14

* comment

* unused read_Py_ssize_t

* fix tombstones

* simplify

* correct msg

* better test

* news for tombstones

* left-over from when testing buggy version

* redundant new line
(cherry picked from commit e0861c6ae7)

Co-authored-by: Maurycy Pawłowski-Wieroński <maurycy@maurycy.com>
(cherry picked from commit 9e401cf7f5)

* [3.15] gh-155811: Add a seqcount to `gc_stats` to prevent torn reads (GH-155828) (#158829)

* update_seq

* no need for XCHGL, MOVL is enough?

* gh-155811: Retry an inconsistent GC snapshot once

---------
(cherry picked from commit 5fecd448bb)

Co-authored-by: Pablo Galindo Salgado <Pablogsal@gmail.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
(cherry picked from commit 028124039c)

* [3.15] gh-151292: `_remote_debugging`: Do not corrupt the binary file when hitting `OverflowError` (GH-152892) (#158830)

gh-151292: `_remote_debugging`: Do not corrupt the binary file when hitting `OverflowError` (GH-152892)

* the kolektor

* test

* better test

* news

* =Q, move const to the base, not self.running

* gh-151292: Track binary writer finalization state

---------
(cherry picked from commit f839c061d3)

Co-authored-by: Maurycy Pawłowski-Wieroński <maurycy@maurycy.com>
Co-authored-by: Pablo Galindo Salgado <Pablogsal@gmail.com>
(cherry picked from commit 8a7c23f316)

* [3.15] gh-158583: Fix uninitialized memory read in bytes.fromhex() (GH-158584) (#158691)

gh-158583: Fix uninitialized memory read in bytes.fromhex() (GH-158584)
(cherry picked from commit 9d22a5334b)

Co-authored-by: Victor Stinner <vstinner@python.org>
(cherry picked from commit 4f7af46e25)

* [3.15] gh-154194: Degrade frames in Tachyon instead of failing the sample (GH-154195) (#158831)

* gh-154194: Degrade frames in Tachyon instead of failing the sample (#154195)

* degrade gracefully

* news

* better NEWS wording

* do not raise on MAX_REMOTE_STR_READ

* bye MAX_REMOTE_STR_READ

* fix -m asyncio ps|pstree

* test truncation and linetable sentinel

* simpler

* simpler

* redundant now

* respect #157790 in the news

---------

Co-authored-by: Pablo Galindo Salgado <Pablogsal@gmail.com>
(cherry picked from commit 7d25916b41)

* Preserve the stable ABI when creating fallback frame names

---------

Co-authored-by: Maurycy Pawłowski-Wieroński <maurycy@maurycy.com>
(cherry picked from commit c27f494061)

* [3.15] Add MSan to CI (GH-158625) (#158832)

Run a job that the test suite with MSan to the CI (#158625)

* Run the test suite with MSan in CI

* Additional fixes

* Add `_Py_MSAN_UNPOISON_STRING`

* Apply Victor's suggestions

* Apply Victor's suggestions

---------

(cherry picked from commit b93fb19a6e)

Co-authored-by: Stan Ulbrych <stan@python.org>
Co-authored-by: Victor Stinner <victor.stinner@gmail.com>
(cherry picked from commit 1bc78dea18)

* [3.15] gh-156810: Write the profiler's collapsed-stack export as UTF-8 (GH-156811) (#156814)

gh-156810: Write the profiler's collapsed-stack export as UTF-8 (GH-156811)
(cherry picked from commit c3706f4c68)

Co-authored-by: tonghuaroot (童话) <tonghuaroot@gmail.com>
(cherry picked from commit fce28da9b7)

* [3.15] gh-158552: Wait for Windows threads to suspend before blocking sampling (GH-158802) (#158845)

gh-158552: Wait for Windows threads to suspend before blocking sampling (GH-158802)

* gh-158552: Wait for Windows threads to suspend before blocking sampling

* Use a named Windows thread enumeration status constant
(cherry picked from commit 1643525f0f)

Co-authored-by: Pablo Galindo Salgado <Pablogsal@gmail.com>
(cherry picked from commit 31288da21d)

* [3.15] gh-152721: Fix quadratic RLE replay time in the profiling binary reader (GH-152722) (#158850)

Backport of GH-152722.

Co-authored-by: tonghuaroot <tonghuaroot@gmail.com>
(cherry picked from commit ebaca2ac2d)

* [3.15] gh-156545: Fix flamegraph export RecursionError on deeply recursive programs (GH-156546) (#158851)

Backport of GH-156546.

Co-authored-by: tonghuaroot (童话) <tonghuaroot@gmail.com>
(cherry picked from commit 0ac7217040)

* [3.15] gh-158540: Add the profiled script's directory to sys.path (GH-158548) (#158844)

gh-158540: Add the profiled script's directory to sys.path (GH-158548)

* gh-158540: Add the profiled script's directory to sys.path

When a script is profiled with ``python -m profiling.sampling run
script.py`` from another directory, the script cannot import modules
placed next to it, because ``_sync_coordinator._execute_script()``
executes it with the working directory (added by
``_setup_environment()`` for the module case) as ``sys.path[0]``
instead of the script's own directory.

Make the script's directory importable in ``_execute_script()``,
matching the behavior of ``python script.py``.

Add a regression test that runs the coordinator on a script importing
a sibling module.

* Update _sync_coordinator.py Comment simplified

* gh-158540: Resolve symlinks when adding the script directory to sys.path

``python script.py`` resolves symlinks when computing ``sys.path[0]``, so
a script reached through a symlink (``link.py -> sub/where.py``) imports
modules from the real script's directory.  Apply ``os.path.realpath()``
before taking the directory name, and make sure the result is placed at
the front of ``sys.path`` even if it was already listed.

Add a regression test for a symlinked script.

---------
(cherry picked from commit 3f02aab9fa)

Co-authored-by: he_tao <53343436+hetaozdh@users.noreply.github.com>
Co-authored-by: Eduardo Villalpando Mello <eduardo.villalpando.mello@gmail.com>
(cherry picked from commit f52d831397)

* [3.15] gh-153838: Skip non-regular source files in the heatmap exporter (GH-153839) (#158853)

Backport of GH-153839.

Co-authored-by: tonghuaroot <tonghuaroot@gmail.com>
(cherry picked from commit 48998df251)

* [3.15] gh-158539: Fix exception mode missing handlers in generators/coroutines (GH-158581) (#158852)

* [3.15] gh-158539: Fix exception mode missing handlers in generators/coroutines (GH-158581)

Backport of GH-158581.

Co-authored-by: LucasZhou <donghao.zhou@outlook.com>

* [3.15] gh-158539: Use portable static assertion messages

* [3.15] gh-158539: Keep layout assertions with debug-offset validation

* [3.15] gh-158539: Use the platform guard for in-process inspection tests

---------

Co-authored-by: LucasZhou <donghao.zhou@outlook.com>
(cherry picked from commit d625ecbb8d)

* [3.15] gh-158522: Fix truncated stack for a task whose coroutine recurses (GH-158526) (#158870)

Co-authored-by: Timofei Ivankov <128279579+deadlovelll@users.noreply.github.com>
(cherry picked from commit a904b397ed)

* gh-156545, gh-158539: Fix deep flamegraph export on small C stacks and macOS runtime lookup (#158874)

(cherry picked from commit 114de198c0)

Include the C-stack test helper from main, introduced by
ce5ae29ef9, which the regression test
requires but 3.15 does not yet provide.

---------

Co-authored-by: Miss Islington (bot) <31488909+miss-islington@users.noreply.github.com>
Co-authored-by: Maurycy Pawłowski-Wieroński <maurycy@maurycy.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: Victor Stinner <vstinner@python.org>
Co-authored-by: Stan Ulbrych <stan@python.org>
Co-authored-by: Victor Stinner <victor.stinner@gmail.com>
Co-authored-by: tonghuaroot (童话) <tonghuaroot@gmail.com>
Co-authored-by: he_tao <53343436+hetaozdh@users.noreply.github.com>
Co-authored-by: Eduardo Villalpando Mello <eduardo.villalpando.mello@gmail.com>
Co-authored-by: LucasZhou <donghao.zhou@outlook.com>
Co-authored-by: Timofei Ivankov <128279579+deadlovelll@users.noreply.github.com>
2026-10-11 15:42:59 +01:00

272 lines
10 KiB
Python

"""Tests for blocking mode sampling profiler."""
import io
import os
import subprocess
import sys
import textwrap
import time
import unittest
from unittest import mock
try:
import _remote_debugging # noqa: F401
import profiling.sampling
import profiling.sampling.sample
from profiling.sampling.stack_collector import CollapsedStackCollector
except ImportError:
raise unittest.SkipTest(
"Test only runs when _remote_debugging is available"
)
from test.support import (
SHORT_TIMEOUT,
os_helper,
requires_remote_subprocess_debugging,
)
from .helpers import test_subprocess
# Duration for profiling in tests
PROFILING_DURATION_SEC = 1
@requires_remote_subprocess_debugging()
class TestBlockingModeStackAccuracy(unittest.TestCase):
"""Test that blocking mode produces accurate stack traces.
When using blocking mode, the target process is stopped during sampling.
This ensures that we see accurate stack traces where functions appear
in the correct caller/callee relationship.
These tests verify that generator functions are correctly shown at the
top of the stack when they are actively executing, and not incorrectly
shown under their caller's code.
"""
@classmethod
def setUpClass(cls):
# Test script that uses a generator consumed in a loop.
# When consume_generator is the executing leaf frame on the arithmetic
# lines (temp1, temp2, etc.), fibonacci_generator should NOT be in the
# stack at all.
# Line numbers are important here - see ARITHMETIC_LINES below.
cls.generator_script = textwrap.dedent('''
def fibonacci_generator(n):
a, b = 0, 1
for _ in range(n):
yield a
a, b = b, a + b
def consume_generator():
gen = fibonacci_generator(10000)
for value in gen:
temp1 = value + 1
temp2 = value * 2
temp3 = value - 1
result = temp1 + temp2 + temp3
def main():
while True:
consume_generator()
_test_sock.sendall(b"working")
main()
''')
# Line numbers of the arithmetic operations in consume_generator.
# These are the lines where fibonacci_generator should NOT be in the
# stack when consume_generator is the executing leaf frame. They account
# for the socket prelude added by test_subprocess().
# temp1 = value + 1 -> line 16
# temp2 = value * 2 -> line 17
# temp3 = value - 1 -> line 18
# result = ... -> line 19
cls.ARITHMETIC_LINES = {16, 17, 18, 19}
def test_generator_not_under_consumer_arithmetic(self):
"""Test that fibonacci_generator doesn't appear when consume_generator does arithmetic.
When consume_generator is the leaf frame on arithmetic lines (temp1,
temp2, etc.), fibonacci_generator should NOT be anywhere in the stack -
it's not being called at that point. Non-leaf frame line numbers are
caller/resume metadata, not proof that the frame is executing.
Valid stacks:
- fibonacci_generator at the top (generator is executing), with
consume_generator below it
- consume_generator at arithmetic lines WITHOUT fibonacci_generator
(we're just doing math, not calling the generator)
Invalid stacks (indicate torn/inconsistent reads):
- consume_generator leaf frame at arithmetic lines WITH
fibonacci_generator
anywhere in the stack
Note: call_tree is ordered from bottom (index 0) to top (index -1).
"""
with test_subprocess(self.generator_script, wait_for_working=True) as subproc:
collector = CollapsedStackCollector(sample_interval_usec=100, skip_idle=False)
with (
io.StringIO() as captured_output,
mock.patch("sys.stdout", captured_output),
):
profiling.sampling.sample.sample(
subproc.process.pid,
collector,
duration_sec=PROFILING_DURATION_SEC,
blocking=True,
)
# Analyze collected stacks
total_samples = 0
invalid_stacks = 0
arithmetic_samples = 0
generator_samples = 0
generator_not_leaf_samples = 0
for (call_tree, _thread_id), count in collector.stack_counter.items():
total_samples += count
if not call_tree:
continue
# Non-leaf frame line numbers can point at resume locations while
# a callee is the executing leaf frame.
_, lineno, funcname = call_tree[-1]
func_names = [frame[2] for frame in call_tree]
if "fibonacci_generator" in func_names:
generator_samples += count
if funcname != "fibonacci_generator":
generator_not_leaf_samples += count
if funcname == "consume_generator" and lineno in self.ARITHMETIC_LINES:
arithmetic_samples += count
# Check if fibonacci_generator appears anywhere in this stack.
if "fibonacci_generator" in func_names:
invalid_stacks += count
self.assertGreater(total_samples, 10,
f"Expected at least 10 samples, got {total_samples}")
# We should have some samples on the arithmetic lines
self.assertGreater(arithmetic_samples, 0,
f"Expected some samples on arithmetic lines, got {arithmetic_samples}")
self.assertGreater(generator_samples, 0,
f"Expected some samples in fibonacci_generator, got {generator_samples}")
self.assertEqual(generator_not_leaf_samples, 0,
f"Found {generator_not_leaf_samples}/{generator_samples} stacks where "
f"fibonacci_generator appears but is not the leaf frame.")
self.assertEqual(invalid_stacks, 0,
f"Found {invalid_stacks}/{arithmetic_samples} invalid stacks where "
f"fibonacci_generator appears in the stack when consume_generator "
f"is the leaf frame on an arithmetic line. This indicates "
f"torn/inconsistent stack traces are being captured.")
@requires_remote_subprocess_debugging()
@unittest.skipUnless(sys.platform == "win32", "Windows only")
class TestBlockingModeCLI(unittest.TestCase):
def test_run_blocking_exits_after_target_process_exits(self):
script = 'print("done")\n'
tmpdir = os.path.abspath(os_helper.TESTFN + "_profiling_blocking")
with os_helper.temp_dir(tmpdir) as tmpdir:
script_path = os.path.join(tmpdir, "tiny_target.py")
profile_path = os.path.join(tmpdir, "blocking.bin")
with open(script_path, "w", encoding="utf-8") as file:
file.write(script)
cmd = [
sys.executable, "-m", "profiling.sampling", "run",
"--binary", "-o", profile_path,
"--mode=cpu", "--blocking", "-r", "100",
script_path,
]
result = subprocess.run(
cmd,
cwd=tmpdir,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
timeout=SHORT_TIMEOUT,
)
self.assertEqual(
result.returncode, 0,
f"stdout:\n{result.stdout}\nstderr:\n{result.stderr}",
)
self.assertGreater(os.path.getsize(profile_path), 0)
replay = subprocess.run(
[sys.executable, "-m", "profiling.sampling", "replay",
profile_path],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
timeout=SHORT_TIMEOUT,
)
self.assertEqual(
replay.returncode, 0,
f"stdout:\n{replay.stdout}\nstderr:\n{replay.stderr}",
)
@requires_remote_subprocess_debugging()
@unittest.skipUnless(sys.platform == "win32", "Windows only")
class TestBlockingModeSuspension(unittest.TestCase):
def test_all_threads_stop_before_pause_returns(self):
import mmap
tag = f"cpython_blocking_{os.getpid()}_{id(self)}"
script = textwrap.dedent(f'''
import mmap
import struct
import threading
memory = mmap.mmap(-1, 16, tagname={tag!r})
def worker(offset):
counter = 0
while True:
counter += 1
struct.pack_into("q", memory, offset, counter)
for offset in (0, 8):
threading.Thread(target=worker, args=(offset,), daemon=True).start()
_test_sock.sendall(b"working")
_test_sock.recv(1)
''')
with mmap.mmap(-1, 16, tagname=tag) as memory:
with test_subprocess(script, wait_for_working=True) as subproc:
unwinder = _remote_debugging.RemoteUnwinder(
subproc.process.pid, all_threads=True)
deadline = time.monotonic() + SHORT_TIMEOUT
while not all(memory[offset:offset + 8] != bytes(8)
for offset in (0, 8)):
self.assertLess(time.monotonic(), deadline,
"Worker threads did not start")
time.sleep(0.001)
for _ in range(100):
self.assertTrue(unwinder.pause_threads())
try:
before = memory[:]
self.assertFalse(unwinder.pause_threads())
unwinder.get_stack_trace()
time.sleep(0.001)
self.assertEqual(memory[:], before,
"Target memory changed while paused")
finally:
unwinder.resume_threads()
self.assertFalse(unwinder.resume_threads())
before = memory[:]
deadline = time.monotonic() + SHORT_TIMEOUT
while any(memory[offset:offset + 8] == before[offset:offset + 8]
for offset in (0, 8)):
self.assertLess(time.monotonic(), deadline,
"Worker threads did not resume")
time.sleep(0.001)