mirror of
https://github.com/python/cpython
synced 2026-11-06 09:02:40 +03:00
* [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 commite0861c6ae7) Co-authored-by: Maurycy Pawłowski-Wieroński <maurycy@maurycy.com> (cherry picked from commit9e401cf7f5) * [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 commit5fecd448bb) Co-authored-by: Pablo Galindo Salgado <Pablogsal@gmail.com> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> (cherry picked from commit028124039c) * [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 commitf839c061d3) Co-authored-by: Maurycy Pawłowski-Wieroński <maurycy@maurycy.com> Co-authored-by: Pablo Galindo Salgado <Pablogsal@gmail.com> (cherry picked from commit8a7c23f316) * [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 commit9d22a5334b) Co-authored-by: Victor Stinner <vstinner@python.org> (cherry picked from commit4f7af46e25) * [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 commit7d25916b41) * Preserve the stable ABI when creating fallback frame names --------- Co-authored-by: Maurycy Pawłowski-Wieroński <maurycy@maurycy.com> (cherry picked from commitc27f494061) * [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 commitb93fb19a6e) Co-authored-by: Stan Ulbrych <stan@python.org> Co-authored-by: Victor Stinner <victor.stinner@gmail.com> (cherry picked from commit1bc78dea18) * [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 commitc3706f4c68) Co-authored-by: tonghuaroot (童话) <tonghuaroot@gmail.com> (cherry picked from commitfce28da9b7) * [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 commit1643525f0f) Co-authored-by: Pablo Galindo Salgado <Pablogsal@gmail.com> (cherry picked from commit31288da21d) * [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 commitebaca2ac2d) * [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 commit0ac7217040) * [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 commit3f02aab9fa) 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 commitf52d831397) * [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 commit48998df251) * [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 commitd625ecbb8d) * [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 commita904b397ed) * gh-156545, gh-158539: Fix deep flamegraph export on small C stacks and macOS runtime lookup (#158874) (cherry picked from commit114de198c0) Include the C-stack test helper from main, introduced byce5ae29ef9, 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>
272 lines
10 KiB
Python
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)
|