austin-python 2.2.0__py3-none-any.whl → 2.4.0__py3-none-any.whl
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/events.py +64 -0
- austin/format/mojo.py +519 -14
- austin/stats.py +12 -4
- {austin_python-2.2.0.dist-info → austin_python-2.4.0.dist-info}/METADATA +2 -2
- {austin_python-2.2.0.dist-info → austin_python-2.4.0.dist-info}/RECORD +10 -10
- {austin_python-2.2.0.dist-info → austin_python-2.4.0.dist-info}/WHEEL +1 -1
- {austin_python-2.2.0.dist-info → austin_python-2.4.0.dist-info}/entry_points.txt +0 -0
- {austin_python-2.2.0.dist-info → austin_python-2.4.0.dist-info}/licenses/AUTHORS +0 -0
- {austin_python-2.2.0.dist-info → austin_python-2.4.0.dist-info}/licenses/COPYING +0 -0
- {austin_python-2.2.0.dist-info → austin_python-2.4.0.dist-info}/licenses/LICENSE.md +0 -0
austin/events.py
CHANGED
|
@@ -43,6 +43,63 @@ 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 last-known position: its coroutine chain if it
|
|
51
|
+
was suspended, or the frames it was running if it was running.
|
|
52
|
+
``awaiting`` are the tasks this task is itself awaiting, i.e. the tasks
|
|
53
|
+
for which this task is the direct waiter.
|
|
54
|
+
|
|
55
|
+
This is the structure of the tree only. The time tasks spend at their
|
|
56
|
+
positions is carried by AustinTaskReport (see AustinSample.task_reports)
|
|
57
|
+
and, while they are running, by the samples they are running at (see
|
|
58
|
+
AustinSample.running_task).
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
task_id: int
|
|
62
|
+
name: t.Optional[str]
|
|
63
|
+
frames: t.Tuple[AustinFrame, ...] = ()
|
|
64
|
+
awaiting: t.Tuple["AustinTask", ...] = ()
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass(frozen=True)
|
|
68
|
+
class AustinTaskReport:
|
|
69
|
+
"""A report of a task's position (3.14+ only).
|
|
70
|
+
|
|
71
|
+
Austin only reports a task's position when it has changed. ``frames`` is
|
|
72
|
+
the task's new position, and ``elapsed`` is how long the task was
|
|
73
|
+
*suspended* at its ``previous`` one, which this report replaces -- the
|
|
74
|
+
earliest point at which that duration could be known at all. Austin
|
|
75
|
+
doesn't report it: readers work it out as the time of the task's thread's
|
|
76
|
+
samples while the task held that position without running. It is await
|
|
77
|
+
time only: the time a task spends running is the time of the samples it
|
|
78
|
+
is running at (see AustinSample.running_task). Always 0 in CPU mode, where
|
|
79
|
+
a suspended task uses no CPU.
|
|
80
|
+
|
|
81
|
+
``previous`` is None on a task's first report, which has no previous
|
|
82
|
+
position (and so ``elapsed`` is 0). A report with no ``frames`` closes the
|
|
83
|
+
task: it is gone, and ``elapsed`` is the time at its last position. At the
|
|
84
|
+
end of a stream, each task still alive gets a report with ``frames`` equal
|
|
85
|
+
to ``previous``, carrying the time it has spent at its position since.
|
|
86
|
+
|
|
87
|
+
Each report is attached to exactly one sample, so summing ``elapsed``
|
|
88
|
+
over every sample's reports counts each duration once.
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
task_id: int
|
|
92
|
+
name: t.Optional[str]
|
|
93
|
+
frames: t.Tuple[AustinFrame, ...]
|
|
94
|
+
previous: t.Optional[t.Tuple[AustinFrame, ...]]
|
|
95
|
+
elapsed: MicroSeconds
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def closing(self) -> bool:
|
|
99
|
+
"""Whether this report closes the task."""
|
|
100
|
+
return not self.frames
|
|
101
|
+
|
|
102
|
+
|
|
46
103
|
@dataclass(frozen=True)
|
|
47
104
|
class AustinSample(AustinEvent):
|
|
48
105
|
"""Austin sample."""
|
|
@@ -63,6 +120,13 @@ class AustinSample(AustinEvent):
|
|
|
63
120
|
frames: t.Optional[t.Tuple[AustinFrame, ...]] = None
|
|
64
121
|
gc: t.Optional[bool] = None
|
|
65
122
|
idle: t.Optional[bool] = None
|
|
123
|
+
# The live tasks owned by this sample's thread, as of this sample.
|
|
124
|
+
tasks: t.Tuple[AustinTask, ...] = ()
|
|
125
|
+
# The task reports that came with this sample, each one exactly once.
|
|
126
|
+
task_reports: t.Tuple[AustinTaskReport, ...] = ()
|
|
127
|
+
# The task this sample's thread was running, if any. The sample's metrics
|
|
128
|
+
# then belong to that task too, at its latest position (see AustinTask).
|
|
129
|
+
running_task: t.Optional[int] = None
|
|
66
130
|
|
|
67
131
|
def key(
|
|
68
132
|
self,
|
austin/format/mojo.py
CHANGED
|
@@ -12,10 +12,17 @@ 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
|
|
16
|
+
from austin.events import AustinTaskReport
|
|
15
17
|
from austin.events import InterpreterId
|
|
16
18
|
from austin.events import ProcessId
|
|
17
19
|
from austin.events import ThreadName
|
|
18
20
|
|
|
21
|
+
# Guards against a corrupted/cyclic waiter graph in the source stream -- a
|
|
22
|
+
# real await chain never comes anywhere close to this. Mirrors austin's own
|
|
23
|
+
# WHERE_MAX_TREE_DEPTH (src/events.c).
|
|
24
|
+
MAX_TASK_TREE_DEPTH = 64
|
|
25
|
+
|
|
19
26
|
|
|
20
27
|
def to_varint(n: int) -> bytes:
|
|
21
28
|
"""Convert an integer to a variable-length integer."""
|
|
@@ -67,6 +74,9 @@ class MojoEvents:
|
|
|
67
74
|
STRING = 11
|
|
68
75
|
STRING_REF = 12
|
|
69
76
|
STACK_REPEAT = 13
|
|
77
|
+
TASK_STACK = 14
|
|
78
|
+
TASK_WAITER = 15
|
|
79
|
+
TASK_RUNNING = 16
|
|
70
80
|
|
|
71
81
|
|
|
72
82
|
class MojoEventHandler:
|
|
@@ -197,6 +207,52 @@ class MojoStackRepeat(MojoEvent):
|
|
|
197
207
|
EVENT_ID = MojoEvents.STACK_REPEAT
|
|
198
208
|
|
|
199
209
|
|
|
210
|
+
@dataclass(frozen=True, eq=True)
|
|
211
|
+
class MojoTaskWaiter(MojoEvent):
|
|
212
|
+
"""MOJO task waiter event.
|
|
213
|
+
|
|
214
|
+
One edge of a task's waiter DAG: ``task_id`` is awaited by
|
|
215
|
+
``waiter_id``.
|
|
216
|
+
"""
|
|
217
|
+
|
|
218
|
+
EVENT_ID = MojoEvents.TASK_WAITER
|
|
219
|
+
|
|
220
|
+
task_id: int
|
|
221
|
+
waiter_id: int
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
@dataclass(frozen=True, eq=True)
|
|
225
|
+
class MojoTaskRunning(MojoEvent):
|
|
226
|
+
"""MOJO task running event.
|
|
227
|
+
|
|
228
|
+
The task the enclosing sample's thread was running at that sample. The
|
|
229
|
+
sample's metrics then belong to that task too, at its latest position.
|
|
230
|
+
"""
|
|
231
|
+
|
|
232
|
+
EVENT_ID = MojoEvents.TASK_RUNNING
|
|
233
|
+
|
|
234
|
+
task_id: int
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
@dataclass(frozen=True, eq=True)
|
|
238
|
+
class MojoTaskStack(MojoEvent):
|
|
239
|
+
"""MOJO task stack event.
|
|
240
|
+
|
|
241
|
+
Introduces a task's new position, keyed by an opaque task id and its
|
|
242
|
+
(possibly cached) name key (0 if unresolved, and on a closing flush). The
|
|
243
|
+
next ``frame_count`` frame references are the task's frames; none means
|
|
244
|
+
the task is gone. The report carries no time: the time a task spends at a
|
|
245
|
+
position is the time of its thread's samples while it holds it -- see
|
|
246
|
+
AustinTaskReport.
|
|
247
|
+
"""
|
|
248
|
+
|
|
249
|
+
EVENT_ID = MojoEvents.TASK_STACK
|
|
250
|
+
|
|
251
|
+
task_id: int
|
|
252
|
+
name_key: int
|
|
253
|
+
frame_count: int
|
|
254
|
+
|
|
255
|
+
|
|
200
256
|
@dataclass(frozen=True, eq=True)
|
|
201
257
|
class MojoFrame(MojoEvent):
|
|
202
258
|
"""MOJO frame."""
|
|
@@ -264,6 +320,33 @@ class _RunningSample:
|
|
|
264
320
|
metrics: t.Dict[MojoMetricType, MojoMetric] = field(default_factory=dict)
|
|
265
321
|
gc: t.Optional[bool] = None
|
|
266
322
|
idle: t.Optional[bool] = None
|
|
323
|
+
running_task: t.Optional[int] = None
|
|
324
|
+
task_reports: t.List[AustinTaskReport] = field(default_factory=list)
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
@dataclass
|
|
328
|
+
class _RunningTaskStack:
|
|
329
|
+
task_id: int
|
|
330
|
+
name_key: int
|
|
331
|
+
frame_count: int
|
|
332
|
+
frames: t.List[MojoFrame] = field(default_factory=list)
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
@dataclass
|
|
336
|
+
class _TaskInfo:
|
|
337
|
+
"""Best-known state for one task_id.
|
|
338
|
+
|
|
339
|
+
Built up from MOJO_TASK_STACK events as they arrive. ``owner`` is the
|
|
340
|
+
(pid, thread) of whichever sample's wire bracket this task's most recent
|
|
341
|
+
MOJO_TASK_STACK fell within -- see BaseMojoStreamReader's task-stack
|
|
342
|
+
handling for why that bracket is the only ownership signal that exists.
|
|
343
|
+
"""
|
|
344
|
+
|
|
345
|
+
name: t.Optional[str]
|
|
346
|
+
owner: t.Tuple[ProcessId, ThreadName]
|
|
347
|
+
frames: t.Tuple[AustinFrame, ...] = ()
|
|
348
|
+
# Time spent at ``frames`` so far, i.e. since the task's last report.
|
|
349
|
+
pending: int = 0
|
|
267
350
|
|
|
268
351
|
|
|
269
352
|
def int_reader() -> t.Generator[t.Optional[int], bytes, int]:
|
|
@@ -340,6 +423,26 @@ class BaseMojoStreamReader(AustinEventIterator):
|
|
|
340
423
|
# last fully-finalised sample for that thread.
|
|
341
424
|
self._prev_frames: t.Dict[t.Tuple[int, str], t.List[MojoFrame]] = {}
|
|
342
425
|
|
|
426
|
+
# Suspended-task coroutine-chain state (3.14+ asyncio). A task stack's
|
|
427
|
+
# frames/terminating time metric are ordinary MOJO_FRAME/MOJO_FRAME_REF/
|
|
428
|
+
# MOJO_METRIC_TIME events on the wire, indistinguishable by event ID
|
|
429
|
+
# alone from those of the enclosing regular stack sample -- so while
|
|
430
|
+
# this is not None, get_frame_ref/get_time_metric route to it instead
|
|
431
|
+
# of the running sample.
|
|
432
|
+
self._running_task_stack: t.Optional[_RunningTaskStack] = None
|
|
433
|
+
|
|
434
|
+
# Best-known state per task_id, keyed by remote TaskObj address.
|
|
435
|
+
self._task_info: t.Dict[int, _TaskInfo] = {}
|
|
436
|
+
|
|
437
|
+
# Best-known waiter DAG edges: (task_id, waiter_id) pairs, meaning
|
|
438
|
+
# task_id is awaited by waiter_id. The wire re-emits a task's
|
|
439
|
+
# *complete* current waiter set whenever it changes (never a diff --
|
|
440
|
+
# see py_asyncio.c's fingerprint comment), so a fresh batch for a
|
|
441
|
+
# task_id replaces its previous edges; batch boundaries are inferred
|
|
442
|
+
# from consecutive same-task_id events (see get_task_waiter).
|
|
443
|
+
self._task_edges: t.Set[t.Tuple[int, int]] = set()
|
|
444
|
+
self._last_waiter_task_id: t.Optional[int] = None
|
|
445
|
+
|
|
343
446
|
# Austin events
|
|
344
447
|
self.metadata: t.Dict[str, str] = {}
|
|
345
448
|
self.samples: t.List[AustinSample] = []
|
|
@@ -384,10 +487,61 @@ class BaseMojoStreamReader(AustinEventIterator):
|
|
|
384
487
|
def gc(self) -> t.Optional[str]:
|
|
385
488
|
return self.metadata.get("gc")
|
|
386
489
|
|
|
387
|
-
def
|
|
388
|
-
"""
|
|
490
|
+
def _accrue_task_time(self, sample: _RunningSample) -> None:
|
|
491
|
+
"""Add the time of a sample to the tasks its thread holds.
|
|
492
|
+
|
|
493
|
+
A task's time at a position is the time of its thread's samples while
|
|
494
|
+
it holds it: the running task's go to it as running (see
|
|
495
|
+
AustinSample.running_task), every other task's as time at its current
|
|
496
|
+
position, which its next report carries. In CPU mode a task that isn't
|
|
497
|
+
running isn't using the CPU, so it gets nothing.
|
|
498
|
+
"""
|
|
499
|
+
if self.mode == "cpu":
|
|
500
|
+
return
|
|
501
|
+
|
|
502
|
+
time_metric = sample.metrics.get(MojoMetricType.TIME)
|
|
503
|
+
if time_metric is None or not time_metric.value:
|
|
504
|
+
return
|
|
505
|
+
|
|
506
|
+
owner = (sample.pid, sample.thread)
|
|
507
|
+
for task_id, info in self._task_info.items():
|
|
508
|
+
if info.owner == owner and task_id != sample.running_task:
|
|
509
|
+
info.pending += time_metric.value
|
|
510
|
+
|
|
511
|
+
def _end_task_reports(self) -> t.List[AustinTaskReport]:
|
|
512
|
+
"""Report the time the tasks still alive have spent at their position.
|
|
513
|
+
|
|
514
|
+
Nothing else would: a task's time at a position is only reported when
|
|
515
|
+
the task moves on, or goes. These reports leave the position unchanged
|
|
516
|
+
(``frames`` and ``previous`` are the same).
|
|
517
|
+
"""
|
|
518
|
+
reports = [
|
|
519
|
+
AustinTaskReport(
|
|
520
|
+
task_id=task_id,
|
|
521
|
+
name=info.name,
|
|
522
|
+
frames=info.frames,
|
|
523
|
+
previous=info.frames,
|
|
524
|
+
elapsed=info.pending,
|
|
525
|
+
)
|
|
526
|
+
for task_id, info in self._task_info.items()
|
|
527
|
+
if info.pending
|
|
528
|
+
]
|
|
529
|
+
for info in self._task_info.values():
|
|
530
|
+
info.pending = 0
|
|
531
|
+
return reports
|
|
532
|
+
|
|
533
|
+
def _finalize_sample(self, last: bool = False) -> AustinSample:
|
|
534
|
+
"""Finalize the current sample.
|
|
535
|
+
|
|
536
|
+
The last sample of the stream also carries the end-of-stream task
|
|
537
|
+
reports (see _end_task_reports).
|
|
538
|
+
"""
|
|
389
539
|
assert self._running_sample is not None, self._running_sample
|
|
390
540
|
|
|
541
|
+
self._accrue_task_time(self._running_sample)
|
|
542
|
+
if last:
|
|
543
|
+
self._running_sample.task_reports.extend(self._end_task_reports())
|
|
544
|
+
|
|
391
545
|
self.samples.append(
|
|
392
546
|
sample := AustinSample(
|
|
393
547
|
pid=self._running_sample.pid,
|
|
@@ -416,6 +570,11 @@ class BaseMojoStreamReader(AustinEventIterator):
|
|
|
416
570
|
),
|
|
417
571
|
gc=self._running_sample.gc,
|
|
418
572
|
idle=self._running_sample.idle,
|
|
573
|
+
running_task=self._running_sample.running_task,
|
|
574
|
+
task_reports=tuple(self._running_sample.task_reports),
|
|
575
|
+
tasks=self.get_tasks(
|
|
576
|
+
self._running_sample.pid, self._running_sample.thread
|
|
577
|
+
),
|
|
419
578
|
)
|
|
420
579
|
)
|
|
421
580
|
|
|
@@ -459,6 +618,10 @@ class BaseMojoStreamReader(AustinEventIterator):
|
|
|
459
618
|
if self._running_sample is not None:
|
|
460
619
|
self._finalize_sample()
|
|
461
620
|
|
|
621
|
+
# A task's waiter set is a run of consecutive MOJO_TASK_WAITER events,
|
|
622
|
+
# which never spans a sample boundary.
|
|
623
|
+
self._last_waiter_task_id = None
|
|
624
|
+
|
|
462
625
|
self._running_sample = _RunningSample(
|
|
463
626
|
pid=pid,
|
|
464
627
|
iid=iid,
|
|
@@ -499,11 +662,184 @@ class BaseMojoStreamReader(AustinEventIterator):
|
|
|
499
662
|
"""Parse a frame reference."""
|
|
500
663
|
frame = self._frame_map[self.ref(ref)]
|
|
501
664
|
|
|
502
|
-
|
|
503
|
-
|
|
665
|
+
if self._running_task_stack is not None:
|
|
666
|
+
self._running_task_stack.frames.append(frame)
|
|
667
|
+
if (
|
|
668
|
+
len(self._running_task_stack.frames)
|
|
669
|
+
>= self._running_task_stack.frame_count
|
|
670
|
+
):
|
|
671
|
+
self._finalize_task_stack()
|
|
672
|
+
else:
|
|
673
|
+
assert self._running_sample is not None, self._running_sample
|
|
674
|
+
self._running_sample.frames.append(frame)
|
|
504
675
|
|
|
505
676
|
return MojoFrameReference(frame)
|
|
506
677
|
|
|
678
|
+
def get_task_waiter(self, task_id: int, waiter_id: int) -> MojoTaskWaiter:
|
|
679
|
+
"""Parse a task waiter edge."""
|
|
680
|
+
if self._last_waiter_task_id != task_id:
|
|
681
|
+
# Starting a fresh batch for this task_id: the wire re-emits the
|
|
682
|
+
# complete current waiter set whenever it changes, so drop
|
|
683
|
+
# whatever we knew about this task_id's waiters before.
|
|
684
|
+
self._task_edges = {e for e in self._task_edges if e[0] != task_id}
|
|
685
|
+
self._last_waiter_task_id = task_id
|
|
686
|
+
|
|
687
|
+
# A waiter of 0 is no task: the task's waiter set is now empty.
|
|
688
|
+
if waiter_id:
|
|
689
|
+
self._task_edges.add((task_id, waiter_id))
|
|
690
|
+
|
|
691
|
+
return MojoTaskWaiter(task_id, waiter_id)
|
|
692
|
+
|
|
693
|
+
def get_task_running(self, task_id: int) -> MojoTaskRunning:
|
|
694
|
+
"""Parse the task the current sample's thread was running."""
|
|
695
|
+
assert self._running_sample is not None, self._running_sample
|
|
696
|
+
self._running_sample.running_task = task_id
|
|
697
|
+
|
|
698
|
+
return MojoTaskRunning(task_id)
|
|
699
|
+
|
|
700
|
+
def get_task_stack(
|
|
701
|
+
self, task_id: int, name_key: int, frame_count: int
|
|
702
|
+
) -> MojoTaskStack:
|
|
703
|
+
"""Parse the header of a task report.
|
|
704
|
+
|
|
705
|
+
Its frames arrive as the next ``frame_count`` frame references -- see
|
|
706
|
+
get_frame_ref. A report with none closes the task right away.
|
|
707
|
+
"""
|
|
708
|
+
self._running_task_stack = _RunningTaskStack(task_id, name_key, frame_count)
|
|
709
|
+
self._last_waiter_task_id = None
|
|
710
|
+
|
|
711
|
+
if not frame_count:
|
|
712
|
+
self._finalize_task_stack()
|
|
713
|
+
|
|
714
|
+
return MojoTaskStack(task_id, name_key, frame_count)
|
|
715
|
+
|
|
716
|
+
def _finalize_task_stack(self) -> None:
|
|
717
|
+
"""Finalize the running task stack.
|
|
718
|
+
|
|
719
|
+
Triggered by its last frame reference (see get_frame_ref), or right
|
|
720
|
+
away by a header with no frames (see get_task_stack). The report's
|
|
721
|
+
frames are the task's new position, and its elapsed time is the time
|
|
722
|
+
the task spent at its previous one -- see AustinTaskReport and
|
|
723
|
+
_accrue_task_time. The report is attached to the sample
|
|
724
|
+
currently being assembled, and the task's owner is recorded as that
|
|
725
|
+
sample's thread: this event only ever arrives bracketed inside that
|
|
726
|
+
sample's own MOJO_STACK...next-MOJO_STACK window, so there's nothing
|
|
727
|
+
else it could belong to.
|
|
728
|
+
|
|
729
|
+
An empty ``ts.frames`` is a closing flush -- the task is gone
|
|
730
|
+
(completed, or otherwise), and this only carries its final time. It
|
|
731
|
+
has no name of its own, nor a meaningful owner (by the time eviction
|
|
732
|
+
runs, every thread for this cycle has already had its turn, so
|
|
733
|
+
"whichever sample is currently being assembled" is arbitrary here).
|
|
734
|
+
The task is forgotten, along with every waiter edge it is part of.
|
|
735
|
+
"""
|
|
736
|
+
ts = self._running_task_stack
|
|
737
|
+
assert ts is not None, ts
|
|
738
|
+
assert self._running_sample is not None, self._running_sample
|
|
739
|
+
|
|
740
|
+
self._running_task_stack = None
|
|
741
|
+
|
|
742
|
+
existing = self._task_info.get(ts.task_id)
|
|
743
|
+
previous = existing.frames if existing is not None else None
|
|
744
|
+
elapsed = existing.pending if existing is not None else 0
|
|
745
|
+
|
|
746
|
+
if ts.name_key:
|
|
747
|
+
name: t.Optional[str] = self._lookup_string(ts.name_key).value
|
|
748
|
+
else:
|
|
749
|
+
name = existing.name if existing is not None else None
|
|
750
|
+
|
|
751
|
+
frames = tuple(
|
|
752
|
+
AustinFrame(
|
|
753
|
+
filename=mf.filename.value,
|
|
754
|
+
function=mf.scope.value,
|
|
755
|
+
line=mf.line,
|
|
756
|
+
line_end=mf.line_end,
|
|
757
|
+
column=mf.column,
|
|
758
|
+
column_end=mf.column_end,
|
|
759
|
+
)
|
|
760
|
+
for mf in ts.frames
|
|
761
|
+
)
|
|
762
|
+
|
|
763
|
+
self._running_sample.task_reports.append(
|
|
764
|
+
AustinTaskReport(
|
|
765
|
+
task_id=ts.task_id,
|
|
766
|
+
name=name,
|
|
767
|
+
frames=frames,
|
|
768
|
+
previous=previous,
|
|
769
|
+
elapsed=elapsed,
|
|
770
|
+
)
|
|
771
|
+
)
|
|
772
|
+
|
|
773
|
+
if not frames:
|
|
774
|
+
self._task_info.pop(ts.task_id, None)
|
|
775
|
+
self._task_edges = {e for e in self._task_edges if ts.task_id not in e}
|
|
776
|
+
return
|
|
777
|
+
|
|
778
|
+
self._task_info[ts.task_id] = _TaskInfo(
|
|
779
|
+
name=name,
|
|
780
|
+
owner=(self._running_sample.pid, self._running_sample.thread),
|
|
781
|
+
frames=frames,
|
|
782
|
+
)
|
|
783
|
+
|
|
784
|
+
def get_tasks(self, pid: ProcessId, thread: ThreadName) -> t.Tuple[AustinTask, ...]:
|
|
785
|
+
"""Return the current best-known root tasks owned by (pid, thread).
|
|
786
|
+
|
|
787
|
+
Every root task it owns (nobody awaits it), and everything it
|
|
788
|
+
transitively awaits, as one AustinTask per root -- a thread can own
|
|
789
|
+
more than one at once (e.g. several siblings under one gather()).
|
|
790
|
+
|
|
791
|
+
Mirrors austin's own where_event_handler__render_tree: only live
|
|
792
|
+
tasks that have been reported at least once appear. A waiter we know
|
|
793
|
+
nothing about (never reported, or already gone) doesn't count as a
|
|
794
|
+
parent, so the tasks it awaited still show up, as roots. Like that
|
|
795
|
+
C-side counterpart, this is a no-op (returns empty) when (pid,
|
|
796
|
+
thread) owns no tasks, so callers can call it unconditionally.
|
|
797
|
+
"""
|
|
798
|
+
|
|
799
|
+
def has_parent(task_id: int) -> bool:
|
|
800
|
+
return any(
|
|
801
|
+
t_id == task_id and w_id in self._task_info
|
|
802
|
+
for t_id, w_id in self._task_edges
|
|
803
|
+
)
|
|
804
|
+
|
|
805
|
+
def build(task_id: int, depth: int) -> AustinTask:
|
|
806
|
+
info = self._task_info[task_id]
|
|
807
|
+
awaiting = (
|
|
808
|
+
tuple(
|
|
809
|
+
build(awaited_id, depth + 1)
|
|
810
|
+
for awaited_id, waiter_id in self._task_edges
|
|
811
|
+
if waiter_id == task_id and awaited_id in self._task_info
|
|
812
|
+
)
|
|
813
|
+
if depth < MAX_TASK_TREE_DEPTH
|
|
814
|
+
else ()
|
|
815
|
+
)
|
|
816
|
+
return AustinTask(
|
|
817
|
+
task_id=task_id,
|
|
818
|
+
name=info.name,
|
|
819
|
+
frames=info.frames,
|
|
820
|
+
awaiting=awaiting,
|
|
821
|
+
)
|
|
822
|
+
|
|
823
|
+
owner = (pid, thread)
|
|
824
|
+
roots = [
|
|
825
|
+
task_id
|
|
826
|
+
for task_id, info in self._task_info.items()
|
|
827
|
+
if info.owner == owner and not has_parent(task_id)
|
|
828
|
+
]
|
|
829
|
+
|
|
830
|
+
if not roots:
|
|
831
|
+
# Best-effort fallback, matching austin's own render_tree: if
|
|
832
|
+
# every one of this owner's tasks appears to have a parent (a
|
|
833
|
+
# cyclic/bogus edge), there's no true root -- show each as its
|
|
834
|
+
# own top-level tree rather than silently showing nothing.
|
|
835
|
+
roots = [
|
|
836
|
+
task_id
|
|
837
|
+
for task_id, info in self._task_info.items()
|
|
838
|
+
if info.owner == owner
|
|
839
|
+
]
|
|
840
|
+
|
|
841
|
+
return tuple(build(task_id, 0) for task_id in roots)
|
|
842
|
+
|
|
507
843
|
def get_kernel_frame(self, name: str) -> MojoKernelFrame:
|
|
508
844
|
"""Parse kernel frame."""
|
|
509
845
|
return MojoKernelFrame(name)
|
|
@@ -630,7 +966,7 @@ class MojoStreamReader(BaseMojoStreamReader):
|
|
|
630
966
|
@handles(MojoEvents.METRIC_TIME)
|
|
631
967
|
def parse_time_metric(self) -> MojoMetric:
|
|
632
968
|
"""Parse time metric."""
|
|
633
|
-
return self.
|
|
969
|
+
return self.get_time_metric(self.read_int())
|
|
634
970
|
|
|
635
971
|
@handles(MojoEvents.METRIC_MEMORY)
|
|
636
972
|
def parse_memory_metric(self) -> MojoMetric:
|
|
@@ -667,6 +1003,21 @@ class MojoStreamReader(BaseMojoStreamReader):
|
|
|
667
1003
|
"""Parse a stack repeat event."""
|
|
668
1004
|
return self.get_stack_repeat()
|
|
669
1005
|
|
|
1006
|
+
@handles(MojoEvents.TASK_WAITER)
|
|
1007
|
+
def parse_task_waiter(self) -> MojoTaskWaiter:
|
|
1008
|
+
"""Parse a task waiter edge."""
|
|
1009
|
+
return self.get_task_waiter(self.read_int(), self.read_int())
|
|
1010
|
+
|
|
1011
|
+
@handles(MojoEvents.TASK_RUNNING)
|
|
1012
|
+
def parse_task_running(self) -> MojoTaskRunning:
|
|
1013
|
+
"""Parse the running task of a sample."""
|
|
1014
|
+
return self.get_task_running(self.read_int())
|
|
1015
|
+
|
|
1016
|
+
@handles(MojoEvents.TASK_STACK)
|
|
1017
|
+
def parse_task_stack(self) -> MojoTaskStack:
|
|
1018
|
+
"""Parse the header of a task report."""
|
|
1019
|
+
return self.get_task_stack(self.read_int(), self.read_int(), self.read_int())
|
|
1020
|
+
|
|
670
1021
|
def parse_event(self) -> t.Optional[MojoEvent]:
|
|
671
1022
|
"""Parse a single event."""
|
|
672
1023
|
try:
|
|
@@ -724,10 +1075,13 @@ class MojoStreamReader(BaseMojoStreamReader):
|
|
|
724
1075
|
if self.samples:
|
|
725
1076
|
yield self.samples[-1]
|
|
726
1077
|
if self._running_sample is not None:
|
|
727
|
-
yield self._finalize_sample()
|
|
1078
|
+
yield self._finalize_sample(last=True)
|
|
728
1079
|
|
|
729
1080
|
def hexdump(
|
|
730
|
-
self,
|
|
1081
|
+
self,
|
|
1082
|
+
start: int,
|
|
1083
|
+
end: int,
|
|
1084
|
+
highlight: t.Set[int] = set(), # noqa: B006
|
|
731
1085
|
) -> None:
|
|
732
1086
|
"""Print a hexdump of the MOJO file."""
|
|
733
1087
|
self.mojo.seek(start)
|
|
@@ -818,7 +1172,7 @@ class AsyncMojoStreamReader(BaseMojoStreamReader):
|
|
|
818
1172
|
@handles(MojoEvents.METRIC_TIME)
|
|
819
1173
|
async def parse_time_metric(self) -> MojoMetric:
|
|
820
1174
|
"""Parse time metric."""
|
|
821
|
-
return await self.
|
|
1175
|
+
return self.get_time_metric(await self.read_int())
|
|
822
1176
|
|
|
823
1177
|
@handles(MojoEvents.METRIC_MEMORY)
|
|
824
1178
|
async def parse_memory_metric(self) -> MojoMetric:
|
|
@@ -857,6 +1211,23 @@ class AsyncMojoStreamReader(BaseMojoStreamReader):
|
|
|
857
1211
|
"""Parse a stack repeat event."""
|
|
858
1212
|
return self.get_stack_repeat()
|
|
859
1213
|
|
|
1214
|
+
@handles(MojoEvents.TASK_WAITER)
|
|
1215
|
+
async def parse_task_waiter(self) -> MojoTaskWaiter:
|
|
1216
|
+
"""Parse a task waiter edge."""
|
|
1217
|
+
return self.get_task_waiter(await self.read_int(), await self.read_int())
|
|
1218
|
+
|
|
1219
|
+
@handles(MojoEvents.TASK_RUNNING)
|
|
1220
|
+
async def parse_task_running(self) -> MojoTaskRunning:
|
|
1221
|
+
"""Parse the running task of a sample."""
|
|
1222
|
+
return self.get_task_running(await self.read_int())
|
|
1223
|
+
|
|
1224
|
+
@handles(MojoEvents.TASK_STACK)
|
|
1225
|
+
async def parse_task_stack(self) -> MojoTaskStack:
|
|
1226
|
+
"""Parse the header of a task report."""
|
|
1227
|
+
return self.get_task_stack(
|
|
1228
|
+
await self.read_int(), await self.read_int(), await self.read_int()
|
|
1229
|
+
)
|
|
1230
|
+
|
|
860
1231
|
async def parse_event(self) -> t.Optional[MojoEvent]:
|
|
861
1232
|
"""Parse a single event."""
|
|
862
1233
|
try:
|
|
@@ -914,13 +1285,13 @@ class AsyncMojoStreamReader(BaseMojoStreamReader):
|
|
|
914
1285
|
if self.samples:
|
|
915
1286
|
yield self.samples[-1]
|
|
916
1287
|
if self._running_sample is not None:
|
|
917
|
-
yield self._finalize_sample()
|
|
1288
|
+
yield self._finalize_sample(last=True)
|
|
918
1289
|
|
|
919
1290
|
|
|
920
1291
|
class BaseMojoStreamWriter(abc.ABC):
|
|
921
1292
|
"""Base class for MOJO stream writers."""
|
|
922
1293
|
|
|
923
|
-
HEADER = b"MOJ\
|
|
1294
|
+
HEADER = b"MOJ\x04"
|
|
924
1295
|
|
|
925
1296
|
def __init__(self, mojo: t.Any) -> None:
|
|
926
1297
|
self.mojo = mojo
|
|
@@ -936,6 +1307,11 @@ class BaseMojoStreamWriter(abc.ABC):
|
|
|
936
1307
|
self._gc = False
|
|
937
1308
|
|
|
938
1309
|
self._new_entries: t.List[MojoEvent] = []
|
|
1310
|
+
self._task_ids: t.Dict[int, int] = {}
|
|
1311
|
+
# The waiter set last written for each task, and the tasks written so
|
|
1312
|
+
# far (see write_tasks).
|
|
1313
|
+
self._written_waiters: t.Dict[int, t.FrozenSet[int]] = {}
|
|
1314
|
+
self._written_tasks: t.Set[int] = set()
|
|
939
1315
|
|
|
940
1316
|
def set_metadata(self, metadata: AustinMetadata) -> None:
|
|
941
1317
|
self._meta[metadata.name] = metadata.value
|
|
@@ -952,6 +1328,15 @@ class BaseMojoStreamWriter(abc.ABC):
|
|
|
952
1328
|
self._new_entries.append(mojo_string)
|
|
953
1329
|
return mojo_string
|
|
954
1330
|
|
|
1331
|
+
def resolve_task_id(self, task_id: int) -> int:
|
|
1332
|
+
try:
|
|
1333
|
+
return self._task_ids[task_id]
|
|
1334
|
+
except KeyError:
|
|
1335
|
+
# 0 is reserved: it stands for no task (e.g. an unknown waiter).
|
|
1336
|
+
compact_id = len(self._task_ids) + 1
|
|
1337
|
+
self._task_ids[task_id] = compact_id
|
|
1338
|
+
return compact_id
|
|
1339
|
+
|
|
955
1340
|
def resolve_frame(self, frame: AustinFrame) -> MojoFrame:
|
|
956
1341
|
try:
|
|
957
1342
|
return self._frames[frame]
|
|
@@ -968,6 +1353,114 @@ class BaseMojoStreamWriter(abc.ABC):
|
|
|
968
1353
|
self._new_entries.append(mojo_frame)
|
|
969
1354
|
return mojo_frame
|
|
970
1355
|
|
|
1356
|
+
def _collect_tasks(
|
|
1357
|
+
self,
|
|
1358
|
+
tasks: t.Sequence[AustinTask],
|
|
1359
|
+
seen: t.Dict[int, AustinTask],
|
|
1360
|
+
edges: t.Dict[int, t.Set[int]],
|
|
1361
|
+
) -> None:
|
|
1362
|
+
"""Flatten a root-task list into its distinct tasks and waiter edges.
|
|
1363
|
+
|
|
1364
|
+
A task can legitimately appear more than once (once per waiter, if
|
|
1365
|
+
it has more than one -- see AustinTask's own docstring on
|
|
1366
|
+
``awaiting``), so this collects each task_id's own stack exactly
|
|
1367
|
+
once (``seen``, first occurrence wins) and every one of its waiters
|
|
1368
|
+
together (``edges``), rather than re-walking and re-emitting it once
|
|
1369
|
+
per occurrence.
|
|
1370
|
+
"""
|
|
1371
|
+
for task in tasks:
|
|
1372
|
+
seen.setdefault(task.task_id, task)
|
|
1373
|
+
for awaited in task.awaiting:
|
|
1374
|
+
edges.setdefault(awaited.task_id, set()).add(task.task_id)
|
|
1375
|
+
self._collect_tasks(task.awaiting, seen, edges)
|
|
1376
|
+
|
|
1377
|
+
def write_tasks(self, sample: AustinSample) -> int:
|
|
1378
|
+
"""Write a sample's task reports, then the waiter edges that changed.
|
|
1379
|
+
|
|
1380
|
+
Each report is written once, as it was read, without its elapsed time,
|
|
1381
|
+
which readers work out from the samples. Reports that leave a task
|
|
1382
|
+
where it was, like end-of-stream reports, aren't written: readers
|
|
1383
|
+
produce those themselves. Waiter edges are written as the complete
|
|
1384
|
+
waiter set of every task whose set differs from the last one written
|
|
1385
|
+
for it, contiguously (required for MojoStreamReader.get_task_waiter's
|
|
1386
|
+
own batch-replace logic to reconstruct a multi-waiter task correctly),
|
|
1387
|
+
and an emptied set as a single edge to the reserved 0. See
|
|
1388
|
+
_collect_tasks for why each task is considered exactly once.
|
|
1389
|
+
|
|
1390
|
+
A task in the sample's tree that the stream hasn't introduced yet
|
|
1391
|
+
(e.g. a sample built by hand, with no reports) is introduced with a
|
|
1392
|
+
report of its own, carrying no time: readers only learn of tasks from
|
|
1393
|
+
reports.
|
|
1394
|
+
"""
|
|
1395
|
+
size = 0
|
|
1396
|
+
|
|
1397
|
+
seen: t.Dict[int, AustinTask] = {}
|
|
1398
|
+
edges: t.Dict[int, t.Set[int]] = {}
|
|
1399
|
+
self._collect_tasks(sample.tasks, seen, edges)
|
|
1400
|
+
|
|
1401
|
+
reported = {r.task_id for r in sample.task_reports}
|
|
1402
|
+
introductions = tuple(
|
|
1403
|
+
AustinTaskReport(
|
|
1404
|
+
task_id=task.task_id,
|
|
1405
|
+
name=task.name,
|
|
1406
|
+
frames=task.frames,
|
|
1407
|
+
previous=None,
|
|
1408
|
+
elapsed=0,
|
|
1409
|
+
)
|
|
1410
|
+
for task in seen.values()
|
|
1411
|
+
if task.task_id not in self._written_tasks
|
|
1412
|
+
and task.task_id not in reported
|
|
1413
|
+
and task.frames
|
|
1414
|
+
)
|
|
1415
|
+
|
|
1416
|
+
for report in sample.task_reports + introductions:
|
|
1417
|
+
if report.frames and report.frames == report.previous:
|
|
1418
|
+
continue
|
|
1419
|
+
|
|
1420
|
+
name_key = (
|
|
1421
|
+
self.resolve_string(report.name).key
|
|
1422
|
+
if report.name and report.frames
|
|
1423
|
+
else 0
|
|
1424
|
+
)
|
|
1425
|
+
task_frames = [self.resolve_frame(f) for f in report.frames]
|
|
1426
|
+
|
|
1427
|
+
while self._new_entries:
|
|
1428
|
+
size += self.mojo.write(self._new_entries.pop(0).to_bytes())
|
|
1429
|
+
|
|
1430
|
+
size += self.mojo.write(
|
|
1431
|
+
MojoTaskStack(
|
|
1432
|
+
task_id=self.resolve_task_id(report.task_id),
|
|
1433
|
+
name_key=name_key,
|
|
1434
|
+
frame_count=len(task_frames),
|
|
1435
|
+
).to_bytes()
|
|
1436
|
+
)
|
|
1437
|
+
|
|
1438
|
+
for frame in task_frames:
|
|
1439
|
+
size += self.mojo.write(MojoFrameReference(frame).to_bytes())
|
|
1440
|
+
|
|
1441
|
+
if report.closing:
|
|
1442
|
+
self._written_waiters.pop(report.task_id, None)
|
|
1443
|
+
self._written_tasks.discard(report.task_id)
|
|
1444
|
+
else:
|
|
1445
|
+
self._written_tasks.add(report.task_id)
|
|
1446
|
+
|
|
1447
|
+
for task_id in seen:
|
|
1448
|
+
waiter_ids = frozenset(edges.get(task_id, ()))
|
|
1449
|
+
if waiter_ids == self._written_waiters.get(task_id, frozenset()):
|
|
1450
|
+
continue
|
|
1451
|
+
self._written_waiters[task_id] = waiter_ids
|
|
1452
|
+
|
|
1453
|
+
compact_task_id = self.resolve_task_id(task_id)
|
|
1454
|
+
for waiter_id in waiter_ids or (0,):
|
|
1455
|
+
size += self.mojo.write(
|
|
1456
|
+
MojoTaskWaiter(
|
|
1457
|
+
task_id=compact_task_id,
|
|
1458
|
+
waiter_id=self.resolve_task_id(waiter_id) if waiter_id else 0,
|
|
1459
|
+
).to_bytes()
|
|
1460
|
+
)
|
|
1461
|
+
|
|
1462
|
+
return size
|
|
1463
|
+
|
|
971
1464
|
@abc.abstractmethod
|
|
972
1465
|
def write(self, event: AustinEvent) -> int: ... # noqa: E704
|
|
973
1466
|
|
|
@@ -990,16 +1483,28 @@ class MojoStreamWriter(BaseMojoStreamWriter):
|
|
|
990
1483
|
)
|
|
991
1484
|
|
|
992
1485
|
elif isinstance(event, AustinSample):
|
|
993
|
-
frames = (
|
|
994
|
-
[self.resolve_frame(f) for f in event.frames] if event.frames else []
|
|
995
|
-
)
|
|
996
|
-
|
|
997
1486
|
size += self.mojo.write(
|
|
998
1487
|
MojoStack(
|
|
999
1488
|
pid=event.pid, iid=event.iid or 0, tid=event.thread
|
|
1000
1489
|
).to_bytes()
|
|
1001
1490
|
)
|
|
1002
1491
|
|
|
1492
|
+
# Task-scan payload (task reports, then waiter edges) is written
|
|
1493
|
+
# before this sample's own frames -- it
|
|
1494
|
+
# rides between the enclosing MOJO_STACK header and the sample's
|
|
1495
|
+
# own frame dump on the real wire too (see austin's own
|
|
1496
|
+
# py_proc.c, _py_proc__sample_threads).
|
|
1497
|
+
size += self.write_tasks(event)
|
|
1498
|
+
|
|
1499
|
+
if event.running_task is not None:
|
|
1500
|
+
size += self.mojo.write(
|
|
1501
|
+
MojoTaskRunning(self.resolve_task_id(event.running_task)).to_bytes()
|
|
1502
|
+
)
|
|
1503
|
+
|
|
1504
|
+
frames = (
|
|
1505
|
+
[self.resolve_frame(f) for f in event.frames] if event.frames else []
|
|
1506
|
+
)
|
|
1507
|
+
|
|
1003
1508
|
while self._new_entries:
|
|
1004
1509
|
size += self.mojo.write(self._new_entries.pop(0).to_bytes())
|
|
1005
1510
|
|
austin/stats.py
CHANGED
|
@@ -67,7 +67,8 @@ class HierarchicalStats:
|
|
|
67
67
|
leaf is given by those instances with an empty ``children`` attribute.
|
|
68
68
|
|
|
69
69
|
The ``label`` attribute is used for indexing reasons and therefore should
|
|
70
|
-
be of a hashable type.
|
|
70
|
+
be of a hashable type. The ``samples`` attribute counts the samples that
|
|
71
|
+
contributed to the ``total`` of the node.
|
|
71
72
|
|
|
72
73
|
This class overrides the default ``add`` operator so that one can perform
|
|
73
74
|
operations like ``stats1 + stats2``. Note, however, that instances of this
|
|
@@ -79,6 +80,7 @@ class HierarchicalStats:
|
|
|
79
80
|
own: int
|
|
80
81
|
total: int
|
|
81
82
|
children: Dict[Any, "HierarchicalStats"] = field(default_factory=dict)
|
|
83
|
+
samples: int = 0
|
|
82
84
|
|
|
83
85
|
def __lshift__(self, other: "HierarchicalStats") -> "HierarchicalStats":
|
|
84
86
|
"""Merge the RHS into the LHS."""
|
|
@@ -87,6 +89,7 @@ class HierarchicalStats:
|
|
|
87
89
|
|
|
88
90
|
self.own += other.own
|
|
89
91
|
self.total += other.total
|
|
92
|
+
self.samples += other.samples
|
|
90
93
|
|
|
91
94
|
for frame, child in other.children.items():
|
|
92
95
|
try:
|
|
@@ -270,8 +273,11 @@ class AustinStats:
|
|
|
270
273
|
by using :func:`Sample.parse` on a sample string passed by Austin to
|
|
271
274
|
the sample callback.
|
|
272
275
|
"""
|
|
273
|
-
if self.stats_type
|
|
276
|
+
if self.stats_type is AustinStatsType.WALL:
|
|
274
277
|
metric = sample.metrics.time
|
|
278
|
+
elif self.stats_type is AustinStatsType.CPU:
|
|
279
|
+
# In full mode, idle samples contribute wall time but no CPU time
|
|
280
|
+
metric = None if sample.idle else sample.metrics.time
|
|
275
281
|
elif (
|
|
276
282
|
self.stats_type is AustinStatsType.MEMORY_ALLOC
|
|
277
283
|
and sample.metrics.memory is not None
|
|
@@ -292,13 +298,15 @@ class AustinStats:
|
|
|
292
298
|
|
|
293
299
|
pid = sample.pid
|
|
294
300
|
thread_info = ThreadInfo(sample.thread, sample.iid or 0)
|
|
295
|
-
thread_stats = ThreadStats(thread_info, own=0, total=metric)
|
|
301
|
+
thread_stats = ThreadStats(thread_info, own=0, total=metric, samples=1)
|
|
296
302
|
|
|
297
303
|
# Convert the list of frames into a nested FrameStats instance
|
|
298
304
|
stats: HierarchicalStats = thread_stats
|
|
299
305
|
container = thread_stats.children
|
|
300
306
|
for height, frame in enumerate(sample.frames or []):
|
|
301
|
-
stats = FrameStats(
|
|
307
|
+
stats = FrameStats(
|
|
308
|
+
label=frame, height=height, own=0, total=metric, samples=1
|
|
309
|
+
)
|
|
302
310
|
container[frame] = stats
|
|
303
311
|
container = stats.children
|
|
304
312
|
stats.own = stats.total
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: austin-python
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.4.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
|
|
@@ -4,14 +4,14 @@ austin/base.py,sha256=Xq734VKzE2ggShs6UThsGznBGazg-gCw_nZEJDJKMlM,7553
|
|
|
4
4
|
austin/cli.py,sha256=--PKm2I4NBBSSSBV5l-YC0mcmLjrvb1FHzxgY4dkby4,7826
|
|
5
5
|
austin/config.py,sha256=PEwdG2Cy8ay1SrFr2N6YfQSZwseYBA1BA5dMfocKR3A,2091
|
|
6
6
|
austin/errors.py,sha256=lCDSTANW_95Az_eDXbn__JRBllB9yw7tGnFo1-tw09Q,1046
|
|
7
|
-
austin/events.py,sha256=
|
|
7
|
+
austin/events.py,sha256=afe6SYCXD_47qn6fVajKAgq4J-ivnEBtSOGqRAWFoA4,4910
|
|
8
8
|
austin/simple.py,sha256=iTmJGAjSBeUv82SgdMTwdNlAQwuK33RyU8FkBCE8rRg,5579
|
|
9
|
-
austin/stats.py,sha256=
|
|
9
|
+
austin/stats.py,sha256=YAE641Uwjt0Ty-CbhsHPeCy2B8OgShY_oo-BW-sJ_7Q,11465
|
|
10
10
|
austin/threads.py,sha256=AvcoOkr3FeFAGBcqgXukNIt9xOZUipv7pXMqPIL_Qts,3649
|
|
11
11
|
austin/format/__init__.py,sha256=ZtBJCLpTiZaLFwCN1UbMXLQvYjOSpwmtyGwqgK8yruQ,405
|
|
12
12
|
austin/format/collapsed_stack.py,sha256=vD0CFkUgnRqHaA7dYsh4QYRZubE5IxyhVFnuPa37BSs,7245
|
|
13
13
|
austin/format/compress.py,sha256=by6i79o0pEmGf2Py8edC6RWHVdr5RGsG2FWT6xQjUvk,3797
|
|
14
|
-
austin/format/mojo.py,sha256=
|
|
14
|
+
austin/format/mojo.py,sha256=YWFoturPDxihzF0ACTF_DFaLni5FyE8BJZa9GKzCa2U,51325
|
|
15
15
|
austin/format/mojo_compress.py,sha256=qhzXsenUThxwAz1lX1Z8xtGgBpPEIbD3cqZz2Iar3ws,3711
|
|
16
16
|
austin/format/speedscope.py,sha256=hIuB1A8wbGFFLQUpZqdM7Tlf4dqFwyy01zFHDybs2Iw,8324
|
|
17
17
|
austin/format/pprof/__init__.py,sha256=E2JJDaw_k_m18OZeITKtepUUIo3n09UHdkpvabSSABc,6804
|
|
@@ -21,10 +21,10 @@ austin/format/pprof/profile_pb2.py,sha256=i_uM8lDExR95yz7qnEVikhpkpNvetKaYzoiH7A
|
|
|
21
21
|
austin/tools/diff.py,sha256=5yrHlniofIeMSqzOUD55l2h6Ps0GqEpfisFRj65gpdQ,5008
|
|
22
22
|
austin/tools/mojodbg.py,sha256=l8Lsa4rTQLXfkau8RyGziyz8ZyPEgacFVXafbHT9FN4,2816
|
|
23
23
|
austin/tools/resolve.py,sha256=bTXkll68nOtXqa0XHW79mp_seAJW8_bhd6ej6wa2_Oc,7816
|
|
24
|
-
austin_python-2.
|
|
25
|
-
austin_python-2.
|
|
26
|
-
austin_python-2.
|
|
27
|
-
austin_python-2.
|
|
28
|
-
austin_python-2.
|
|
29
|
-
austin_python-2.
|
|
30
|
-
austin_python-2.
|
|
24
|
+
austin_python-2.4.0.dist-info/METADATA,sha256=ELAptpJDAMuBwv7PBHYsyG4GAYKvT3vD4k436ECt9wo,7537
|
|
25
|
+
austin_python-2.4.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
26
|
+
austin_python-2.4.0.dist-info/entry_points.txt,sha256=xmPVC_6UkSOYu7-YMYm3BwIcMFYEhRw0qlF05Cq5gf0,378
|
|
27
|
+
austin_python-2.4.0.dist-info/licenses/AUTHORS,sha256=pARRmpJQYH8iWy_0SonXsxOiV02AFitowle4AtURxUI,45
|
|
28
|
+
austin_python-2.4.0.dist-info/licenses/COPYING,sha256=jOtLnuWt7d5Hsx6XXB2QxzrSe2sWWh3NgMfFRetluQM,35147
|
|
29
|
+
austin_python-2.4.0.dist-info/licenses/LICENSE.md,sha256=edVDhmEiPf2W5lWzqLkrj6EYLSrCeYg9MJy28YsiMFU,34937
|
|
30
|
+
austin_python-2.4.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|