https://github.com/python/cpython/commit/3439df909caff05c0297d370ae4bb4e9cd02ef1a
commit: 3439df909caff05c0297d370ae4bb4e9cd02ef1a
branch: main
author: Maurycy Pawłowski-Wieroński <[email protected]>
committer: pablogsal <[email protected]>
date: 2026-09-24T17:00:23+01:00
summary:

gh-153804: `_remote_debugging`: Tachyon Oracle (#153806)

* tachyon oracle

* news

* news in tools, not library

* subprocess.DEVNULL

* validate args.run

* do not sample past deadline

* show raw_tvd

* do not loop forever

* comment on the limitation

* docstring

* simple gen testing

* test all classifiers

* better no lineno

* test both ways

* merge test_classify_gen

* time.perf_counter

* docstring

* -stderr=subprocess.DEVNULL,

files:
A Lib/test/test_tools/test_inspection.py
A Misc/NEWS.d/next/Tools-Demos/2026-07-16-11-38-15.gh-issue-153804.KSTYg7.rst
A Tools/inspection/oracle_external_inspection.py
A Tools/inspection/snippets.py
M Tools/inspection/benchmark_external_inspection.py

diff --git a/Lib/test/test_tools/test_inspection.py 
b/Lib/test/test_tools/test_inspection.py
new file mode 100644
index 00000000000000..f4a0d163adbc56
--- /dev/null
+++ b/Lib/test/test_tools/test_inspection.py
@@ -0,0 +1,111 @@
+"""Tests for snippets in Tools/inspection."""
+
+import unittest
+from types import SimpleNamespace
+
+from test.test_tools import imports_under_tool, skip_if_missing
+
+
+skip_if_missing("inspection")
+with imports_under_tool("inspection"):
+    import snippets
+
+
+def frame(funcname, *, filename="", lineno=None):
+    location = SimpleNamespace(lineno=lineno) if lineno is not None else None
+    return SimpleNamespace(
+        funcname=funcname,
+        filename=filename,
+        location=location,
+    )
+
+
+def frames(*names):
+    return [frame(name) for name in names]
+
+
+class ClassifierTests(unittest.TestCase):
+    def test_classifiers(self):
+        flat_lines = snippets.FLAT_ALTERNATING_LINES
+        short_line = min(snippets.SHARED_LEAF_SHORT_LINES)
+        flat_a = [
+            frame("leaf_a", lineno=flat_lines["leaf_a"]),
+            frame("hot_a", lineno=flat_lines["hot_a"]),
+        ]
+        flat_crossed = [
+            frame("hot_a", lineno=flat_lines["hot_a"]),
+            frame("hot_b", lineno=flat_lines["hot_b"]),
+        ]
+        shared_a = [
+            frame("shared_leaf", lineno=short_line),
+            frame("a_wrapper"),
+        ]
+        shared_crossed = [
+            frame("shared_leaf", lineno=short_line),
+            frame("b_wrapper"),
+        ]
+        cases = [
+            (snippets.classify_flat, flat_a, False),
+            (snippets.classify_flat, flat_crossed, True),
+            (
+                snippets.classify_nested,
+                frames("burn_a", "a_leaf", "a_parent"),
+                False,
+            ),
+            (
+                snippets.classify_nested,
+                frames("a_parent", "a_leaf", "burn_a"),
+                True,
+            ),
+            (snippets.classify_shared, shared_a, False),
+            (snippets.classify_shared, shared_crossed, True),
+            (snippets.classify_gen, frames("agen", "drv_a"), False),
+            (snippets.classify_gen, frames("agen", "drv_b"), True),
+            (snippets.classify_gen, frames("bgen", "drv_a"), True),
+            (snippets.classify_gen, frames("agen"), False),
+            (
+                snippets.classify_gen,
+                frames("agen", "agen", "drv_a"),
+                False,
+            ),
+
+            (snippets.classify_recursion, frames("a", "a"), False),
+            (snippets.classify_recursion, frames("a", "b"), True),
+            (
+                snippets.classify_async_running_task,
+                (None, "hot", None, frames("leaf_hot")),
+                False,
+            ),
+            (
+                snippets.classify_async_running_task,
+                (None, "hot", None, frames("leaf_rare")),
+                True,
+            ),
+            (
+                snippets.classify_code_object_reuse,
+                [frame("func_a", filename="A_file.py")],
+                False,
+            ),
+            (
+                snippets.classify_code_object_reuse,
+                [frame("func_a", filename="B_file.py")],
+                True,
+            ),
+            (
+                snippets.classify_oversized_chunk,
+                [frame("big_a", filename="a.py")],
+                False,
+            ),
+            (
+                snippets.classify_oversized_chunk,
+                [frame("big_b", filename="a.py")],
+                True,
+            ),
+        ]
+        for classifier, sample, expected in cases:
+            with self.subTest(classifier=classifier.__name__, sample=sample):
+                self.assertEqual(classifier(sample), expected)
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git 
a/Misc/NEWS.d/next/Tools-Demos/2026-07-16-11-38-15.gh-issue-153804.KSTYg7.rst 
b/Misc/NEWS.d/next/Tools-Demos/2026-07-16-11-38-15.gh-issue-153804.KSTYg7.rst
new file mode 100644
index 00000000000000..7d0f287a72445b
--- /dev/null
+++ 
b/Misc/NEWS.d/next/Tools-Demos/2026-07-16-11-38-15.gh-issue-153804.KSTYg7.rst
@@ -0,0 +1,4 @@
+Add ``Tools/inspection/oracle_external_inspection.py``, a harness for
+measuring the accuracy of Tachyon. It reports impossible-stack rates, speed,
+error rates and the statistical distance from the blocking non-cached best
+reference on selected snippets. Patch by Maurycy Pawłowski-Wieroński.
diff --git a/Tools/inspection/benchmark_external_inspection.py 
b/Tools/inspection/benchmark_external_inspection.py
index b7aa0e5de7ed99..5c491e3cfc6211 100644
--- a/Tools/inspection/benchmark_external_inspection.py
+++ b/Tools/inspection/benchmark_external_inspection.py
@@ -7,206 +7,8 @@
 import argparse
 from _colorize import get_colors, can_colorize
 
-CODE = '''\
-import time
-import os
-import sys
-import math
-
-def slow_fibonacci(n):
-    """Intentionally slow recursive fibonacci - should show up prominently in 
profiler"""
-    if n <= 1:
-        return n
-    return slow_fibonacci(n-1) + slow_fibonacci(n-2)
-
-def medium_computation():
-    """Medium complexity function"""
-    result = 0
-    for i in range(1000):
-        result += math.sqrt(i) * math.sin(i)
-    return result
-
-def fast_loop():
-    """Fast simple loop"""
-    total = 0
-    for i in range(100):
-        total += i
-    return total
-
-def string_operations():
-    """String manipulation that should be visible in profiler"""
-    text = "hello world " * 100
-    words = text.split()
-    return " ".join(reversed(words))
-
-def nested_calls():
-    """Nested function calls to test call stack depth"""
-    def level1():
-        def level2():
-            def level3():
-                return medium_computation()
-            return level3()
-        return level2()
-    return level1()
-
-def main_loop():
-    """Main computation loop with different execution paths"""
-    iteration = 0
-
-    while True:
-        iteration += 1
-
-        # Different execution paths with different frequencies
-        if iteration % 50 == 0:
-            # Expensive operation - should show high per-call time
-            result = slow_fibonacci(20)
-
-        elif iteration % 10 == 0:
-            # Medium operation
-            result = nested_calls()
-
-        elif iteration % 5 == 0:
-            # String operations
-            result = string_operations()
-
-        else:
-            # Fast operation - most common
-            result = fast_loop()
-
-        # Small delay to make sampling more interesting
-        time.sleep(0.001)
-
-if __name__ == "__main__":
-    main_loop()
-'''
-
-DEEP_STATIC_CODE = """\
-import time
-def factorial(n):
-    if n <= 1:
-        time.sleep(10000)
-        return 1
-    return n * factorial(n-1)
-
-factorial(900)
-"""
+from snippets import CODE_EXAMPLES, CODE
 
-CODE_WITH_TONS_OF_THREADS = '''\
-import time
-import threading
-import random
-import math
-
-def cpu_intensive_work():
-    """Do some CPU intensive calculations"""
-    result = 0
-    for _ in range(10000):
-        result += math.sin(random.random()) * math.cos(random.random())
-    return result
-
-def io_intensive_work():
-    """Simulate IO intensive work with sleeps"""
-    time.sleep(0.1)
-
-def mixed_workload():
-    """Mix of CPU and IO work"""
-    while True:
-        if random.random() < 0.3:
-            cpu_intensive_work()
-        else:
-            io_intensive_work()
-
-def create_threads(n):
-    """Create n threads doing mixed workloads"""
-    threads = []
-    for _ in range(n):
-        t = threading.Thread(target=mixed_workload, daemon=True)
-        t.start()
-        threads.append(t)
-    return threads
-
-# Start with 5 threads
-active_threads = create_threads(5)
-thread_count = 5
-
-# Main thread manages threads and does work
-while True:
-    # Randomly add or remove threads
-    if random.random() < 0.1:  # 10% chance each iteration
-        if random.random() < 0.5 and thread_count < 100:
-            # Add 1-5 new threads
-            new_count = random.randint(1, 5)
-            new_threads = create_threads(new_count)
-            active_threads.extend(new_threads)
-            thread_count += new_count
-        elif thread_count > 10:
-            # Remove 1-3 threads
-            remove_count = random.randint(1, 5)
-            # The threads will terminate naturally since they're daemons
-            active_threads = active_threads[remove_count:]
-            thread_count -= remove_count
-
-    cpu_intensive_work()
-    time.sleep(0.05)
-'''
-
-ASYNC_CODE = '''\
-import asyncio
-import contextlib
-import math
-
-def compute_slice(seed):
-    result = 0.0
-    for i in range(2000):
-        result += math.sin(seed + i) * math.sqrt(i + 1)
-    return result
-
-async def leaf_task(seed):
-    total = 0.0
-    while True:
-        total += compute_slice(seed)
-        await asyncio.sleep(0)
-
-async def parent_task(seed):
-    child = asyncio.create_task(leaf_task(seed + 1000), name=f"leaf-{seed}")
-    try:
-        while True:
-            compute_slice(seed)
-            await asyncio.sleep(0.001)
-    finally:
-        child.cancel()
-        with contextlib.suppress(asyncio.CancelledError):
-            await child
-
-async def main():
-    tasks = [
-        asyncio.create_task(parent_task(i), name=f"parent-{i}")
-        for i in range(8)
-    ]
-    await asyncio.gather(*tasks)
-
-if __name__ == "__main__":
-    asyncio.run(main())
-'''
-
-CODE_EXAMPLES = {
-    "basic": {
-        "code": CODE,
-        "description": "Mixed workload with fibonacci, computations, and 
string operations",
-    },
-    "deep_static": {
-        "code": DEEP_STATIC_CODE,
-        "description": "Deep recursive call stack with 900+ frames 
(factorial)",
-    },
-    "threads": {
-        "code": CODE_WITH_TONS_OF_THREADS,
-        "description": "Tons of threads doing mixed CPU/IO work",
-    },
-    "asyncio": {
-        "code": ASYNC_CODE,
-        "description": "Asyncio tasks with active and awaited coroutine 
chains",
-    },
-}
 
 OPERATIONS = {
     "stack_trace": {
diff --git a/Tools/inspection/oracle_external_inspection.py 
b/Tools/inspection/oracle_external_inspection.py
new file mode 100644
index 00000000000000..ec5060a1c9a4a5
--- /dev/null
+++ b/Tools/inspection/oracle_external_inspection.py
@@ -0,0 +1,436 @@
+"""Compare external inspection modes against a reference mode ("Oracle").
+
+This script reports the following validation metrics:
+
+- impossible: Number of stacks matching a known impossible pattern. The
+  classifiers are not exhaustive.
+
+- raw_tvd: Total variation distance between this mode's stack distribution
+  and the reference's distribution. Stacks classified as impossible are
+  excluded.
+
+- tvd_excess: raw_tvd minus tvd_floor.
+
+- tvd_floor: IID heuristic for the TVD expected from finite sampling of both
+  distributions. This is not a lower bound.
+"""
+
+import argparse
+import contextlib
+import math
+import os
+import random
+import subprocess
+import statistics
+import sys
+import tempfile
+import time
+from collections import Counter
+
+import _remote_debugging
+
+from snippets import CASES, _get_lineno
+
+
+TRANSIENT_ERRORS = (OSError, RuntimeError, UnicodeDecodeError)
+
+MODES = {
+    "live-cache": (False, True),
+    "live-nocache": (False, False),
+    "blocking-cache": (True, True),
+    "blocking-nocache": (True, False),
+}
+
+
+def collapse_cache(mode):
+    blocking, _ = MODES[mode]
+    return "blocking-nocache" if blocking else "live-nocache"
+
+
+def tvd(left, right):
+    lt, rt = sum(left.values()), sum(right.values())
+    if not lt or not rt:
+        return None
+    return 0.5 * sum(
+        abs(left[k] / lt - right[k] / rt) for k in set(left) | set(right)
+    )
+
+
+def tvd_floor(reference_obs, n_live):
+    n_ref = sum(reference_obs.values())
+    if not n_ref or not n_live:
+        return None
+    spread = sum(
+        math.sqrt(p * (1 - p))
+        for p in (c / n_ref for c in reference_obs.values())
+    )
+    return (
+        0.5
+        * math.sqrt(2 / math.pi)
+        * math.sqrt(1 / n_live + 1 / n_ref)
+        * spread
+    )
+
+
+def print_run_info(args, cases):
+    print(sys.version.replace("\n", " "))
+    print(
+        f"cases={','.join(cases)} runs={args.runs} "
+        f"duration={args.duration} "
+        f"rate_khz={args.rate_khz} warmup={args.warmup} "
+        f"poisson_sampling={args.poisson_sampling}"
+    )
+
+
+def terminate_process(proc):
+    if proc.poll() is not None:
+        return
+    proc.terminate()
+    try:
+        proc.wait(timeout=5)
+    except subprocess.TimeoutExpired:
+        proc.kill()
+        proc.wait()
+
+
[email protected]
+def target_process(code, warmup):
+    with tempfile.NamedTemporaryFile("w", suffix=".py", delete=False) as tmp:
+        tmp.write(code)
+        tmp.flush()
+        tmp_name = tmp.name
+    proc = None
+    try:
+        proc = subprocess.Popen(
+            [sys.executable, tmp_name],
+            stdout=subprocess.DEVNULL,
+        )
+        time.sleep(warmup)
+        if proc.poll() is not None:
+            raise RuntimeError(
+                f"target exited unexpectedly with code {proc.returncode}"
+            )
+        yield proc
+    finally:
+        with contextlib.suppress(Exception):
+            if proc is not None:
+                terminate_process(proc)
+        with contextlib.suppress(OSError):
+            os.unlink(tmp_name)
+
+
+def get_trace(unwinder, blocking, op="get_stack_trace"):
+    call = getattr(unwinder, op)
+    if not blocking:
+        return call()
+    unwinder.pause_threads()
+    try:
+        return call()
+    finally:
+        unwinder.resume_threads()
+
+
+def iter_units(raw, op):
+    if op == "get_stack_trace":
+        for interp in raw:
+            for thread in interp.threads:
+                yield (
+                    thread.thread_id,
+                    None,
+                    thread.status,
+                    thread.frame_info,
+                )
+    else:
+        for awaited_info in raw:
+            for task in awaited_info.awaited_by:
+                frames = [
+                    frame
+                    for coro in task.coroutine_stack
+                    for frame in coro.call_stack
+                ]
+                if frames:
+                    yield (task.task_id, task.task_name, None, frames)
+
+
+def run_mode(case, mode_name, args):
+    code, classify, *rest = case
+    op = rest[0] if rest else "get_stack_trace"
+    classify_units = op != "get_stack_trace"
+    blocking, cache_frames = MODES[mode_name]
+    result = {
+        "attempts": 0,
+        "samples": 0,
+        "stacks": 0,
+        "errors": 0,
+        "observations": Counter(),
+        "impossible": 0,
+        "work_time": 0.0,
+    }
+    with target_process(code, args.warmup) as proc:
+        unwinder = _remote_debugging.RemoteUnwinder(
+            proc.pid,
+            all_threads=True,
+            cache_frames=cache_frames,
+        )
+        rate_hz = args.rate_khz * 1000
+        period = 1.0 / rate_hz if rate_hz else 0
+        next_sample = time.perf_counter()
+        deadline = next_sample + args.duration
+
+        while time.perf_counter() < deadline:
+            if period:
+                if args.poisson_sampling:
+                    next_sample += random.expovariate(rate_hz)
+                if next_sample >= deadline:
+                    break
+                now = time.perf_counter()
+                if next_sample > now:
+                    time.sleep(next_sample - now)
+                if not args.poisson_sampling:
+                    next_sample += period
+
+            if time.perf_counter() >= deadline:
+                break
+
+            result["attempts"] += 1
+            work_start = time.perf_counter()
+            try:
+                trace = get_trace(unwinder, blocking, op)
+            except TRANSIENT_ERRORS:
+                trace = None
+                result["errors"] += 1
+            result["work_time"] += time.perf_counter() - work_start
+
+            if not trace:
+                continue
+
+            result["samples"] += 1
+            for unit in iter_units(trace, op):
+                frames = unit[3]
+                impossible = classify is not None and (
+                    classify(unit) if classify_units else classify(frames)
+                )
+                result["stacks"] += 1
+                if impossible:
+                    result["impossible"] += 1
+                else:
+                    result["observations"][
+                        ";".join(
+                            f"{frame.funcname}:{_get_lineno(frame)}"
+                            for frame in frames
+                        )
+                    ] += 1
+    return result
+
+
+def result_metrics(result, reference_obs, is_reference, op):
+    skip_tvd = is_reference or op != "get_stack_trace"
+    n_live = sum(result["observations"].values())
+    raw_tvd = None if skip_tvd else tvd(result["observations"], reference_obs)
+    # IID heuristic, not a calibrated noise bound.
+    floor = None if skip_tvd else tvd_floor(reference_obs, n_live)
+    return {
+        "samples": result["samples"],
+        "stacks": result["stacks"],
+        "empty": result["attempts"] - result["samples"] - result["errors"],
+        "impossible": result["impossible"],
+        "error_percent": (
+            100.0 * result["errors"] / result["attempts"]
+            if result["attempts"]
+            else 0.0
+        ),
+        "impossible_percent": (
+            100.0 * result["impossible"] / result["stacks"]
+            if result["stacks"]
+            else 0.0
+        ),
+        "avg_us": (
+            1e6 * result["work_time"] / result["attempts"]
+            if result["attempts"]
+            else 0.0
+        ),
+        "raw_tvd": raw_tvd,
+        "tvd_floor": floor,
+        "tvd_excess": (
+            None if (raw_tvd is None or floor is None) else raw_tvd - floor
+        ),
+    }
+
+
+def fmt_stat(values, precision):
+    vals = [value for value in values if value is not None]
+    if not vals:
+        return "n/a"
+    mean = statistics.mean(vals)
+    if len(vals) > 1:
+        return f"{mean:.{precision}f}±{statistics.stdev(vals):.{precision}f}"
+    return f"{mean:.{precision}f}"
+
+
+def fmt_floor(values):
+    vals = [value for value in values if value is not None]
+    return "n/a" if not vals else f"{statistics.median(vals):.3f}"
+
+
+def print_results(
+    case_name, run_results, modes, reference_mode, op, has_classify
+):
+    is_sync = op == "get_stack_trace"
+    ref_keys = len(run_results[0][reference_mode]["observations"])
+    rows = {}
+    for mode in modes:
+        metrics = [
+            result_metrics(
+                results[mode],
+                results[reference_mode]["observations"],
+                mode == reference_mode,
+                op,
+            )
+            for results in run_results
+        ]
+        rows[mode] = {
+            "mode": mode,
+            "samples": sum(item["samples"] for item in metrics),
+            "stacks": sum(item["stacks"] for item in metrics),
+            "empty": sum(item["empty"] for item in metrics),
+            "impossible": sum(item["impossible"] for item in metrics),
+            "errors": fmt_stat([item["error_percent"] for item in metrics], 2),
+            "us": fmt_stat([item["avg_us"] for item in metrics], 2),
+            "impossible_pct": fmt_stat(
+                [item["impossible_percent"] for item in metrics], 2
+            ),
+            "raw_tvd": "ref"
+            if mode == reference_mode
+            else fmt_stat([item["raw_tvd"] for item in metrics], 3),
+            "tvd_excess": "ref"
+            if mode == reference_mode
+            else fmt_stat([item["tvd_excess"] for item in metrics], 3),
+            "tvd_floor": "-"
+            if mode == reference_mode
+            else fmt_floor([item["tvd_floor"] for item in metrics]),
+        }
+
+    show_stacks = any(row["stacks"] != row["samples"] for row in rows.values())
+    ordered = [reference_mode] + [m for m in modes if m != reference_mode]
+
+    columns = [("mode", "<18", "mode"), ("samples", ">9", "samples")]
+    if show_stacks:
+        columns.append(("stacks", ">9", "stacks"))
+    if not is_sync:
+        columns.append(("empty", ">9", "empty"))
+    columns += [("µs", ">12", "us"), ("errors%", ">12", "errors")]
+    if has_classify:
+        columns += [
+            ("impossible", ">10", "impossible"),
+            ("impossible%", ">12", "impossible_pct"),
+        ]
+    if is_sync:
+        columns += [
+            ("raw_tvd", ">12", "raw_tvd"),
+            ("tvd_excess", ">12", "tvd_excess"),
+            ("tvd_floor", ">10", "tvd_floor"),
+        ]
+
+    print(f"\n{case_name} ({op}) ref_keys={ref_keys}")
+    print(" ".join(f"{label:{spec}}" for label, spec, _ in columns))
+    for mode in ordered:
+        row = rows[mode]
+        print(" ".join(f"{row[key]:{spec}}" for _, spec, key in columns))
+
+
+def parse_args():
+    parser = argparse.ArgumentParser(
+        formatter_class=argparse.ArgumentDefaultsHelpFormatter
+    )
+    parser.add_argument(
+        "--snippet",
+        action="append",
+        help="snippet name or Python file; may be passed more than once",
+    )
+    parser.add_argument(
+        "--mode",
+        choices=sorted(MODES),
+        action="append",
+        help="mode to run; may be passed more than once; omit to run all 
modes",
+    )
+    parser.add_argument(
+        "--reference-mode",
+        choices=sorted(MODES),
+        default="blocking-nocache",
+        help="mode used as the distribution reference",
+    )
+    parser.add_argument(
+        "--duration",
+        type=float,
+        default=3.0,
+        help="seconds to sample each mode",
+    )
+    parser.add_argument(
+        "--runs",
+        type=int,
+        default=1,
+        help="number of independent runs per case",
+    )
+    parser.add_argument(
+        "--rate-khz",
+        type=float,
+        default=100.0,
+        help="target sampling rate in kHz; 0 samples as fast as possible",
+    )
+    parser.add_argument(
+        "--warmup",
+        type=float,
+        default=0.7,
+        help="seconds to let the target run before sampling",
+    )
+    parser.add_argument(
+        "--poisson-sampling",
+        action="store_true",
+        help=(
+            "sample with exponential inter-arrival times instead of a fixed "
+            "period"
+        ),
+    )
+    args = parser.parse_args()
+    if args.runs < 1:
+        parser.error("--runs must be greater than zero")
+    return args
+
+
+def main():
+    args = parse_args()
+    cases = list(args.snippet) if args.snippet else sorted(CASES)
+    modes = list(args.mode) if args.mode else sorted(MODES)
+    if args.reference_mode not in modes:
+        modes.append(args.reference_mode)
+
+    print_run_info(args, cases)
+
+    for name in cases:
+        if name in CASES:
+            case = CASES[name]
+        else:
+            with open(name, encoding="utf-8") as file:
+                case = (file.read(), None)
+        op = case[2] if len(case) > 2 else "get_stack_trace"
+        case_ref = args.reference_mode
+        case_modes = list(modes)
+        if op != "get_stack_trace":
+            case_ref = collapse_cache(case_ref)
+            case_modes = list(
+                dict.fromkeys(collapse_cache(m) for m in case_modes)
+            )
+        if case_ref not in case_modes:
+            case_modes.append(case_ref)
+        run_results = [
+            {mode: run_mode(case, mode, args) for mode in case_modes}
+            for _ in range(args.runs)
+        ]
+        print_results(
+            name, run_results, case_modes, case_ref, op, case[1] is not None
+        )
+    return 0
+
+
+if __name__ == "__main__":
+    sys.exit(main())
diff --git a/Tools/inspection/snippets.py b/Tools/inspection/snippets.py
new file mode 100644
index 00000000000000..47173c4fd12543
--- /dev/null
+++ b/Tools/inspection/snippets.py
@@ -0,0 +1,601 @@
+"""Scripts and classifiers for external inspection validation.
+
+Classifiers detect only recognized impossible patterns. They do not validate
+entire stacks. False does not imply that the stack as a whole is valid.
+"""
+import os
+
+
+def _get_lineno(frame, default=None):
+    if frame is None:
+        return default
+    loc = getattr(frame, "location", None)
+    return getattr(loc, "lineno", default) if loc is not None else default
+
+
+CODE = '''\
+import time
+import os
+import sys
+import math
+
+def slow_fibonacci(n):
+    """Intentionally slow recursive fibonacci - should show up prominently in 
profiler"""
+    if n <= 1:
+        return n
+    return slow_fibonacci(n-1) + slow_fibonacci(n-2)
+
+def medium_computation():
+    """Medium complexity function"""
+    result = 0
+    for i in range(1000):
+        result += math.sqrt(i) * math.sin(i)
+    return result
+
+def fast_loop():
+    """Fast simple loop"""
+    total = 0
+    for i in range(100):
+        total += i
+    return total
+
+def string_operations():
+    """String manipulation that should be visible in profiler"""
+    text = "hello world " * 100
+    words = text.split()
+    return " ".join(reversed(words))
+
+def nested_calls():
+    """Nested function calls to test call stack depth"""
+    def level1():
+        def level2():
+            def level3():
+                return medium_computation()
+            return level3()
+        return level2()
+    return level1()
+
+def main_loop():
+    """Main computation loop with different execution paths"""
+    iteration = 0
+
+    while True:
+        iteration += 1
+
+        # Different execution paths with different frequencies
+        if iteration % 50 == 0:
+            # Expensive operation - should show high per-call time
+            result = slow_fibonacci(20)
+
+        elif iteration % 10 == 0:
+            # Medium operation
+            result = nested_calls()
+
+        elif iteration % 5 == 0:
+            # String operations
+            result = string_operations()
+
+        else:
+            # Fast operation - most common
+            result = fast_loop()
+
+        # Small delay to make sampling more interesting
+        time.sleep(0.001)
+
+if __name__ == "__main__":
+    main_loop()
+'''
+
+DEEP_STATIC_CODE = """\
+import time
+def factorial(n):
+    if n <= 1:
+        time.sleep(10000)
+        return 1
+    return n * factorial(n-1)
+
+factorial(900)
+"""
+
+CODE_WITH_TONS_OF_THREADS = '''\
+import time
+import threading
+import random
+import math
+
+def cpu_intensive_work():
+    """Do some CPU intensive calculations"""
+    result = 0
+    for _ in range(10000):
+        result += math.sin(random.random()) * math.cos(random.random())
+    return result
+
+def io_intensive_work():
+    """Simulate IO intensive work with sleeps"""
+    time.sleep(0.1)
+
+def mixed_workload():
+    """Mix of CPU and IO work"""
+    while True:
+        if random.random() < 0.3:
+            cpu_intensive_work()
+        else:
+            io_intensive_work()
+
+def create_threads(n):
+    """Create n threads doing mixed workloads"""
+    threads = []
+    for _ in range(n):
+        t = threading.Thread(target=mixed_workload, daemon=True)
+        t.start()
+        threads.append(t)
+    return threads
+
+# Start with 5 threads
+active_threads = create_threads(5)
+
+# Main thread manages threads and does work
+while True:
+    # Randomly add threads up to the limit
+    if random.random() < 0.1:  # 10% chance each iteration
+        if random.random() < 0.5 and len(active_threads) < 100:
+            new_count = min(
+                random.randint(1, 5),
+                100 - len(active_threads),
+            )
+            new_threads = create_threads(new_count)
+            active_threads.extend(new_threads)
+
+    cpu_intensive_work()
+    time.sleep(0.05)
+'''
+
+ASYNC_CODE = '''\
+import asyncio
+import contextlib
+import math
+
+def compute_slice(seed):
+    result = 0.0
+    for i in range(2000):
+        result += math.sin(seed + i) * math.sqrt(i + 1)
+    return result
+
+async def leaf_task(seed):
+    total = 0.0
+    while True:
+        total += compute_slice(seed)
+        await asyncio.sleep(0)
+
+async def parent_task(seed):
+    child = asyncio.create_task(leaf_task(seed + 1000), name=f"leaf-{seed}")
+    try:
+        while True:
+            compute_slice(seed)
+            await asyncio.sleep(0.001)
+    finally:
+        child.cancel()
+        with contextlib.suppress(asyncio.CancelledError):
+            await child
+
+async def main():
+    tasks = [
+        asyncio.create_task(parent_task(i), name=f"parent-{i}")
+        for i in range(8)
+    ]
+    await asyncio.gather(*tasks)
+
+if __name__ == "__main__":
+    asyncio.run(main())
+'''
+
+
+FLAT_ALTERNATING_CODE = """\
+def leaf_a(): return sum(range(50))
+def leaf_b(): return sum(range(50))
+def hot_a(): return leaf_a()
+def hot_b(): return leaf_b()
+while True:
+    hot_a(); hot_b()
+"""
+
+
+def _expected_lines(code):
+    expected = {}
+    for number, line in enumerate(code.splitlines(), 1):
+        stripped = line.strip()
+        if stripped.startswith("def "):
+            expected[stripped[4:].split("(")[0].strip()] = number
+    return expected
+
+
+FLAT_ALTERNATING_LINES = _expected_lines(FLAT_ALTERNATING_CODE)
+
+
+def classify_flat(frames):
+    present = {}
+    for frame in frames:
+        if frame.funcname in FLAT_ALTERNATING_LINES:
+            present[frame.funcname] = _get_lineno(frame, -1)
+    if not present:
+        return False
+    for name, lineno in present.items():
+        if lineno != FLAT_ALTERNATING_LINES[name]:
+            return True
+    hot_a, hot_b = "hot_a" in present, "hot_b" in present
+    leaf_a, leaf_b = "leaf_a" in present, "leaf_b" in present
+    any_a = leaf_a or hot_a
+    any_b = leaf_b or hot_b
+    return (any_a and any_b) or (leaf_a and not hot_a) or (leaf_b and not 
hot_b)
+
+
+NESTED_ALTERNATING_CODE = """\
+def burn_a():
+    total = 0
+    for i in range(20000):
+        total += i
+    return total
+
+def burn_b():
+    total = 0
+    for i in range(20000):
+        total += i
+    return total
+
+def a_leaf():
+    return burn_a()
+
+def b_leaf():
+    return burn_b()
+
+def a_parent():
+    return a_leaf()
+
+def b_parent():
+    return b_leaf()
+
+while True:
+    a_parent()
+    b_parent()
+"""
+
+
+NESTED_ALTERNATING_BRANCHES = {
+    "a": ["a_parent", "a_leaf", "burn_a"],
+    "b": ["b_parent", "b_leaf", "burn_b"],
+}
+
+
+def classify_nested(frames):
+    frame_names = [frame.funcname for frame in frames]
+    names = set(frame_names)
+    present = [
+        family
+        for family, chain in NESTED_ALTERNATING_BRANCHES.items()
+        if names.intersection(chain)
+    ]
+    if len(present) > 1:
+        return True
+    if not present:
+        return False
+    chain = NESTED_ALTERNATING_BRANCHES[present[0]]
+    active = [name for name in chain if name in names]
+    depth = chain.index(active[-1])
+    if len(active) != depth + 1:
+        return True
+    indices = [frame_names.index(name) for name in reversed(active)]
+    return indices != sorted(indices)
+
+
+SHARED_LEAF_CODE = """\
+def shared_leaf(long_run):
+    total = 0
+    if long_run:
+        for i in range(50000):
+            total += i
+    else:
+        for i in range(200):
+            total += i
+    return total
+
+def a_wrapper():
+    return shared_leaf(False)
+
+def b_wrapper():
+    return shared_leaf(True)
+
+while True:
+    a_wrapper()
+    b_wrapper()
+"""
+
+
+def _branch_lines(code, marker):
+    for number, line in enumerate(code.splitlines(), 1):
+        if marker in line:
+            return {number, number + 1}
+    return set()
+
+
+SHARED_LEAF_LONG_LINES = _branch_lines(SHARED_LEAF_CODE, "range(50000)")
+SHARED_LEAF_SHORT_LINES = _branch_lines(SHARED_LEAF_CODE, "range(200)")
+
+
+def classify_shared(frames):
+    frame_names = [frame.funcname for frame in frames]
+    names = set(frame_names)
+    if "a_wrapper" in names and "b_wrapper" in names:
+        return True
+    if "shared_leaf" not in names:
+        return False
+    index = frame_names.index("shared_leaf")
+    parent = frame_names[index + 1] if index + 1 < len(frame_names) else None
+    if parent not in ("a_wrapper", "b_wrapper"):
+        return True
+    lineno = _get_lineno(frames[index], -1)
+    if lineno in SHARED_LEAF_LONG_LINES:
+        return parent != "b_wrapper"
+    if lineno in SHARED_LEAF_SHORT_LINES:
+        return parent != "a_wrapper"
+    return False
+
+
+GEN_ALTERNATING_CODE = """\
+def agen(n):
+    total = 0
+    for i in range(n):
+        total += i
+        yield i
+
+def bgen(n):
+    total = 0
+    for i in range(n):
+        total += i
+        yield i
+
+def drv_a():
+    for _ in agen(60):
+        pass
+
+def drv_b():
+    for _ in bgen(60):
+        pass
+
+while True:
+    drv_a()
+    drv_b()
+"""
+
+
+def classify_gen(frames):
+    names = {frame.funcname for frame in frames}
+    return ("agen" in names and "drv_b" in names) or (
+        "bgen" in names and "drv_a" in names
+    )
+
+
+DEEP_RECURSION_CODE = """\
+def leaf():
+    total = 0
+    for i in range(40):
+        total += i
+
+def a(n):
+    return a(n - 1) if n else leaf()
+
+def b(n):
+    return b(n - 1) if n else leaf()
+
+while True:
+    a(300)
+    b(300)
+"""
+
+
+def classify_recursion(frames):
+    names = {frame.funcname for frame in frames}
+    return "a" in names and "b" in names
+
+
+ASYNC_RUNNING_TASK_CODE = """\
+import asyncio
+
+
+def leaf_hot(n):
+    return sum(range(n))
+
+
+def leaf_rare(n):
+    return sum(range(n))
+
+
+async def run_hot():
+    while True:
+        leaf_hot(50000)
+        await asyncio.sleep(0)
+
+
+async def run_rare(k):
+    while True:
+        leaf_rare(500)
+        await asyncio.sleep(0)
+
+
+async def main():
+    tasks = [asyncio.create_task(run_hot(), name="hot")]
+    for k in range(8):
+        tasks.append(asyncio.create_task(run_rare(k), name=f"rare{k}"))
+    await asyncio.gather(*tasks)
+
+
+asyncio.run(main())
+"""
+
+
+def _name_tag(label):
+    label = (label or "").lower()
+    return "hot" if "hot" in label else "rare" if "rare" in label else None
+
+
+def _frame_tag(frames):
+    fns = {frame.funcname for frame in frames}
+    hot = bool(fns & {"run_hot", "leaf_hot"})
+    rare = bool(fns & {"run_rare", "leaf_rare"})
+    return (
+        "mixed"
+        if (hot and rare)
+        else "hot"
+        if hot
+        else "rare"
+        if rare
+        else None
+    )
+
+
+def classify_async_running_task(unit):
+    name = _name_tag(unit[1])
+    frame = _frame_tag(unit[3])
+    return name is not None and frame is not None and frame != name
+
+
+CODE_OBJECT_REUSE_CODE = """\
+SRC_A = "def func_a(n):\\n total=0\\n for i in range(n): total+=i*i\\n return 
total\\n"
+SRC_B = "def func_b(n):\\n total=0\\n for i in range(n): total+=i*i\\n return 
total\\n"
+WORK = 60000
+
+
+def build_a():
+    ns = {}
+    code = compile(SRC_A, "A_file.py", "exec")
+    exec(code, ns)
+    return ns["func_a"], code
+
+
+def build_b():
+    ns = {}
+    code = compile(SRC_B, "B_file.py", "exec")
+    exec(code, ns)
+    return ns["func_b"], code
+
+
+while True:
+    fa, ca = build_a()
+    fa(WORK)   # call_a
+    del fa, ca
+    fb, cb = build_b()
+    fb(WORK)   # call_b
+    del fb, cb
+"""
+
+
+def _marker_line(code, marker):
+    for number, line in enumerate(code.splitlines(), 1):
+        if marker in line:
+            return number
+    return None
+
+
+CALL_A_LINE = _marker_line(CODE_OBJECT_REUSE_CODE, "# call_a")
+CALL_B_LINE = _marker_line(CODE_OBJECT_REUSE_CODE, "# call_b")
+
+
+def classify_code_object_reuse(frames):
+    real = [f for f in frames if f.funcname != "<GC>"]
+    leaf = next((f for f in real if f.funcname in ("func_a", "func_b")), None)
+    if leaf is None:
+        return False
+    base = os.path.basename(leaf.filename)
+    fn = leaf.funcname
+    if (fn == "func_a" and base == "B_file.py") or (
+        fn == "func_b" and base == "A_file.py"
+    ):
+        return True
+    index = real.index(leaf)
+    caller = real[index + 1] if index + 1 < len(real) else None
+    line = _get_lineno(caller)
+    if line == CALL_A_LINE and fn == "func_b":
+        return True
+    if line == CALL_B_LINE and fn == "func_a":
+        return True
+    return False
+
+
+OVERSIZED_CHUNK_CODE = """\
+NLOCALS = 1800
+
+def make(name, tag, hotbody):
+    params = ", ".join(f"x{i}=0" for i in range(NLOCALS))
+    src = (
+        f"def hot_{tag}():\\n{hotbody}\\n"
+        f"def {name}({params}):\\n    return hot_{tag}()\\n"
+    )
+    exec(compile(src, f"{tag}.py", "exec"), globals())
+
+make("big_a", "a", "    s=0\\n    for i in range(2000):\\n        s+=i*3\\n    
return s")
+make("big_b", "b", "    s=1\\n    for i in range(2000):\\n        s^=(i<<1)\\n 
   return s")
+
+while True:
+    big_a()
+    big_b()
+"""
+
+
+OVERSIZED_A_FUNCS = {"big_a", "hot_a"}
+OVERSIZED_B_FUNCS = {"big_b", "hot_b"}
+
+
+def classify_oversized_chunk(frames):
+    saw_a = saw_b = False
+    for frame in frames:
+        base = os.path.basename(frame.filename)
+        fn = frame.funcname
+        if base == "a.py":
+            if fn in OVERSIZED_A_FUNCS:
+                saw_a = True
+            elif fn in OVERSIZED_B_FUNCS:
+                return True
+        elif base == "b.py":
+            if fn in OVERSIZED_B_FUNCS:
+                saw_b = True
+            elif fn in OVERSIZED_A_FUNCS:
+                return True
+    return saw_a and saw_b
+
+
+CODE_EXAMPLES = {
+    "basic": {
+        "code": CODE,
+        "description": "Mixed workload with fibonacci, computations, and 
string operations",
+    },
+    "deep_static": {
+        "code": DEEP_STATIC_CODE,
+        "description": "Deep recursive call stack with 900+ frames 
(factorial)",
+    },
+    "threads": {
+        "code": CODE_WITH_TONS_OF_THREADS,
+        "description": "Tons of threads doing mixed CPU/IO work",
+    },
+    "asyncio": {
+        "code": ASYNC_CODE,
+        "description": "Asyncio tasks with active and awaited coroutine 
chains",
+    },
+}
+
+CASES = {
+    "basic": (CODE, None),
+    "deep_static": (DEEP_STATIC_CODE, None),
+    "threads": (CODE_WITH_TONS_OF_THREADS, None),
+    "asyncio": (ASYNC_CODE, None, "get_async_stack_trace"),
+    "flat_alternating": (FLAT_ALTERNATING_CODE, classify_flat),
+    "nested_alternating": (NESTED_ALTERNATING_CODE, classify_nested),
+    "shared_leaf": (SHARED_LEAF_CODE, classify_shared),
+    "gen_alternating": (GEN_ALTERNATING_CODE, classify_gen),
+    "deep_recursion": (DEEP_RECURSION_CODE, classify_recursion),
+    "async_running_task": (
+        ASYNC_RUNNING_TASK_CODE,
+        classify_async_running_task,
+        "get_async_stack_trace",
+    ),
+    "code_object_reuse": (CODE_OBJECT_REUSE_CODE, classify_code_object_reuse),
+    "oversized_chunk": (OVERSIZED_CHUNK_CODE, classify_oversized_chunk),
+}

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to