Skip to content

Commit 4bf929f

Browse files
cursoragentogrisel
andcommitted
Add private Windows ETW thread spawn tracer prototype
Introduce threadpoolctl._thread_tracer as an experimental private package with a ctypes-based WindowsThreadSpawnTracer that counts kernel Thread Start events for a target PID via the NT Kernel Logger. Includes pure-Python event payload parsing, mocked ETW lifecycle tests that run on all platforms, and keeps the tracer out of the public threadpoolctl API. Co-authored-by: Olivier Grisel <olivier.grisel@ensta.org>
1 parent 8d6100c commit 4bf929f

5 files changed

Lines changed: 689 additions & 0 deletions

File tree

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
"""Private experimental thread spawn tracer (not part of the public API)."""
2+
3+
import sys
4+
5+
from threadpoolctl._thread_tracer._types import ThreadSpawnStats, ThreadTracerError
6+
from threadpoolctl._thread_tracer._windows_etw import WindowsThreadSpawnTracer
7+
8+
9+
def windows_tracer_available():
10+
"""Return whether the Windows ETW tracer can run on this platform."""
11+
return sys.platform == "win32"
12+
13+
14+
__all__ = [
15+
"ThreadSpawnStats",
16+
"ThreadTracerError",
17+
"WindowsThreadSpawnTracer",
18+
"windows_tracer_available",
19+
]
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
"""Pure-Python helpers to classify kernel thread ETW events."""
2+
3+
import ctypes as ct
4+
import struct
5+
6+
# Kernel thread event opcodes (EVENT_TRACE_TYPE_* for the Thread task).
7+
EVENT_TRACE_TYPE_START = 1
8+
EVENT_TRACE_TYPE_END = 2
9+
EVENT_TRACE_TYPE_DCSTART = 3
10+
EVENT_TRACE_TYPE_DCEND = 4
11+
12+
_MIN_USER_DATA_LENGTH = 8
13+
14+
15+
def parse_kernel_thread_payload(user_data, user_data_length):
16+
"""Extract ``(process_id, thread_id)`` from a kernel Thread event payload.
17+
18+
The first two fields are stable across Thread event schema versions. Pointer
19+
fields that follow are ignored because only the identifiers are needed.
20+
"""
21+
if user_data is None or user_data_length < _MIN_USER_DATA_LENGTH:
22+
return None
23+
24+
if isinstance(user_data, (bytes, bytearray)):
25+
raw = user_data[:user_data_length]
26+
else:
27+
address = user_data
28+
if not isinstance(address, int):
29+
address = ct.cast(user_data, ct.c_void_p).value or 0
30+
if not address:
31+
return None
32+
raw = ct.string_at(address, user_data_length)
33+
34+
if len(raw) < _MIN_USER_DATA_LENGTH:
35+
return None
36+
process_id, thread_id = struct.unpack_from("<II", raw, 0)
37+
return process_id, thread_id
38+
39+
40+
def classify_kernel_thread_event(opcode, user_data, user_data_length, target_pid):
41+
"""Return a spawn classification for a kernel Thread event.
42+
43+
Returns one of:
44+
- ``"spawn"`` for a thread start event in ``target_pid``
45+
- ``"existing"`` for a DCStart rundown event in ``target_pid``
46+
- ``None`` if the event should be ignored
47+
"""
48+
if opcode not in (
49+
EVENT_TRACE_TYPE_START,
50+
EVENT_TRACE_TYPE_END,
51+
EVENT_TRACE_TYPE_DCSTART,
52+
EVENT_TRACE_TYPE_DCEND,
53+
):
54+
return None
55+
56+
parsed = parse_kernel_thread_payload(user_data, user_data_length)
57+
if parsed is None:
58+
return None
59+
60+
process_id, _thread_id = parsed
61+
if process_id != target_pid:
62+
return None
63+
64+
if opcode == EVENT_TRACE_TYPE_START:
65+
return "spawn"
66+
if opcode == EVENT_TRACE_TYPE_DCSTART:
67+
return "existing"
68+
return None
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
"""Private types for the experimental thread spawn tracer."""
2+
3+
4+
class ThreadSpawnStats(object):
5+
"""Statistics collected during a thread tracing window."""
6+
7+
__slots__ = ("spawn_count", "existing_thread_count", "thread_ids")
8+
9+
def __init__(self, spawn_count=0, existing_thread_count=0, thread_ids=None):
10+
self.spawn_count = spawn_count
11+
self.existing_thread_count = existing_thread_count
12+
self.thread_ids = frozenset(thread_ids or ())
13+
14+
def __eq__(self, other):
15+
if not isinstance(other, ThreadSpawnStats):
16+
return NotImplemented
17+
return (
18+
self.spawn_count == other.spawn_count
19+
and self.existing_thread_count == other.existing_thread_count
20+
and self.thread_ids == other.thread_ids
21+
)
22+
23+
def __repr__(self):
24+
return (
25+
"ThreadSpawnStats(spawn_count={0.spawn_count}, "
26+
"existing_thread_count={0.existing_thread_count}, "
27+
"thread_ids={0.thread_ids!r})"
28+
).format(self)
29+
30+
31+
class ThreadTracerError(OSError):
32+
"""Raised when the platform tracer cannot start or stop cleanly."""

0 commit comments

Comments
 (0)