diff --git a/_thread_tracer/__init__.py b/_thread_tracer/__init__.py new file mode 100644 index 00000000..61fb6cda --- /dev/null +++ b/_thread_tracer/__init__.py @@ -0,0 +1,10 @@ +"""Private experimental thread spawn tracer (not part of the public API).""" + +from _thread_tracer._types import ThreadSpawnStats, ThreadTracerError +from _thread_tracer._windows_etw import WindowsThreadSpawnTracer + +__all__ = [ + "ThreadSpawnStats", + "ThreadTracerError", + "WindowsThreadSpawnTracer", +] diff --git a/_thread_tracer/_parsing.py b/_thread_tracer/_parsing.py new file mode 100644 index 00000000..14812ee3 --- /dev/null +++ b/_thread_tracer/_parsing.py @@ -0,0 +1,68 @@ +"""Pure-Python helpers to classify kernel thread ETW events.""" + +import ctypes as ct +import struct + +# Kernel thread event opcodes (EVENT_TRACE_TYPE_* for the Thread task). +EVENT_TRACE_TYPE_START = 1 +EVENT_TRACE_TYPE_END = 2 +EVENT_TRACE_TYPE_DCSTART = 3 +EVENT_TRACE_TYPE_DCEND = 4 + +_MIN_USER_DATA_LENGTH = 8 + + +def parse_kernel_thread_payload(user_data, user_data_length): + """Extract ``(process_id, thread_id)`` from a kernel Thread event payload. + + The first two fields are stable across Thread event schema versions. Pointer + fields that follow are ignored because only the identifiers are needed. + """ + if user_data is None or user_data_length < _MIN_USER_DATA_LENGTH: + return None + + if isinstance(user_data, (bytes, bytearray)): + raw = user_data[:user_data_length] + else: + address = user_data + if not isinstance(address, int): + address = ct.cast(user_data, ct.c_void_p).value or 0 + if not address: + return None + raw = ct.string_at(address, user_data_length) + + if len(raw) < _MIN_USER_DATA_LENGTH: + return None + process_id, thread_id = struct.unpack_from("= 1 + """ + stats = _run_traced_child(body) + _assert_spawn_count_relative_to_baseline( + stats, + noop_child_spawn_count, + expected_extra_spawn_count, + ) + + +def test_tracer_counts_blas_thread_spawns(noop_child_spawn_count): + pytest.importorskip("numpy") + _configure_blas_thread_env() + + import numpy as np + + rng = np.random.RandomState(0) + warmup = rng.rand(100, 100) + np.dot(warmup, warmup) + expected_extra_spawn_count = _expected_pool_spawn_count("blas") + + body = """ + import os + + os.environ["OMP_NUM_THREADS"] = "1" + os.environ["OPENBLAS_NUM_THREADS"] = "4" + os.environ["MKL_NUM_THREADS"] = "4" + + import numpy as np + + rng = np.random.RandomState(0) + a = rng.rand(2000, 2000) + np.dot(a, a) + """ + stats = _run_traced_child(body) + _assert_spawn_count_relative_to_baseline( + stats, + noop_child_spawn_count, + expected_extra_spawn_count, + )