austin-python 2.2.0__tar.gz → 2.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {austin_python-2.2.0 → austin_python-2.3.0}/PKG-INFO +2 -2
- {austin_python-2.2.0 → austin_python-2.3.0}/austin/events.py +30 -0
- {austin_python-2.2.0 → austin_python-2.3.0}/austin/format/mojo.py +361 -11
- austin_python-2.3.0/test/format/test_mojo.py +504 -0
- austin_python-2.2.0/test/format/test_mojo.py +0 -235
- {austin_python-2.2.0 → austin_python-2.3.0}/.flake8 +0 -0
- {austin_python-2.2.0 → austin_python-2.3.0}/.gitignore +0 -0
- {austin_python-2.2.0 → austin_python-2.3.0}/.readthedocs.yml +0 -0
- {austin_python-2.2.0 → austin_python-2.3.0}/AUTHORS +0 -0
- {austin_python-2.2.0 → austin_python-2.3.0}/CODE-OF-CONDUCT.md +0 -0
- {austin_python-2.2.0 → austin_python-2.3.0}/CONTRIBUTING.md +0 -0
- {austin_python-2.2.0 → austin_python-2.3.0}/COPYING +0 -0
- {austin_python-2.2.0 → austin_python-2.3.0}/ISSUE_TEMPLATE.md +0 -0
- {austin_python-2.2.0 → austin_python-2.3.0}/LICENSE.md +0 -0
- {austin_python-2.2.0 → austin_python-2.3.0}/MANIFEST.in +0 -0
- {austin_python-2.2.0 → austin_python-2.3.0}/PULL_REQUEST_TEMPLATE.md +0 -0
- {austin_python-2.2.0 → austin_python-2.3.0}/README.md +0 -0
- {austin_python-2.2.0 → austin_python-2.3.0}/austin/__init__.py +0 -0
- {austin_python-2.2.0 → austin_python-2.3.0}/austin/aio.py +0 -0
- {austin_python-2.2.0 → austin_python-2.3.0}/austin/base.py +0 -0
- {austin_python-2.2.0 → austin_python-2.3.0}/austin/cli.py +0 -0
- {austin_python-2.2.0 → austin_python-2.3.0}/austin/config.py +0 -0
- {austin_python-2.2.0 → austin_python-2.3.0}/austin/errors.py +0 -0
- {austin_python-2.2.0 → austin_python-2.3.0}/austin/format/__init__.py +0 -0
- {austin_python-2.2.0 → austin_python-2.3.0}/austin/format/collapsed_stack.py +0 -0
- {austin_python-2.2.0 → austin_python-2.3.0}/austin/format/compress.py +0 -0
- {austin_python-2.2.0 → austin_python-2.3.0}/austin/format/mojo_compress.py +0 -0
- {austin_python-2.2.0 → austin_python-2.3.0}/austin/format/pprof/__init__.py +0 -0
- {austin_python-2.2.0 → austin_python-2.3.0}/austin/format/pprof/__main__.py +0 -0
- {austin_python-2.2.0 → austin_python-2.3.0}/austin/format/pprof/profile.proto +0 -0
- {austin_python-2.2.0 → austin_python-2.3.0}/austin/format/pprof/profile_pb2.py +0 -0
- {austin_python-2.2.0 → austin_python-2.3.0}/austin/format/speedscope.py +0 -0
- {austin_python-2.2.0 → austin_python-2.3.0}/austin/simple.py +0 -0
- {austin_python-2.2.0 → austin_python-2.3.0}/austin/stats.py +0 -0
- {austin_python-2.2.0 → austin_python-2.3.0}/austin/threads.py +0 -0
- {austin_python-2.2.0 → austin_python-2.3.0}/austin/tools/diff.py +0 -0
- {austin_python-2.2.0 → austin_python-2.3.0}/austin/tools/mojodbg.py +0 -0
- {austin_python-2.2.0 → austin_python-2.3.0}/austin/tools/resolve.py +0 -0
- {austin_python-2.2.0 → austin_python-2.3.0}/pyproject.toml +0 -0
- {austin_python-2.2.0 → austin_python-2.3.0}/test/__init__.py +0 -0
- {austin_python-2.2.0 → austin_python-2.3.0}/test/conftest.py +0 -0
- {austin_python-2.2.0 → austin_python-2.3.0}/test/data/austin.json +0 -0
- {austin_python-2.2.0 → austin_python-2.3.0}/test/data/austin.out +0 -0
- {austin_python-2.2.0 → austin_python-2.3.0}/test/data/austin.pprof +0 -0
- {austin_python-2.2.0 → austin_python-2.3.0}/test/data/austinp.mojo +0 -0
- {austin_python-2.2.0 → austin_python-2.3.0}/test/data/austinp.resolved.mojo +0 -0
- {austin_python-2.2.0 → austin_python-2.3.0}/test/data/column.mojo +0 -0
- {austin_python-2.2.0 → austin_python-2.3.0}/test/data/column.py +0 -0
- {austin_python-2.2.0 → austin_python-2.3.0}/test/data/diff_a-b.austin +0 -0
- {austin_python-2.2.0 → austin_python-2.3.0}/test/data/diff_a.austin +0 -0
- {austin_python-2.2.0 → austin_python-2.3.0}/test/data/diff_b.austin +0 -0
- {austin_python-2.2.0 → austin_python-2.3.0}/test/data/mp.austin +0 -0
- {austin_python-2.2.0 → austin_python-2.3.0}/test/data/mp.mojo +0 -0
- {austin_python-2.2.0 → austin_python-2.3.0}/test/data/repeat.austin +0 -0
- {austin_python-2.2.0 → austin_python-2.3.0}/test/data/repeat.mojo +0 -0
- {austin_python-2.2.0 → austin_python-2.3.0}/test/data/test.austin +0 -0
- {austin_python-2.2.0 → austin_python-2.3.0}/test/data/test.mojo +0 -0
- {austin_python-2.2.0 → austin_python-2.3.0}/test/format/__init__.py +0 -0
- {austin_python-2.2.0 → austin_python-2.3.0}/test/format/test_compress.py +0 -0
- {austin_python-2.2.0 → austin_python-2.3.0}/test/format/test_mojo_compress.py +0 -0
- {austin_python-2.2.0 → austin_python-2.3.0}/test/format/test_pprof.py +0 -0
- {austin_python-2.2.0 → austin_python-2.3.0}/test/format/test_speedscope.py +0 -0
- {austin_python-2.2.0 → austin_python-2.3.0}/test/stats/test_austin_file_reader.py +0 -0
- {austin_python-2.2.0 → austin_python-2.3.0}/test/stats/test_austin_stats.py +0 -0
- {austin_python-2.2.0 → austin_python-2.3.0}/test/stats/test_hierarchical_stats.py +0 -0
- {austin_python-2.2.0 → austin_python-2.3.0}/test/test_aio.py +0 -0
- {austin_python-2.2.0 → austin_python-2.3.0}/test/test_base.py +0 -0
- {austin_python-2.2.0 → austin_python-2.3.0}/test/test_cli.py +0 -0
- {austin_python-2.2.0 → austin_python-2.3.0}/test/test_config.py +0 -0
- {austin_python-2.2.0 → austin_python-2.3.0}/test/test_semver.py +0 -0
- {austin_python-2.2.0 → austin_python-2.3.0}/test/test_simple.py +0 -0
- {austin_python-2.2.0 → austin_python-2.3.0}/test/test_threads.py +0 -0
- {austin_python-2.2.0 → austin_python-2.3.0}/test/tools/test_diff.py +0 -0
- {austin_python-2.2.0 → austin_python-2.3.0}/test/tools/test_resolve.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: austin-python
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.0
|
|
4
4
|
Summary: Python wrapper for Austin, the CPython frame stack sampler.
|
|
5
5
|
Project-URL: homepage, https://github.com/P403n1x87/austin-python
|
|
6
6
|
Project-URL: documentation, https://austin-python.readthedocs.io
|
|
@@ -43,6 +43,35 @@ class AustinMetadata(AustinEvent):
|
|
|
43
43
|
value: str
|
|
44
44
|
|
|
45
45
|
|
|
46
|
+
@dataclass(frozen=True)
|
|
47
|
+
class AustinTask:
|
|
48
|
+
"""A node in an asyncio task tree (3.14+ only).
|
|
49
|
+
|
|
50
|
+
``frames`` is this task's own last-known suspended coroutine-chain
|
|
51
|
+
snapshot -- empty if it was never captured suspended (e.g. it was only
|
|
52
|
+
ever seen actively running rather than awaiting something). ``awaiting``
|
|
53
|
+
are the tasks this task is itself awaiting, i.e. the tasks for which this
|
|
54
|
+
task is the direct waiter.
|
|
55
|
+
|
|
56
|
+
``elapsed`` does NOT describe ``frames``. Austin only re-samples a task's
|
|
57
|
+
chain (and reports a new node) when its identity has actually changed
|
|
58
|
+
since the last scan, so the two are always one step apart: ``frames`` is
|
|
59
|
+
the task's brand new position (just captured because it changed), while
|
|
60
|
+
``elapsed`` is how long the task dwelled at its *previous*, now-replaced
|
|
61
|
+
position -- the earliest point at which that duration could be known at
|
|
62
|
+
all. None on a task's first-ever sighting, when there is no previous
|
|
63
|
+
position to report a duration for. Always a wall/CPU time value, or None
|
|
64
|
+
if Austin was run in pure memory mode (no per-task memory delta exists to
|
|
65
|
+
report -- see austin's own py_proc.c, _py_proc__maybe_discover_asyncio).
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
task_id: int
|
|
69
|
+
name: t.Optional[str]
|
|
70
|
+
frames: t.Tuple[AustinFrame, ...] = ()
|
|
71
|
+
elapsed: t.Optional[MicroSeconds] = None
|
|
72
|
+
awaiting: t.Tuple["AustinTask", ...] = ()
|
|
73
|
+
|
|
74
|
+
|
|
46
75
|
@dataclass(frozen=True)
|
|
47
76
|
class AustinSample(AustinEvent):
|
|
48
77
|
"""Austin sample."""
|
|
@@ -63,6 +92,7 @@ class AustinSample(AustinEvent):
|
|
|
63
92
|
frames: t.Optional[t.Tuple[AustinFrame, ...]] = None
|
|
64
93
|
gc: t.Optional[bool] = None
|
|
65
94
|
idle: t.Optional[bool] = None
|
|
95
|
+
tasks: t.Tuple[AustinTask, ...] = ()
|
|
66
96
|
|
|
67
97
|
def key(
|
|
68
98
|
self,
|
|
@@ -12,10 +12,16 @@ from austin.events import AustinFrame
|
|
|
12
12
|
from austin.events import AustinMetadata
|
|
13
13
|
from austin.events import AustinMetrics
|
|
14
14
|
from austin.events import AustinSample
|
|
15
|
+
from austin.events import AustinTask
|
|
15
16
|
from austin.events import InterpreterId
|
|
16
17
|
from austin.events import ProcessId
|
|
17
18
|
from austin.events import ThreadName
|
|
18
19
|
|
|
20
|
+
# Guards against a corrupted/cyclic waiter graph in the source stream -- a
|
|
21
|
+
# real await chain never comes anywhere close to this. Mirrors austin's own
|
|
22
|
+
# WHERE_MAX_TREE_DEPTH (src/events.c).
|
|
23
|
+
MAX_TASK_TREE_DEPTH = 64
|
|
24
|
+
|
|
19
25
|
|
|
20
26
|
def to_varint(n: int) -> bytes:
|
|
21
27
|
"""Convert an integer to a variable-length integer."""
|
|
@@ -67,6 +73,8 @@ class MojoEvents:
|
|
|
67
73
|
STRING = 11
|
|
68
74
|
STRING_REF = 12
|
|
69
75
|
STACK_REPEAT = 13
|
|
76
|
+
TASK_STACK = 14
|
|
77
|
+
TASK_WAITER = 15
|
|
70
78
|
|
|
71
79
|
|
|
72
80
|
class MojoEventHandler:
|
|
@@ -197,6 +205,44 @@ class MojoStackRepeat(MojoEvent):
|
|
|
197
205
|
EVENT_ID = MojoEvents.STACK_REPEAT
|
|
198
206
|
|
|
199
207
|
|
|
208
|
+
@dataclass(frozen=True, eq=True)
|
|
209
|
+
class MojoTaskWaiter(MojoEvent):
|
|
210
|
+
"""MOJO task waiter event.
|
|
211
|
+
|
|
212
|
+
One edge of a task's waiter DAG: ``task_id`` is awaited by
|
|
213
|
+
``waiter_id``.
|
|
214
|
+
"""
|
|
215
|
+
|
|
216
|
+
EVENT_ID = MojoEvents.TASK_WAITER
|
|
217
|
+
|
|
218
|
+
task_id: int
|
|
219
|
+
waiter_id: int
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
@dataclass(frozen=True, eq=True)
|
|
223
|
+
class MojoTaskStack(MojoEvent):
|
|
224
|
+
"""MOJO task stack event.
|
|
225
|
+
|
|
226
|
+
Introduces the coroutine stack of a suspended task, keyed by the remote
|
|
227
|
+
address of its TaskObj and its (possibly cached) name key (0 if
|
|
228
|
+
unresolved). The frames that make up the stack, and the trailing time
|
|
229
|
+
metric that terminates it, follow as ordinary MOJO_FRAME/MOJO_FRAME_REF/
|
|
230
|
+
MOJO_METRIC_TIME events rather than being part of this event itself --
|
|
231
|
+
see BaseMojoStreamReader's task-stack handling.
|
|
232
|
+
|
|
233
|
+
The trailing metric is always a time value, and describes how long the
|
|
234
|
+
task dwelled at its PREVIOUS position, not the frames that just followed
|
|
235
|
+
this event -- see AustinTask's own docstring for why the two are always
|
|
236
|
+
one step apart. It never appears at all when austin was run in pure
|
|
237
|
+
memory mode.
|
|
238
|
+
"""
|
|
239
|
+
|
|
240
|
+
EVENT_ID = MojoEvents.TASK_STACK
|
|
241
|
+
|
|
242
|
+
task_id: int
|
|
243
|
+
name_key: int
|
|
244
|
+
|
|
245
|
+
|
|
200
246
|
@dataclass(frozen=True, eq=True)
|
|
201
247
|
class MojoFrame(MojoEvent):
|
|
202
248
|
"""MOJO frame."""
|
|
@@ -266,6 +312,29 @@ class _RunningSample:
|
|
|
266
312
|
idle: t.Optional[bool] = None
|
|
267
313
|
|
|
268
314
|
|
|
315
|
+
@dataclass
|
|
316
|
+
class _RunningTaskStack:
|
|
317
|
+
task_id: int
|
|
318
|
+
name_key: int
|
|
319
|
+
frames: t.List[MojoFrame] = field(default_factory=list)
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
@dataclass
|
|
323
|
+
class _TaskInfo:
|
|
324
|
+
"""Best-known state for one task_id.
|
|
325
|
+
|
|
326
|
+
Built up from MOJO_TASK_STACK events as they arrive. ``owner`` is the
|
|
327
|
+
(pid, thread) of whichever sample's wire bracket this task's most recent
|
|
328
|
+
MOJO_TASK_STACK fell within -- see BaseMojoStreamReader's task-stack
|
|
329
|
+
handling for why that bracket is the only ownership signal that exists.
|
|
330
|
+
"""
|
|
331
|
+
|
|
332
|
+
name: t.Optional[str]
|
|
333
|
+
owner: t.Tuple[ProcessId, ThreadName]
|
|
334
|
+
frames: t.Tuple[AustinFrame, ...] = ()
|
|
335
|
+
elapsed: t.Optional[int] = None
|
|
336
|
+
|
|
337
|
+
|
|
269
338
|
def int_reader() -> t.Generator[t.Optional[int], bytes, int]:
|
|
270
339
|
(b,) = yield None
|
|
271
340
|
while True:
|
|
@@ -340,6 +409,26 @@ class BaseMojoStreamReader(AustinEventIterator):
|
|
|
340
409
|
# last fully-finalised sample for that thread.
|
|
341
410
|
self._prev_frames: t.Dict[t.Tuple[int, str], t.List[MojoFrame]] = {}
|
|
342
411
|
|
|
412
|
+
# Suspended-task coroutine-chain state (3.14+ asyncio). A task stack's
|
|
413
|
+
# frames/terminating time metric are ordinary MOJO_FRAME/MOJO_FRAME_REF/
|
|
414
|
+
# MOJO_METRIC_TIME events on the wire, indistinguishable by event ID
|
|
415
|
+
# alone from those of the enclosing regular stack sample -- so while
|
|
416
|
+
# this is not None, get_frame_ref/get_time_metric route to it instead
|
|
417
|
+
# of the running sample.
|
|
418
|
+
self._running_task_stack: t.Optional[_RunningTaskStack] = None
|
|
419
|
+
|
|
420
|
+
# Best-known state per task_id, keyed by remote TaskObj address.
|
|
421
|
+
self._task_info: t.Dict[int, _TaskInfo] = {}
|
|
422
|
+
|
|
423
|
+
# Best-known waiter DAG edges: (task_id, waiter_id) pairs, meaning
|
|
424
|
+
# task_id is awaited by waiter_id. The wire re-emits a task's
|
|
425
|
+
# *complete* current waiter set whenever it changes (never a diff --
|
|
426
|
+
# see py_asyncio.c's fingerprint comment), so a fresh batch for a
|
|
427
|
+
# task_id replaces its previous edges; batch boundaries are inferred
|
|
428
|
+
# from consecutive same-task_id events (see get_task_waiter).
|
|
429
|
+
self._task_edges: t.Set[t.Tuple[int, int]] = set()
|
|
430
|
+
self._last_waiter_task_id: t.Optional[int] = None
|
|
431
|
+
|
|
343
432
|
# Austin events
|
|
344
433
|
self.metadata: t.Dict[str, str] = {}
|
|
345
434
|
self.samples: t.List[AustinSample] = []
|
|
@@ -416,6 +505,9 @@ class BaseMojoStreamReader(AustinEventIterator):
|
|
|
416
505
|
),
|
|
417
506
|
gc=self._running_sample.gc,
|
|
418
507
|
idle=self._running_sample.idle,
|
|
508
|
+
tasks=self.get_tasks(
|
|
509
|
+
self._running_sample.pid, self._running_sample.thread
|
|
510
|
+
),
|
|
419
511
|
)
|
|
420
512
|
)
|
|
421
513
|
|
|
@@ -499,11 +591,148 @@ class BaseMojoStreamReader(AustinEventIterator):
|
|
|
499
591
|
"""Parse a frame reference."""
|
|
500
592
|
frame = self._frame_map[self.ref(ref)]
|
|
501
593
|
|
|
502
|
-
|
|
503
|
-
|
|
594
|
+
if self._running_task_stack is not None:
|
|
595
|
+
self._running_task_stack.frames.append(frame)
|
|
596
|
+
else:
|
|
597
|
+
assert self._running_sample is not None, self._running_sample
|
|
598
|
+
self._running_sample.frames.append(frame)
|
|
504
599
|
|
|
505
600
|
return MojoFrameReference(frame)
|
|
506
601
|
|
|
602
|
+
def get_task_waiter(self, task_id: int, waiter_id: int) -> MojoTaskWaiter:
|
|
603
|
+
"""Parse a task waiter edge."""
|
|
604
|
+
if self._last_waiter_task_id != task_id:
|
|
605
|
+
# Starting a fresh batch for this task_id: the wire re-emits the
|
|
606
|
+
# complete current waiter set whenever it changes, so drop
|
|
607
|
+
# whatever we knew about this task_id's waiters before.
|
|
608
|
+
self._task_edges = {e for e in self._task_edges if e[0] != task_id}
|
|
609
|
+
self._last_waiter_task_id = task_id
|
|
610
|
+
|
|
611
|
+
self._task_edges.add((task_id, waiter_id))
|
|
612
|
+
|
|
613
|
+
return MojoTaskWaiter(task_id, waiter_id)
|
|
614
|
+
|
|
615
|
+
def get_task_stack(self, task_id: int, name_key: int) -> MojoTaskStack:
|
|
616
|
+
"""Parse the header of a suspended task's coroutine-chain snapshot.
|
|
617
|
+
|
|
618
|
+
The frames and terminating time metric that complete it arrive as
|
|
619
|
+
subsequent, ordinary events -- see get_frame_ref/get_time_metric.
|
|
620
|
+
"""
|
|
621
|
+
self._running_task_stack = _RunningTaskStack(task_id, name_key)
|
|
622
|
+
|
|
623
|
+
return MojoTaskStack(task_id, name_key)
|
|
624
|
+
|
|
625
|
+
def _finalize_task_stack(self, elapsed: int) -> None:
|
|
626
|
+
"""Finalize the running task stack.
|
|
627
|
+
|
|
628
|
+
Triggered by its terminating MOJO_METRIC_TIME (see get_time_metric).
|
|
629
|
+
``ts.frames`` (the task's brand new position) and ``elapsed`` (how
|
|
630
|
+
long it dwelled at its previous one) describe two different moments
|
|
631
|
+
even though they land in the same _TaskInfo/AustinTask -- see
|
|
632
|
+
AustinTask's own docstring. The task's owner is recorded as whichever
|
|
633
|
+
sample is currently being assembled: this event only ever arrives
|
|
634
|
+
bracketed inside that sample's own MOJO_STACK...next-MOJO_STACK
|
|
635
|
+
window, so there's nothing else it could belong to.
|
|
636
|
+
|
|
637
|
+
An empty ``ts.frames`` is a closing flush -- the task has just been
|
|
638
|
+
evicted (completed, or otherwise gone), and this only carries its
|
|
639
|
+
final dwell time, with no new content and no meaningful owner (by
|
|
640
|
+
the time eviction runs, every thread for this cycle has already had
|
|
641
|
+
its turn, so "whichever sample is currently being assembled" is
|
|
642
|
+
arbitrary here, not this task's actual owner). Only update elapsed
|
|
643
|
+
on whatever's already known; leave frames/owner untouched rather
|
|
644
|
+
than wiping the task's last real position right as it disappears.
|
|
645
|
+
"""
|
|
646
|
+
ts = self._running_task_stack
|
|
647
|
+
assert ts is not None, ts
|
|
648
|
+
assert self._running_sample is not None, self._running_sample
|
|
649
|
+
|
|
650
|
+
if not ts.frames:
|
|
651
|
+
existing = self._task_info.get(ts.task_id)
|
|
652
|
+
if existing is not None:
|
|
653
|
+
existing.elapsed = elapsed
|
|
654
|
+
self._running_task_stack = None
|
|
655
|
+
return
|
|
656
|
+
|
|
657
|
+
name = self._lookup_string(ts.name_key).value if ts.name_key else None
|
|
658
|
+
|
|
659
|
+
self._task_info[ts.task_id] = _TaskInfo(
|
|
660
|
+
name=name,
|
|
661
|
+
owner=(self._running_sample.pid, self._running_sample.thread),
|
|
662
|
+
frames=tuple(
|
|
663
|
+
AustinFrame(
|
|
664
|
+
filename=mf.filename.value,
|
|
665
|
+
function=mf.scope.value,
|
|
666
|
+
line=mf.line,
|
|
667
|
+
line_end=mf.line_end,
|
|
668
|
+
column=mf.column,
|
|
669
|
+
column_end=mf.column_end,
|
|
670
|
+
)
|
|
671
|
+
for mf in ts.frames
|
|
672
|
+
),
|
|
673
|
+
elapsed=elapsed,
|
|
674
|
+
)
|
|
675
|
+
|
|
676
|
+
self._running_task_stack = None
|
|
677
|
+
|
|
678
|
+
def get_tasks(self, pid: ProcessId, thread: ThreadName) -> t.Tuple[AustinTask, ...]:
|
|
679
|
+
"""Return the current best-known root tasks owned by (pid, thread).
|
|
680
|
+
|
|
681
|
+
Every root task it owns (nobody awaits it), and everything it
|
|
682
|
+
transitively awaits, as one AustinTask per root -- a thread can own
|
|
683
|
+
more than one at once (e.g. several siblings under one gather()).
|
|
684
|
+
|
|
685
|
+
Mirrors austin's own where_event_handler__render_tree: a task only
|
|
686
|
+
appears if it was itself captured suspended at least once (a waiter
|
|
687
|
+
edge naming a task we never saw a MOJO_TASK_STACK for -- e.g. it was
|
|
688
|
+
only ever seen actively running -- is omitted, exactly like austin's
|
|
689
|
+
own where_event_handler__find_task returning NULL for it). Like that
|
|
690
|
+
C-side counterpart, this is a no-op (returns empty) when (pid,
|
|
691
|
+
thread) owns no tasks, so callers can call it unconditionally.
|
|
692
|
+
"""
|
|
693
|
+
|
|
694
|
+
def has_parent(task_id: int) -> bool:
|
|
695
|
+
return any(t_id == task_id for t_id, _ in self._task_edges)
|
|
696
|
+
|
|
697
|
+
def build(task_id: int, depth: int) -> AustinTask:
|
|
698
|
+
info = self._task_info[task_id]
|
|
699
|
+
awaiting = (
|
|
700
|
+
tuple(
|
|
701
|
+
build(awaited_id, depth + 1)
|
|
702
|
+
for awaited_id, waiter_id in self._task_edges
|
|
703
|
+
if waiter_id == task_id and awaited_id in self._task_info
|
|
704
|
+
)
|
|
705
|
+
if depth < MAX_TASK_TREE_DEPTH
|
|
706
|
+
else ()
|
|
707
|
+
)
|
|
708
|
+
return AustinTask(
|
|
709
|
+
task_id=task_id,
|
|
710
|
+
name=info.name,
|
|
711
|
+
frames=info.frames,
|
|
712
|
+
elapsed=info.elapsed,
|
|
713
|
+
awaiting=awaiting,
|
|
714
|
+
)
|
|
715
|
+
|
|
716
|
+
owner = (pid, thread)
|
|
717
|
+
roots = [
|
|
718
|
+
task_id
|
|
719
|
+
for task_id, info in self._task_info.items()
|
|
720
|
+
if info.owner == owner and not has_parent(task_id)
|
|
721
|
+
]
|
|
722
|
+
|
|
723
|
+
if not roots:
|
|
724
|
+
# Best-effort fallback, matching austin's own render_tree: if
|
|
725
|
+
# every one of this owner's tasks appears to have a parent (a
|
|
726
|
+
# cyclic/bogus edge), there's no true root -- show each as its
|
|
727
|
+
# own top-level tree rather than silently showing nothing.
|
|
728
|
+
roots = [
|
|
729
|
+
task_id
|
|
730
|
+
for task_id, info in self._task_info.items()
|
|
731
|
+
if info.owner == owner
|
|
732
|
+
]
|
|
733
|
+
|
|
734
|
+
return tuple(build(task_id, 0) for task_id in roots)
|
|
735
|
+
|
|
507
736
|
def get_kernel_frame(self, name: str) -> MojoKernelFrame:
|
|
508
737
|
"""Parse kernel frame."""
|
|
509
738
|
return MojoKernelFrame(name)
|
|
@@ -517,7 +746,17 @@ class BaseMojoStreamReader(AustinEventIterator):
|
|
|
517
746
|
return metric
|
|
518
747
|
|
|
519
748
|
def get_time_metric(self, value: int) -> MojoMetric:
|
|
520
|
-
"""Parse time metric.
|
|
749
|
+
"""Parse time metric.
|
|
750
|
+
|
|
751
|
+
A task stack's frame sequence has no length prefix on the wire -- its
|
|
752
|
+
end is only knowable by the terminating MOJO_METRIC_TIME that always
|
|
753
|
+
follows it (see mojo_event_handler__handle_task_stack_end), which is
|
|
754
|
+
why this, rather than get_task_stack, is where finalization happens.
|
|
755
|
+
"""
|
|
756
|
+
if self._running_task_stack is not None:
|
|
757
|
+
self._finalize_task_stack(value)
|
|
758
|
+
return MojoMetric(MojoMetricType.TIME, value)
|
|
759
|
+
|
|
521
760
|
return self._get_metric(MojoMetricType.TIME, value)
|
|
522
761
|
|
|
523
762
|
def get_memory_metric(self, value: int) -> MojoMetric:
|
|
@@ -630,7 +869,7 @@ class MojoStreamReader(BaseMojoStreamReader):
|
|
|
630
869
|
@handles(MojoEvents.METRIC_TIME)
|
|
631
870
|
def parse_time_metric(self) -> MojoMetric:
|
|
632
871
|
"""Parse time metric."""
|
|
633
|
-
return self.
|
|
872
|
+
return self.get_time_metric(self.read_int())
|
|
634
873
|
|
|
635
874
|
@handles(MojoEvents.METRIC_MEMORY)
|
|
636
875
|
def parse_memory_metric(self) -> MojoMetric:
|
|
@@ -667,6 +906,16 @@ class MojoStreamReader(BaseMojoStreamReader):
|
|
|
667
906
|
"""Parse a stack repeat event."""
|
|
668
907
|
return self.get_stack_repeat()
|
|
669
908
|
|
|
909
|
+
@handles(MojoEvents.TASK_WAITER)
|
|
910
|
+
def parse_task_waiter(self) -> MojoTaskWaiter:
|
|
911
|
+
"""Parse a task waiter edge."""
|
|
912
|
+
return self.get_task_waiter(self.read_int(), self.read_int())
|
|
913
|
+
|
|
914
|
+
@handles(MojoEvents.TASK_STACK)
|
|
915
|
+
def parse_task_stack(self) -> MojoTaskStack:
|
|
916
|
+
"""Parse the header of a suspended task's coroutine-chain snapshot."""
|
|
917
|
+
return self.get_task_stack(self.read_int(), self.read_int())
|
|
918
|
+
|
|
670
919
|
def parse_event(self) -> t.Optional[MojoEvent]:
|
|
671
920
|
"""Parse a single event."""
|
|
672
921
|
try:
|
|
@@ -727,7 +976,10 @@ class MojoStreamReader(BaseMojoStreamReader):
|
|
|
727
976
|
yield self._finalize_sample()
|
|
728
977
|
|
|
729
978
|
def hexdump(
|
|
730
|
-
self,
|
|
979
|
+
self,
|
|
980
|
+
start: int,
|
|
981
|
+
end: int,
|
|
982
|
+
highlight: t.Set[int] = set(), # noqa: B006
|
|
731
983
|
) -> None:
|
|
732
984
|
"""Print a hexdump of the MOJO file."""
|
|
733
985
|
self.mojo.seek(start)
|
|
@@ -818,7 +1070,7 @@ class AsyncMojoStreamReader(BaseMojoStreamReader):
|
|
|
818
1070
|
@handles(MojoEvents.METRIC_TIME)
|
|
819
1071
|
async def parse_time_metric(self) -> MojoMetric:
|
|
820
1072
|
"""Parse time metric."""
|
|
821
|
-
return await self.
|
|
1073
|
+
return self.get_time_metric(await self.read_int())
|
|
822
1074
|
|
|
823
1075
|
@handles(MojoEvents.METRIC_MEMORY)
|
|
824
1076
|
async def parse_memory_metric(self) -> MojoMetric:
|
|
@@ -857,6 +1109,16 @@ class AsyncMojoStreamReader(BaseMojoStreamReader):
|
|
|
857
1109
|
"""Parse a stack repeat event."""
|
|
858
1110
|
return self.get_stack_repeat()
|
|
859
1111
|
|
|
1112
|
+
@handles(MojoEvents.TASK_WAITER)
|
|
1113
|
+
async def parse_task_waiter(self) -> MojoTaskWaiter:
|
|
1114
|
+
"""Parse a task waiter edge."""
|
|
1115
|
+
return self.get_task_waiter(await self.read_int(), await self.read_int())
|
|
1116
|
+
|
|
1117
|
+
@handles(MojoEvents.TASK_STACK)
|
|
1118
|
+
async def parse_task_stack(self) -> MojoTaskStack:
|
|
1119
|
+
"""Parse the header of a suspended task's coroutine-chain snapshot."""
|
|
1120
|
+
return self.get_task_stack(await self.read_int(), await self.read_int())
|
|
1121
|
+
|
|
860
1122
|
async def parse_event(self) -> t.Optional[MojoEvent]:
|
|
861
1123
|
"""Parse a single event."""
|
|
862
1124
|
try:
|
|
@@ -920,7 +1182,7 @@ class AsyncMojoStreamReader(BaseMojoStreamReader):
|
|
|
920
1182
|
class BaseMojoStreamWriter(abc.ABC):
|
|
921
1183
|
"""Base class for MOJO stream writers."""
|
|
922
1184
|
|
|
923
|
-
HEADER = b"MOJ\
|
|
1185
|
+
HEADER = b"MOJ\x04"
|
|
924
1186
|
|
|
925
1187
|
def __init__(self, mojo: t.Any) -> None:
|
|
926
1188
|
self.mojo = mojo
|
|
@@ -936,6 +1198,7 @@ class BaseMojoStreamWriter(abc.ABC):
|
|
|
936
1198
|
self._gc = False
|
|
937
1199
|
|
|
938
1200
|
self._new_entries: t.List[MojoEvent] = []
|
|
1201
|
+
self._task_ids: t.Dict[int, int] = {}
|
|
939
1202
|
|
|
940
1203
|
def set_metadata(self, metadata: AustinMetadata) -> None:
|
|
941
1204
|
self._meta[metadata.name] = metadata.value
|
|
@@ -952,6 +1215,14 @@ class BaseMojoStreamWriter(abc.ABC):
|
|
|
952
1215
|
self._new_entries.append(mojo_string)
|
|
953
1216
|
return mojo_string
|
|
954
1217
|
|
|
1218
|
+
def resolve_task_id(self, task_id: int) -> int:
|
|
1219
|
+
try:
|
|
1220
|
+
return self._task_ids[task_id]
|
|
1221
|
+
except KeyError:
|
|
1222
|
+
compact_id = len(self._task_ids)
|
|
1223
|
+
self._task_ids[task_id] = compact_id
|
|
1224
|
+
return compact_id
|
|
1225
|
+
|
|
955
1226
|
def resolve_frame(self, frame: AustinFrame) -> MojoFrame:
|
|
956
1227
|
try:
|
|
957
1228
|
return self._frames[frame]
|
|
@@ -968,6 +1239,77 @@ class BaseMojoStreamWriter(abc.ABC):
|
|
|
968
1239
|
self._new_entries.append(mojo_frame)
|
|
969
1240
|
return mojo_frame
|
|
970
1241
|
|
|
1242
|
+
def _collect_tasks(
|
|
1243
|
+
self,
|
|
1244
|
+
tasks: t.Sequence[AustinTask],
|
|
1245
|
+
seen: t.Dict[int, AustinTask],
|
|
1246
|
+
edges: t.Dict[int, t.Set[int]],
|
|
1247
|
+
) -> None:
|
|
1248
|
+
"""Flatten a root-task list into its distinct tasks and waiter edges.
|
|
1249
|
+
|
|
1250
|
+
A task can legitimately appear more than once (once per waiter, if
|
|
1251
|
+
it has more than one -- see AustinTask's own docstring on
|
|
1252
|
+
``awaiting``), so this collects each task_id's own stack exactly
|
|
1253
|
+
once (``seen``, first occurrence wins) and every one of its waiters
|
|
1254
|
+
together (``edges``), rather than re-walking and re-emitting it once
|
|
1255
|
+
per occurrence.
|
|
1256
|
+
"""
|
|
1257
|
+
for task in tasks:
|
|
1258
|
+
seen.setdefault(task.task_id, task)
|
|
1259
|
+
for awaited in task.awaiting:
|
|
1260
|
+
edges.setdefault(awaited.task_id, set()).add(task.task_id)
|
|
1261
|
+
self._collect_tasks(task.awaiting, seen, edges)
|
|
1262
|
+
|
|
1263
|
+
def write_tasks(self, tasks: t.Sequence[AustinTask]) -> int:
|
|
1264
|
+
"""Write every task's own coroutine-chain snapshot, then every waiter edge.
|
|
1265
|
+
|
|
1266
|
+
See _collect_tasks for why each is written exactly once, and edges
|
|
1267
|
+
for one task_id are always written contiguously (required for
|
|
1268
|
+
MojoStreamReader.get_task_waiter's own batch-replace logic to
|
|
1269
|
+
reconstruct a multi-waiter task correctly).
|
|
1270
|
+
"""
|
|
1271
|
+
size = 0
|
|
1272
|
+
|
|
1273
|
+
seen: t.Dict[int, AustinTask] = {}
|
|
1274
|
+
edges: t.Dict[int, t.Set[int]] = {}
|
|
1275
|
+
self._collect_tasks(tasks, seen, edges)
|
|
1276
|
+
|
|
1277
|
+
for task in seen.values():
|
|
1278
|
+
name_key = self.resolve_string(task.name).key if task.name else 0
|
|
1279
|
+
|
|
1280
|
+
while self._new_entries:
|
|
1281
|
+
size += self.mojo.write(self._new_entries.pop(0).to_bytes())
|
|
1282
|
+
|
|
1283
|
+
size += self.mojo.write(
|
|
1284
|
+
MojoTaskStack(
|
|
1285
|
+
task_id=self.resolve_task_id(task.task_id), name_key=name_key
|
|
1286
|
+
).to_bytes()
|
|
1287
|
+
)
|
|
1288
|
+
|
|
1289
|
+
task_frames = [self.resolve_frame(f) for f in task.frames]
|
|
1290
|
+
|
|
1291
|
+
while self._new_entries:
|
|
1292
|
+
size += self.mojo.write(self._new_entries.pop(0).to_bytes())
|
|
1293
|
+
|
|
1294
|
+
for frame in task_frames:
|
|
1295
|
+
size += self.mojo.write(MojoFrameReference(frame).to_bytes())
|
|
1296
|
+
|
|
1297
|
+
size += self.mojo.write(
|
|
1298
|
+
MojoMetric(MojoMetricType.TIME, task.elapsed or 0).to_bytes()
|
|
1299
|
+
)
|
|
1300
|
+
|
|
1301
|
+
for task_id, waiter_ids in edges.items():
|
|
1302
|
+
compact_task_id = self.resolve_task_id(task_id)
|
|
1303
|
+
for waiter_id in waiter_ids:
|
|
1304
|
+
size += self.mojo.write(
|
|
1305
|
+
MojoTaskWaiter(
|
|
1306
|
+
task_id=compact_task_id,
|
|
1307
|
+
waiter_id=self.resolve_task_id(waiter_id),
|
|
1308
|
+
).to_bytes()
|
|
1309
|
+
)
|
|
1310
|
+
|
|
1311
|
+
return size
|
|
1312
|
+
|
|
971
1313
|
@abc.abstractmethod
|
|
972
1314
|
def write(self, event: AustinEvent) -> int: ... # noqa: E704
|
|
973
1315
|
|
|
@@ -990,16 +1332,24 @@ class MojoStreamWriter(BaseMojoStreamWriter):
|
|
|
990
1332
|
)
|
|
991
1333
|
|
|
992
1334
|
elif isinstance(event, AustinSample):
|
|
993
|
-
frames = (
|
|
994
|
-
[self.resolve_frame(f) for f in event.frames] if event.frames else []
|
|
995
|
-
)
|
|
996
|
-
|
|
997
1335
|
size += self.mojo.write(
|
|
998
1336
|
MojoStack(
|
|
999
1337
|
pid=event.pid, iid=event.iid or 0, tid=event.thread
|
|
1000
1338
|
).to_bytes()
|
|
1001
1339
|
)
|
|
1002
1340
|
|
|
1341
|
+
# Task-scan payload (waiter edges + suspended coroutine-chain
|
|
1342
|
+
# snapshots) is written before this sample's own frames -- it
|
|
1343
|
+
# rides between the enclosing MOJO_STACK header and the sample's
|
|
1344
|
+
# own frame dump on the real wire too (see austin's own
|
|
1345
|
+
# py_proc.c, _py_proc__sample_threads).
|
|
1346
|
+
if event.tasks:
|
|
1347
|
+
size += self.write_tasks(event.tasks)
|
|
1348
|
+
|
|
1349
|
+
frames = (
|
|
1350
|
+
[self.resolve_frame(f) for f in event.frames] if event.frames else []
|
|
1351
|
+
)
|
|
1352
|
+
|
|
1003
1353
|
while self._new_entries:
|
|
1004
1354
|
size += self.mojo.write(self._new_entries.pop(0).to_bytes())
|
|
1005
1355
|
|