austin-python 2.3.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 +51 -17
- austin/format/mojo.py +267 -112
- austin/stats.py +12 -4
- {austin_python-2.3.0.dist-info → austin_python-2.4.0.dist-info}/METADATA +1 -1
- {austin_python-2.3.0.dist-info → austin_python-2.4.0.dist-info}/RECORD +10 -10
- {austin_python-2.3.0.dist-info → austin_python-2.4.0.dist-info}/WHEEL +1 -1
- {austin_python-2.3.0.dist-info → austin_python-2.4.0.dist-info}/entry_points.txt +0 -0
- {austin_python-2.3.0.dist-info → austin_python-2.4.0.dist-info}/licenses/AUTHORS +0 -0
- {austin_python-2.3.0.dist-info → austin_python-2.4.0.dist-info}/licenses/COPYING +0 -0
- {austin_python-2.3.0.dist-info → austin_python-2.4.0.dist-info}/licenses/LICENSE.md +0 -0
austin/events.py
CHANGED
|
@@ -47,31 +47,59 @@ class AustinMetadata(AustinEvent):
|
|
|
47
47
|
class AustinTask:
|
|
48
48
|
"""A node in an asyncio task tree (3.14+ only).
|
|
49
49
|
|
|
50
|
-
``frames`` is this task's
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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).
|
|
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).
|
|
66
59
|
"""
|
|
67
60
|
|
|
68
61
|
task_id: int
|
|
69
62
|
name: t.Optional[str]
|
|
70
63
|
frames: t.Tuple[AustinFrame, ...] = ()
|
|
71
|
-
elapsed: t.Optional[MicroSeconds] = None
|
|
72
64
|
awaiting: t.Tuple["AustinTask", ...] = ()
|
|
73
65
|
|
|
74
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
|
+
|
|
75
103
|
@dataclass(frozen=True)
|
|
76
104
|
class AustinSample(AustinEvent):
|
|
77
105
|
"""Austin sample."""
|
|
@@ -92,7 +120,13 @@ class AustinSample(AustinEvent):
|
|
|
92
120
|
frames: t.Optional[t.Tuple[AustinFrame, ...]] = None
|
|
93
121
|
gc: t.Optional[bool] = None
|
|
94
122
|
idle: t.Optional[bool] = None
|
|
123
|
+
# The live tasks owned by this sample's thread, as of this sample.
|
|
95
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
|
|
96
130
|
|
|
97
131
|
def key(
|
|
98
132
|
self,
|
austin/format/mojo.py
CHANGED
|
@@ -13,6 +13,7 @@ from austin.events import AustinMetadata
|
|
|
13
13
|
from austin.events import AustinMetrics
|
|
14
14
|
from austin.events import AustinSample
|
|
15
15
|
from austin.events import AustinTask
|
|
16
|
+
from austin.events import AustinTaskReport
|
|
16
17
|
from austin.events import InterpreterId
|
|
17
18
|
from austin.events import ProcessId
|
|
18
19
|
from austin.events import ThreadName
|
|
@@ -75,6 +76,7 @@ class MojoEvents:
|
|
|
75
76
|
STACK_REPEAT = 13
|
|
76
77
|
TASK_STACK = 14
|
|
77
78
|
TASK_WAITER = 15
|
|
79
|
+
TASK_RUNNING = 16
|
|
78
80
|
|
|
79
81
|
|
|
80
82
|
class MojoEventHandler:
|
|
@@ -219,28 +221,36 @@ class MojoTaskWaiter(MojoEvent):
|
|
|
219
221
|
waiter_id: int
|
|
220
222
|
|
|
221
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
|
+
|
|
222
237
|
@dataclass(frozen=True, eq=True)
|
|
223
238
|
class MojoTaskStack(MojoEvent):
|
|
224
239
|
"""MOJO task stack event.
|
|
225
240
|
|
|
226
|
-
Introduces
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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.
|
|
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.
|
|
238
247
|
"""
|
|
239
248
|
|
|
240
249
|
EVENT_ID = MojoEvents.TASK_STACK
|
|
241
250
|
|
|
242
251
|
task_id: int
|
|
243
252
|
name_key: int
|
|
253
|
+
frame_count: int
|
|
244
254
|
|
|
245
255
|
|
|
246
256
|
@dataclass(frozen=True, eq=True)
|
|
@@ -310,12 +320,15 @@ class _RunningSample:
|
|
|
310
320
|
metrics: t.Dict[MojoMetricType, MojoMetric] = field(default_factory=dict)
|
|
311
321
|
gc: t.Optional[bool] = None
|
|
312
322
|
idle: t.Optional[bool] = None
|
|
323
|
+
running_task: t.Optional[int] = None
|
|
324
|
+
task_reports: t.List[AustinTaskReport] = field(default_factory=list)
|
|
313
325
|
|
|
314
326
|
|
|
315
327
|
@dataclass
|
|
316
328
|
class _RunningTaskStack:
|
|
317
329
|
task_id: int
|
|
318
330
|
name_key: int
|
|
331
|
+
frame_count: int
|
|
319
332
|
frames: t.List[MojoFrame] = field(default_factory=list)
|
|
320
333
|
|
|
321
334
|
|
|
@@ -332,7 +345,8 @@ class _TaskInfo:
|
|
|
332
345
|
name: t.Optional[str]
|
|
333
346
|
owner: t.Tuple[ProcessId, ThreadName]
|
|
334
347
|
frames: t.Tuple[AustinFrame, ...] = ()
|
|
335
|
-
|
|
348
|
+
# Time spent at ``frames`` so far, i.e. since the task's last report.
|
|
349
|
+
pending: int = 0
|
|
336
350
|
|
|
337
351
|
|
|
338
352
|
def int_reader() -> t.Generator[t.Optional[int], bytes, int]:
|
|
@@ -473,10 +487,61 @@ class BaseMojoStreamReader(AustinEventIterator):
|
|
|
473
487
|
def gc(self) -> t.Optional[str]:
|
|
474
488
|
return self.metadata.get("gc")
|
|
475
489
|
|
|
476
|
-
def
|
|
477
|
-
"""
|
|
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
|
+
"""
|
|
478
539
|
assert self._running_sample is not None, self._running_sample
|
|
479
540
|
|
|
541
|
+
self._accrue_task_time(self._running_sample)
|
|
542
|
+
if last:
|
|
543
|
+
self._running_sample.task_reports.extend(self._end_task_reports())
|
|
544
|
+
|
|
480
545
|
self.samples.append(
|
|
481
546
|
sample := AustinSample(
|
|
482
547
|
pid=self._running_sample.pid,
|
|
@@ -505,6 +570,8 @@ class BaseMojoStreamReader(AustinEventIterator):
|
|
|
505
570
|
),
|
|
506
571
|
gc=self._running_sample.gc,
|
|
507
572
|
idle=self._running_sample.idle,
|
|
573
|
+
running_task=self._running_sample.running_task,
|
|
574
|
+
task_reports=tuple(self._running_sample.task_reports),
|
|
508
575
|
tasks=self.get_tasks(
|
|
509
576
|
self._running_sample.pid, self._running_sample.thread
|
|
510
577
|
),
|
|
@@ -551,6 +618,10 @@ class BaseMojoStreamReader(AustinEventIterator):
|
|
|
551
618
|
if self._running_sample is not None:
|
|
552
619
|
self._finalize_sample()
|
|
553
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
|
+
|
|
554
625
|
self._running_sample = _RunningSample(
|
|
555
626
|
pid=pid,
|
|
556
627
|
iid=iid,
|
|
@@ -593,6 +664,11 @@ class BaseMojoStreamReader(AustinEventIterator):
|
|
|
593
664
|
|
|
594
665
|
if self._running_task_stack is not None:
|
|
595
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()
|
|
596
672
|
else:
|
|
597
673
|
assert self._running_sample is not None, self._running_sample
|
|
598
674
|
self._running_sample.frames.append(frame)
|
|
@@ -608,73 +684,103 @@ class BaseMojoStreamReader(AustinEventIterator):
|
|
|
608
684
|
self._task_edges = {e for e in self._task_edges if e[0] != task_id}
|
|
609
685
|
self._last_waiter_task_id = task_id
|
|
610
686
|
|
|
611
|
-
|
|
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))
|
|
612
690
|
|
|
613
691
|
return MojoTaskWaiter(task_id, waiter_id)
|
|
614
692
|
|
|
615
|
-
def
|
|
616
|
-
"""Parse the
|
|
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)
|
|
617
699
|
|
|
618
|
-
|
|
619
|
-
|
|
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.
|
|
620
707
|
"""
|
|
621
|
-
self._running_task_stack = _RunningTaskStack(task_id, name_key)
|
|
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()
|
|
622
713
|
|
|
623
|
-
return MojoTaskStack(task_id, name_key)
|
|
714
|
+
return MojoTaskStack(task_id, name_key, frame_count)
|
|
624
715
|
|
|
625
|
-
def _finalize_task_stack(self
|
|
716
|
+
def _finalize_task_stack(self) -> None:
|
|
626
717
|
"""Finalize the running task stack.
|
|
627
718
|
|
|
628
|
-
Triggered by its
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
bracketed inside that
|
|
635
|
-
window, so there's nothing
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
than wiping the task's last real position right as it disappears.
|
|
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.
|
|
645
735
|
"""
|
|
646
736
|
ts = self._running_task_stack
|
|
647
737
|
assert ts is not None, ts
|
|
648
738
|
assert self._running_sample is not None, self._running_sample
|
|
649
739
|
|
|
650
|
-
|
|
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
|
|
740
|
+
self._running_task_stack = None
|
|
656
741
|
|
|
657
|
-
|
|
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
|
|
658
777
|
|
|
659
778
|
self._task_info[ts.task_id] = _TaskInfo(
|
|
660
779
|
name=name,
|
|
661
780
|
owner=(self._running_sample.pid, self._running_sample.thread),
|
|
662
|
-
frames=
|
|
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,
|
|
781
|
+
frames=frames,
|
|
674
782
|
)
|
|
675
783
|
|
|
676
|
-
self._running_task_stack = None
|
|
677
|
-
|
|
678
784
|
def get_tasks(self, pid: ProcessId, thread: ThreadName) -> t.Tuple[AustinTask, ...]:
|
|
679
785
|
"""Return the current best-known root tasks owned by (pid, thread).
|
|
680
786
|
|
|
@@ -682,17 +788,19 @@ class BaseMojoStreamReader(AustinEventIterator):
|
|
|
682
788
|
transitively awaits, as one AustinTask per root -- a thread can own
|
|
683
789
|
more than one at once (e.g. several siblings under one gather()).
|
|
684
790
|
|
|
685
|
-
Mirrors austin's own where_event_handler__render_tree:
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
own where_event_handler__find_task returning NULL for it). Like that
|
|
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
|
|
690
795
|
C-side counterpart, this is a no-op (returns empty) when (pid,
|
|
691
796
|
thread) owns no tasks, so callers can call it unconditionally.
|
|
692
797
|
"""
|
|
693
798
|
|
|
694
799
|
def has_parent(task_id: int) -> bool:
|
|
695
|
-
return any(
|
|
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
|
+
)
|
|
696
804
|
|
|
697
805
|
def build(task_id: int, depth: int) -> AustinTask:
|
|
698
806
|
info = self._task_info[task_id]
|
|
@@ -709,7 +817,6 @@ class BaseMojoStreamReader(AustinEventIterator):
|
|
|
709
817
|
task_id=task_id,
|
|
710
818
|
name=info.name,
|
|
711
819
|
frames=info.frames,
|
|
712
|
-
elapsed=info.elapsed,
|
|
713
820
|
awaiting=awaiting,
|
|
714
821
|
)
|
|
715
822
|
|
|
@@ -746,17 +853,7 @@ class BaseMojoStreamReader(AustinEventIterator):
|
|
|
746
853
|
return metric
|
|
747
854
|
|
|
748
855
|
def get_time_metric(self, value: int) -> MojoMetric:
|
|
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
|
-
|
|
856
|
+
"""Parse time metric."""
|
|
760
857
|
return self._get_metric(MojoMetricType.TIME, value)
|
|
761
858
|
|
|
762
859
|
def get_memory_metric(self, value: int) -> MojoMetric:
|
|
@@ -911,10 +1008,15 @@ class MojoStreamReader(BaseMojoStreamReader):
|
|
|
911
1008
|
"""Parse a task waiter edge."""
|
|
912
1009
|
return self.get_task_waiter(self.read_int(), self.read_int())
|
|
913
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
|
+
|
|
914
1016
|
@handles(MojoEvents.TASK_STACK)
|
|
915
1017
|
def parse_task_stack(self) -> MojoTaskStack:
|
|
916
|
-
"""Parse the header of a
|
|
917
|
-
return self.get_task_stack(self.read_int(), self.read_int())
|
|
1018
|
+
"""Parse the header of a task report."""
|
|
1019
|
+
return self.get_task_stack(self.read_int(), self.read_int(), self.read_int())
|
|
918
1020
|
|
|
919
1021
|
def parse_event(self) -> t.Optional[MojoEvent]:
|
|
920
1022
|
"""Parse a single event."""
|
|
@@ -973,7 +1075,7 @@ class MojoStreamReader(BaseMojoStreamReader):
|
|
|
973
1075
|
if self.samples:
|
|
974
1076
|
yield self.samples[-1]
|
|
975
1077
|
if self._running_sample is not None:
|
|
976
|
-
yield self._finalize_sample()
|
|
1078
|
+
yield self._finalize_sample(last=True)
|
|
977
1079
|
|
|
978
1080
|
def hexdump(
|
|
979
1081
|
self,
|
|
@@ -1114,10 +1216,17 @@ class AsyncMojoStreamReader(BaseMojoStreamReader):
|
|
|
1114
1216
|
"""Parse a task waiter edge."""
|
|
1115
1217
|
return self.get_task_waiter(await self.read_int(), await self.read_int())
|
|
1116
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
|
+
|
|
1117
1224
|
@handles(MojoEvents.TASK_STACK)
|
|
1118
1225
|
async def parse_task_stack(self) -> MojoTaskStack:
|
|
1119
|
-
"""Parse the header of a
|
|
1120
|
-
return self.get_task_stack(
|
|
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
|
+
)
|
|
1121
1230
|
|
|
1122
1231
|
async def parse_event(self) -> t.Optional[MojoEvent]:
|
|
1123
1232
|
"""Parse a single event."""
|
|
@@ -1176,7 +1285,7 @@ class AsyncMojoStreamReader(BaseMojoStreamReader):
|
|
|
1176
1285
|
if self.samples:
|
|
1177
1286
|
yield self.samples[-1]
|
|
1178
1287
|
if self._running_sample is not None:
|
|
1179
|
-
yield self._finalize_sample()
|
|
1288
|
+
yield self._finalize_sample(last=True)
|
|
1180
1289
|
|
|
1181
1290
|
|
|
1182
1291
|
class BaseMojoStreamWriter(abc.ABC):
|
|
@@ -1199,6 +1308,10 @@ class BaseMojoStreamWriter(abc.ABC):
|
|
|
1199
1308
|
|
|
1200
1309
|
self._new_entries: t.List[MojoEvent] = []
|
|
1201
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()
|
|
1202
1315
|
|
|
1203
1316
|
def set_metadata(self, metadata: AustinMetadata) -> None:
|
|
1204
1317
|
self._meta[metadata.name] = metadata.value
|
|
@@ -1219,7 +1332,8 @@ class BaseMojoStreamWriter(abc.ABC):
|
|
|
1219
1332
|
try:
|
|
1220
1333
|
return self._task_ids[task_id]
|
|
1221
1334
|
except KeyError:
|
|
1222
|
-
|
|
1335
|
+
# 0 is reserved: it stands for no task (e.g. an unknown waiter).
|
|
1336
|
+
compact_id = len(self._task_ids) + 1
|
|
1223
1337
|
self._task_ids[task_id] = compact_id
|
|
1224
1338
|
return compact_id
|
|
1225
1339
|
|
|
@@ -1260,51 +1374,88 @@ class BaseMojoStreamWriter(abc.ABC):
|
|
|
1260
1374
|
edges.setdefault(awaited.task_id, set()).add(task.task_id)
|
|
1261
1375
|
self._collect_tasks(task.awaiting, seen, edges)
|
|
1262
1376
|
|
|
1263
|
-
def write_tasks(self,
|
|
1264
|
-
"""Write
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
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.
|
|
1270
1394
|
"""
|
|
1271
1395
|
size = 0
|
|
1272
1396
|
|
|
1273
1397
|
seen: t.Dict[int, AustinTask] = {}
|
|
1274
1398
|
edges: t.Dict[int, t.Set[int]] = {}
|
|
1275
|
-
self._collect_tasks(tasks, seen, edges)
|
|
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
|
+
)
|
|
1276
1415
|
|
|
1277
|
-
for
|
|
1278
|
-
|
|
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]
|
|
1279
1426
|
|
|
1280
1427
|
while self._new_entries:
|
|
1281
1428
|
size += self.mojo.write(self._new_entries.pop(0).to_bytes())
|
|
1282
1429
|
|
|
1283
1430
|
size += self.mojo.write(
|
|
1284
1431
|
MojoTaskStack(
|
|
1285
|
-
task_id=self.resolve_task_id(
|
|
1432
|
+
task_id=self.resolve_task_id(report.task_id),
|
|
1433
|
+
name_key=name_key,
|
|
1434
|
+
frame_count=len(task_frames),
|
|
1286
1435
|
).to_bytes()
|
|
1287
1436
|
)
|
|
1288
1437
|
|
|
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
1438
|
for frame in task_frames:
|
|
1295
1439
|
size += self.mojo.write(MojoFrameReference(frame).to_bytes())
|
|
1296
1440
|
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
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
|
|
1300
1452
|
|
|
1301
|
-
for task_id, waiter_ids in edges.items():
|
|
1302
1453
|
compact_task_id = self.resolve_task_id(task_id)
|
|
1303
|
-
for waiter_id in waiter_ids:
|
|
1454
|
+
for waiter_id in waiter_ids or (0,):
|
|
1304
1455
|
size += self.mojo.write(
|
|
1305
1456
|
MojoTaskWaiter(
|
|
1306
1457
|
task_id=compact_task_id,
|
|
1307
|
-
waiter_id=self.resolve_task_id(waiter_id),
|
|
1458
|
+
waiter_id=self.resolve_task_id(waiter_id) if waiter_id else 0,
|
|
1308
1459
|
).to_bytes()
|
|
1309
1460
|
)
|
|
1310
1461
|
|
|
@@ -1338,13 +1489,17 @@ class MojoStreamWriter(BaseMojoStreamWriter):
|
|
|
1338
1489
|
).to_bytes()
|
|
1339
1490
|
)
|
|
1340
1491
|
|
|
1341
|
-
# Task-scan payload (waiter edges
|
|
1342
|
-
#
|
|
1492
|
+
# Task-scan payload (task reports, then waiter edges) is written
|
|
1493
|
+
# before this sample's own frames -- it
|
|
1343
1494
|
# rides between the enclosing MOJO_STACK header and the sample's
|
|
1344
1495
|
# own frame dump on the real wire too (see austin's own
|
|
1345
1496
|
# py_proc.c, _py_proc__sample_threads).
|
|
1346
|
-
|
|
1347
|
-
|
|
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
|
+
)
|
|
1348
1503
|
|
|
1349
1504
|
frames = (
|
|
1350
1505
|
[self.resolve_frame(f) for f in event.frames] if event.frames else []
|
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
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
|