mirror of
https://github.com/python/cpython.git
synced 2026-01-01 04:53:46 +00:00
141 lines
5.4 KiB
Python
141 lines
5.4 KiB
Python
"""Tests for blocking mode sampling profiler."""
|
|
|
|
import io
|
|
import textwrap
|
|
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 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 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.
|
|
# The socket injection code adds 7 lines before our script.
|
|
# temp1 = value + 1 -> line 17
|
|
# temp2 = value * 2 -> line 18
|
|
# temp3 = value - 1 -> line 19
|
|
# result = ... -> line 20
|
|
cls.ARITHMETIC_LINES = {17, 18, 19, 20}
|
|
|
|
def test_generator_not_under_consumer_arithmetic(self):
|
|
"""Test that fibonacci_generator doesn't appear when consume_generator does arithmetic.
|
|
|
|
When consume_generator is executing arithmetic lines (temp1, temp2, etc.),
|
|
fibonacci_generator should NOT be anywhere in the stack - it's not being
|
|
called at that point.
|
|
|
|
Valid stacks:
|
|
- consume_generator at 'for value in gen:' line WITH fibonacci_generator
|
|
at the top (generator is yielding)
|
|
- 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 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
|
|
|
|
for (call_tree, _thread_id), count in collector.stack_counter.items():
|
|
total_samples += count
|
|
|
|
if not call_tree:
|
|
continue
|
|
|
|
# Find consume_generator in the stack and check its line number
|
|
for i, (filename, lineno, funcname) in enumerate(call_tree):
|
|
if funcname == "consume_generator" and lineno in self.ARITHMETIC_LINES:
|
|
arithmetic_samples += count
|
|
# Check if fibonacci_generator appears anywhere in this stack
|
|
func_names = [frame[2] for frame in call_tree]
|
|
if "fibonacci_generator" in func_names:
|
|
invalid_stacks += count
|
|
break
|
|
|
|
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.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 on an arithmetic line. This indicates torn/inconsistent stack "
|
|
f"traces are being captured.")
|