Skip to content
Open
111 changes: 111 additions & 0 deletions Lib/test/test_tools/test_inspection.py
Original file line number Diff line number Diff line change
@@ -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()
Original file line number Diff line number Diff line change
@@ -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.
200 changes: 1 addition & 199 deletions Tools/inspection/benchmark_external_inspection.py
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
Loading
Loading