austin-python 2.1.2__py3-none-any.whl → 2.3.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/base.py +0 -1
- austin/events.py +30 -1
- austin/format/collapsed_stack.py +4 -5
- austin/format/compress.py +7 -5
- austin/format/mojo.py +450 -33
- austin/format/mojo_compress.py +122 -0
- austin/format/pprof/__init__.py +0 -1
- austin/format/speedscope.py +0 -2
- austin/stats.py +10 -8
- austin/tools/diff.py +6 -4
- austin/tools/mojodbg.py +0 -1
- austin/tools/resolve.py +8 -5
- {austin_python-2.1.2.dist-info → austin_python-2.3.0.dist-info}/METADATA +5 -5
- austin_python-2.3.0.dist-info/RECORD +30 -0
- {austin_python-2.1.2.dist-info → austin_python-2.3.0.dist-info}/WHEEL +1 -1
- {austin_python-2.1.2.dist-info → austin_python-2.3.0.dist-info}/entry_points.txt +1 -0
- austin_python-2.1.2.dist-info/RECORD +0 -29
- {austin_python-2.1.2.dist-info → austin_python-2.3.0.dist-info}/licenses/AUTHORS +0 -0
- {austin_python-2.1.2.dist-info → austin_python-2.3.0.dist-info}/licenses/COPYING +0 -0
- {austin_python-2.1.2.dist-info → austin_python-2.3.0.dist-info}/licenses/LICENSE.md +0 -0
austin/base.py
CHANGED
austin/events.py
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import typing as t
|
|
2
2
|
from dataclasses import dataclass
|
|
3
3
|
|
|
4
|
-
|
|
5
4
|
ProcessId = int
|
|
6
5
|
ThreadName = str
|
|
7
6
|
InterpreterId = int
|
|
@@ -44,6 +43,35 @@ class AustinMetadata(AustinEvent):
|
|
|
44
43
|
value: str
|
|
45
44
|
|
|
46
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
|
+
|
|
47
75
|
@dataclass(frozen=True)
|
|
48
76
|
class AustinSample(AustinEvent):
|
|
49
77
|
"""Austin sample."""
|
|
@@ -64,6 +92,7 @@ class AustinSample(AustinEvent):
|
|
|
64
92
|
frames: t.Optional[t.Tuple[AustinFrame, ...]] = None
|
|
65
93
|
gc: t.Optional[bool] = None
|
|
66
94
|
idle: t.Optional[bool] = None
|
|
95
|
+
tasks: t.Tuple[AustinTask, ...] = ()
|
|
67
96
|
|
|
68
97
|
def key(
|
|
69
98
|
self,
|
austin/format/collapsed_stack.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import typing as t
|
|
1
2
|
from functools import singledispatchmethod
|
|
2
3
|
from pathlib import Path
|
|
3
|
-
import typing as t
|
|
4
4
|
|
|
5
5
|
from austin.events import AustinEvent
|
|
6
6
|
from austin.events import AustinEventIterator
|
|
@@ -10,7 +10,6 @@ from austin.events import AustinMetrics
|
|
|
10
10
|
from austin.events import AustinSample
|
|
11
11
|
from austin.format.mojo import MojoStreamReader
|
|
12
12
|
|
|
13
|
-
|
|
14
13
|
__version__ = "0.1.0"
|
|
15
14
|
|
|
16
15
|
|
|
@@ -66,17 +65,17 @@ def parse_collapsed_stack(sample: str) -> AustinSample:
|
|
|
66
65
|
raise InvalidSample(sample)
|
|
67
66
|
|
|
68
67
|
if sample[0] != "P":
|
|
69
|
-
raise InvalidSample(f"No process ID in sample
|
|
68
|
+
raise InvalidSample(f"No process ID in sample {sample!r}")
|
|
70
69
|
|
|
71
70
|
head, _, metrics = sample.rpartition(" ")
|
|
72
71
|
process, _, rest = head.partition(";")
|
|
73
72
|
try:
|
|
74
73
|
pid = int(process[1:])
|
|
75
74
|
except ValueError:
|
|
76
|
-
raise InvalidSample(f"Invalid process ID in sample
|
|
75
|
+
raise InvalidSample(f"Invalid process ID in sample {sample!r}") from None
|
|
77
76
|
|
|
78
77
|
if rest[0] != "T":
|
|
79
|
-
raise InvalidSample(f"No thread ID in sample
|
|
78
|
+
raise InvalidSample(f"No thread ID in sample {sample!r}")
|
|
80
79
|
|
|
81
80
|
thread, _, frames_data = rest.partition(";")
|
|
82
81
|
thread = thread[1:]
|
austin/format/compress.py
CHANGED
|
@@ -62,9 +62,11 @@ def compress(source: AustinFileReader, dest: TextIO, counts: bool = False) -> No
|
|
|
62
62
|
formatter.format(
|
|
63
63
|
AustinSample.from_key_and_metrics(
|
|
64
64
|
key,
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
65
|
+
(
|
|
66
|
+
AustinMetrics(memory=metrics)
|
|
67
|
+
if is_memory
|
|
68
|
+
else AustinMetrics(time=metrics)
|
|
69
|
+
),
|
|
68
70
|
)
|
|
69
71
|
)
|
|
70
72
|
+ "\n"
|
|
@@ -108,10 +110,10 @@ def main() -> None:
|
|
|
108
110
|
|
|
109
111
|
try:
|
|
110
112
|
with (
|
|
111
|
-
|
|
113
|
+
open(args.input) as _fin,
|
|
112
114
|
open(args.output or args.input, "w") as fout,
|
|
113
115
|
):
|
|
114
|
-
compress(
|
|
116
|
+
compress(AustinFileReader(_fin), fout, args.counts)
|
|
115
117
|
except FileNotFoundError:
|
|
116
118
|
print(f"No such input file: {args.input}")
|
|
117
119
|
exit(1)
|
austin/format/mojo.py
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import abc
|
|
2
2
|
import asyncio
|
|
3
|
-
import io
|
|
4
3
|
import typing as t
|
|
5
4
|
from dataclasses import dataclass
|
|
6
5
|
from dataclasses import field
|
|
@@ -13,10 +12,16 @@ from austin.events import AustinFrame
|
|
|
13
12
|
from austin.events import AustinMetadata
|
|
14
13
|
from austin.events import AustinMetrics
|
|
15
14
|
from austin.events import AustinSample
|
|
15
|
+
from austin.events import AustinTask
|
|
16
16
|
from austin.events import InterpreterId
|
|
17
17
|
from austin.events import ProcessId
|
|
18
18
|
from austin.events import ThreadName
|
|
19
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
|
+
|
|
20
25
|
|
|
21
26
|
def to_varint(n: int) -> bytes:
|
|
22
27
|
"""Convert an integer to a variable-length integer."""
|
|
@@ -67,6 +72,9 @@ class MojoEvents:
|
|
|
67
72
|
METRIC_MEMORY = 10
|
|
68
73
|
STRING = 11
|
|
69
74
|
STRING_REF = 12
|
|
75
|
+
STACK_REPEAT = 13
|
|
76
|
+
TASK_STACK = 14
|
|
77
|
+
TASK_WAITER = 15
|
|
70
78
|
|
|
71
79
|
|
|
72
80
|
class MojoEventHandler:
|
|
@@ -84,6 +92,7 @@ class MojoEvent:
|
|
|
84
92
|
"""MOJO event."""
|
|
85
93
|
|
|
86
94
|
EVENT_ID: t.ClassVar = None
|
|
95
|
+
raw: t.ClassVar[bytes] = b""
|
|
87
96
|
|
|
88
97
|
def ref(self) -> int:
|
|
89
98
|
return getattr(self, fields(self)[0].name)
|
|
@@ -93,9 +102,7 @@ class MojoEvent:
|
|
|
93
102
|
for f in fields(self):
|
|
94
103
|
value = getattr(self, f.name)
|
|
95
104
|
field_type = (
|
|
96
|
-
f.type
|
|
97
|
-
if isinstance(f.type, t._UnionGenericAlias)
|
|
98
|
-
else f.type
|
|
105
|
+
t.get_args(f.type)[0] if t.get_origin(f.type) is t.Union else f.type
|
|
99
106
|
)
|
|
100
107
|
if field_type is str:
|
|
101
108
|
buffer += value.encode()
|
|
@@ -127,9 +134,11 @@ class MojoMetric(MojoEvent):
|
|
|
127
134
|
def to_bytes(self) -> bytes:
|
|
128
135
|
buffer = bytearray(
|
|
129
136
|
[
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
137
|
+
(
|
|
138
|
+
MojoEvents.METRIC_TIME
|
|
139
|
+
if self.metric_type is MojoMetricType.TIME
|
|
140
|
+
else MojoEvents.METRIC_MEMORY
|
|
141
|
+
)
|
|
133
142
|
]
|
|
134
143
|
)
|
|
135
144
|
buffer += to_varint(self.value)
|
|
@@ -184,6 +193,56 @@ class MojoStack(MojoEvent):
|
|
|
184
193
|
tid: str
|
|
185
194
|
|
|
186
195
|
|
|
196
|
+
@dataclass(frozen=True, eq=True)
|
|
197
|
+
class MojoStackRepeat(MojoEvent):
|
|
198
|
+
"""MOJO stack repeat event.
|
|
199
|
+
|
|
200
|
+
Signals that the previous sample for this thread provides the base
|
|
201
|
+
(outermost) frames for the current sample. The frames accumulated so
|
|
202
|
+
far in the current sample are the innermost (top) part of the call stack.
|
|
203
|
+
"""
|
|
204
|
+
|
|
205
|
+
EVENT_ID = MojoEvents.STACK_REPEAT
|
|
206
|
+
|
|
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
|
+
|
|
187
246
|
@dataclass(frozen=True, eq=True)
|
|
188
247
|
class MojoFrame(MojoEvent):
|
|
189
248
|
"""MOJO frame."""
|
|
@@ -253,6 +312,29 @@ class _RunningSample:
|
|
|
253
312
|
idle: t.Optional[bool] = None
|
|
254
313
|
|
|
255
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
|
+
|
|
256
338
|
def int_reader() -> t.Generator[t.Optional[int], bytes, int]:
|
|
257
339
|
(b,) = yield None
|
|
258
340
|
while True:
|
|
@@ -322,6 +404,31 @@ class BaseMojoStreamReader(AustinEventIterator):
|
|
|
322
404
|
self._str_reader = str_reader()
|
|
323
405
|
next(self._str_reader)
|
|
324
406
|
|
|
407
|
+
# Per-thread previous frame list for STACK_REPEAT expansion.
|
|
408
|
+
# Key: (pid, thread_name); value: list of MojoFrame accumulated by the
|
|
409
|
+
# last fully-finalised sample for that thread.
|
|
410
|
+
self._prev_frames: t.Dict[t.Tuple[int, str], t.List[MojoFrame]] = {}
|
|
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
|
+
|
|
325
432
|
# Austin events
|
|
326
433
|
self.metadata: t.Dict[str, str] = {}
|
|
327
434
|
self.samples: t.List[AustinSample] = []
|
|
@@ -381,28 +488,64 @@ class BaseMojoStreamReader(AustinEventIterator):
|
|
|
381
488
|
for metric_type, metric in self._running_sample.metrics.items()
|
|
382
489
|
}
|
|
383
490
|
),
|
|
384
|
-
frames=
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
491
|
+
frames=(
|
|
492
|
+
tuple(
|
|
493
|
+
AustinFrame(
|
|
494
|
+
filename=mf.filename.value,
|
|
495
|
+
function=mf.scope.value,
|
|
496
|
+
line=mf.line,
|
|
497
|
+
line_end=mf.line_end,
|
|
498
|
+
column=mf.column,
|
|
499
|
+
column_end=mf.column_end,
|
|
500
|
+
)
|
|
501
|
+
for mf in self._running_sample.frames
|
|
392
502
|
)
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
else None,
|
|
503
|
+
if self._running_sample.frames
|
|
504
|
+
else None
|
|
505
|
+
),
|
|
397
506
|
gc=self._running_sample.gc,
|
|
398
507
|
idle=self._running_sample.idle,
|
|
508
|
+
tasks=self.get_tasks(
|
|
509
|
+
self._running_sample.pid, self._running_sample.thread
|
|
510
|
+
),
|
|
399
511
|
)
|
|
400
512
|
)
|
|
401
513
|
|
|
514
|
+
# Save fully-expanded frame list for future STACK_REPEAT events from
|
|
515
|
+
# this thread.
|
|
516
|
+
self._prev_frames[(self._running_sample.pid, self._running_sample.thread)] = (
|
|
517
|
+
list(self._running_sample.frames)
|
|
518
|
+
)
|
|
519
|
+
|
|
402
520
|
self._running_sample = None
|
|
403
521
|
|
|
404
522
|
return sample
|
|
405
523
|
|
|
524
|
+
@staticmethod
|
|
525
|
+
def _is_native_frame(frame: MojoFrame) -> bool:
|
|
526
|
+
name = frame.filename.value
|
|
527
|
+
return not name.endswith(".py") and not (
|
|
528
|
+
name.startswith("<") and name.endswith(">")
|
|
529
|
+
)
|
|
530
|
+
|
|
531
|
+
def get_stack_repeat(self) -> MojoStackRepeat:
|
|
532
|
+
"""Handle a STACK_REPEAT event.
|
|
533
|
+
|
|
534
|
+
The previous sample's frames (minus any trailing native frames that
|
|
535
|
+
belonged to the old top-of-stack above the eval frame) are prepended
|
|
536
|
+
to the frames accumulated so far.
|
|
537
|
+
"""
|
|
538
|
+
assert self._running_sample is not None
|
|
539
|
+
key = (self._running_sample.pid, self._running_sample.thread)
|
|
540
|
+
prev = list(self._prev_frames.get(key, []))
|
|
541
|
+
# Strip native frames from the innermost (top) end of the previous
|
|
542
|
+
# stack — those were above the eval frame and have now been replaced
|
|
543
|
+
# by the new frames in the current sample.
|
|
544
|
+
while prev and self._is_native_frame(prev[-1]):
|
|
545
|
+
prev.pop()
|
|
546
|
+
self._running_sample.frames = prev + self._running_sample.frames
|
|
547
|
+
return MojoStackRepeat()
|
|
548
|
+
|
|
406
549
|
def get_stack(self, pid: int, iid: t.Optional[int], thread: str) -> MojoStack:
|
|
407
550
|
"""Parse a stack."""
|
|
408
551
|
if self._running_sample is not None:
|
|
@@ -448,11 +591,148 @@ class BaseMojoStreamReader(AustinEventIterator):
|
|
|
448
591
|
"""Parse a frame reference."""
|
|
449
592
|
frame = self._frame_map[self.ref(ref)]
|
|
450
593
|
|
|
451
|
-
|
|
452
|
-
|
|
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)
|
|
453
599
|
|
|
454
600
|
return MojoFrameReference(frame)
|
|
455
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
|
+
|
|
456
736
|
def get_kernel_frame(self, name: str) -> MojoKernelFrame:
|
|
457
737
|
"""Parse kernel frame."""
|
|
458
738
|
return MojoKernelFrame(name)
|
|
@@ -466,7 +746,17 @@ class BaseMojoStreamReader(AustinEventIterator):
|
|
|
466
746
|
return metric
|
|
467
747
|
|
|
468
748
|
def get_time_metric(self, value: int) -> MojoMetric:
|
|
469
|
-
"""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
|
+
|
|
470
760
|
return self._get_metric(MojoMetricType.TIME, value)
|
|
471
761
|
|
|
472
762
|
def get_memory_metric(self, value: int) -> MojoMetric:
|
|
@@ -579,7 +869,7 @@ class MojoStreamReader(BaseMojoStreamReader):
|
|
|
579
869
|
@handles(MojoEvents.METRIC_TIME)
|
|
580
870
|
def parse_time_metric(self) -> MojoMetric:
|
|
581
871
|
"""Parse time metric."""
|
|
582
|
-
return self.
|
|
872
|
+
return self.get_time_metric(self.read_int())
|
|
583
873
|
|
|
584
874
|
@handles(MojoEvents.METRIC_MEMORY)
|
|
585
875
|
def parse_memory_metric(self) -> MojoMetric:
|
|
@@ -611,6 +901,21 @@ class MojoStreamReader(BaseMojoStreamReader):
|
|
|
611
901
|
"""Parse string reference."""
|
|
612
902
|
return self.get_string_ref(self.read_int())
|
|
613
903
|
|
|
904
|
+
@handles(MojoEvents.STACK_REPEAT)
|
|
905
|
+
def parse_stack_repeat(self) -> MojoStackRepeat:
|
|
906
|
+
"""Parse a stack repeat event."""
|
|
907
|
+
return self.get_stack_repeat()
|
|
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
|
+
|
|
614
919
|
def parse_event(self) -> t.Optional[MojoEvent]:
|
|
615
920
|
"""Parse a single event."""
|
|
616
921
|
try:
|
|
@@ -670,7 +975,12 @@ class MojoStreamReader(BaseMojoStreamReader):
|
|
|
670
975
|
if self._running_sample is not None:
|
|
671
976
|
yield self._finalize_sample()
|
|
672
977
|
|
|
673
|
-
def hexdump(
|
|
978
|
+
def hexdump(
|
|
979
|
+
self,
|
|
980
|
+
start: int,
|
|
981
|
+
end: int,
|
|
982
|
+
highlight: t.Set[int] = set(), # noqa: B006
|
|
983
|
+
) -> None:
|
|
674
984
|
"""Print a hexdump of the MOJO file."""
|
|
675
985
|
self.mojo.seek(start)
|
|
676
986
|
data = self.mojo.read(end - start)
|
|
@@ -760,7 +1070,7 @@ class AsyncMojoStreamReader(BaseMojoStreamReader):
|
|
|
760
1070
|
@handles(MojoEvents.METRIC_TIME)
|
|
761
1071
|
async def parse_time_metric(self) -> MojoMetric:
|
|
762
1072
|
"""Parse time metric."""
|
|
763
|
-
return await self.
|
|
1073
|
+
return self.get_time_metric(await self.read_int())
|
|
764
1074
|
|
|
765
1075
|
@handles(MojoEvents.METRIC_MEMORY)
|
|
766
1076
|
async def parse_memory_metric(self) -> MojoMetric:
|
|
@@ -794,6 +1104,21 @@ class AsyncMojoStreamReader(BaseMojoStreamReader):
|
|
|
794
1104
|
"""Parse string reference."""
|
|
795
1105
|
return self.get_string_ref(await self.read_int())
|
|
796
1106
|
|
|
1107
|
+
@handles(MojoEvents.STACK_REPEAT)
|
|
1108
|
+
async def parse_stack_repeat(self) -> MojoStackRepeat:
|
|
1109
|
+
"""Parse a stack repeat event."""
|
|
1110
|
+
return self.get_stack_repeat()
|
|
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
|
+
|
|
797
1122
|
async def parse_event(self) -> t.Optional[MojoEvent]:
|
|
798
1123
|
"""Parse a single event."""
|
|
799
1124
|
try:
|
|
@@ -855,7 +1180,9 @@ class AsyncMojoStreamReader(BaseMojoStreamReader):
|
|
|
855
1180
|
|
|
856
1181
|
|
|
857
1182
|
class BaseMojoStreamWriter(abc.ABC):
|
|
858
|
-
|
|
1183
|
+
"""Base class for MOJO stream writers."""
|
|
1184
|
+
|
|
1185
|
+
HEADER = b"MOJ\x04"
|
|
859
1186
|
|
|
860
1187
|
def __init__(self, mojo: t.Any) -> None:
|
|
861
1188
|
self.mojo = mojo
|
|
@@ -871,6 +1198,7 @@ class BaseMojoStreamWriter(abc.ABC):
|
|
|
871
1198
|
self._gc = False
|
|
872
1199
|
|
|
873
1200
|
self._new_entries: t.List[MojoEvent] = []
|
|
1201
|
+
self._task_ids: t.Dict[int, int] = {}
|
|
874
1202
|
|
|
875
1203
|
def set_metadata(self, metadata: AustinMetadata) -> None:
|
|
876
1204
|
self._meta[metadata.name] = metadata.value
|
|
@@ -887,6 +1215,14 @@ class BaseMojoStreamWriter(abc.ABC):
|
|
|
887
1215
|
self._new_entries.append(mojo_string)
|
|
888
1216
|
return mojo_string
|
|
889
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
|
+
|
|
890
1226
|
def resolve_frame(self, frame: AustinFrame) -> MojoFrame:
|
|
891
1227
|
try:
|
|
892
1228
|
return self._frames[frame]
|
|
@@ -903,12 +1239,85 @@ class BaseMojoStreamWriter(abc.ABC):
|
|
|
903
1239
|
self._new_entries.append(mojo_frame)
|
|
904
1240
|
return mojo_frame
|
|
905
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
|
+
|
|
906
1313
|
@abc.abstractmethod
|
|
907
|
-
def write(self, event: AustinEvent) -> int: ...
|
|
1314
|
+
def write(self, event: AustinEvent) -> int: ... # noqa: E704
|
|
908
1315
|
|
|
909
1316
|
|
|
910
1317
|
class MojoStreamWriter(BaseMojoStreamWriter):
|
|
911
|
-
|
|
1318
|
+
"""MOJO stream writer."""
|
|
1319
|
+
|
|
1320
|
+
def __init__(self, mojo: t.BinaryIO) -> None:
|
|
912
1321
|
super().__init__(mojo)
|
|
913
1322
|
|
|
914
1323
|
mojo.write(self.HEADER)
|
|
@@ -923,23 +1332,31 @@ class MojoStreamWriter(BaseMojoStreamWriter):
|
|
|
923
1332
|
)
|
|
924
1333
|
|
|
925
1334
|
elif isinstance(event, AustinSample):
|
|
926
|
-
frames = (
|
|
927
|
-
[self.resolve_frame(f) for f in event.frames] if event.frames else []
|
|
928
|
-
)
|
|
929
|
-
|
|
930
1335
|
size += self.mojo.write(
|
|
931
1336
|
MojoStack(
|
|
932
1337
|
pid=event.pid, iid=event.iid or 0, tid=event.thread
|
|
933
1338
|
).to_bytes()
|
|
934
1339
|
)
|
|
935
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
|
+
|
|
936
1353
|
while self._new_entries:
|
|
937
1354
|
size += self.mojo.write(self._new_entries.pop(0).to_bytes())
|
|
938
1355
|
|
|
939
1356
|
for frame in frames:
|
|
940
1357
|
size += self.mojo.write(MojoFrameReference(frame).to_bytes())
|
|
941
1358
|
|
|
942
|
-
if
|
|
1359
|
+
if event.gc:
|
|
943
1360
|
size += self.mojo.write(bytes([MojoEvents.GC]))
|
|
944
1361
|
|
|
945
1362
|
if self._mode == "full":
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# This file is part of "austin-python" which is released under GPL.
|
|
2
|
+
#
|
|
3
|
+
# See file LICENCE or go to http://www.gnu.org/licenses/ for full license
|
|
4
|
+
# details.
|
|
5
|
+
#
|
|
6
|
+
# austin-python is a Python wrapper around Austin, the CPython frame stack
|
|
7
|
+
# sampler.
|
|
8
|
+
#
|
|
9
|
+
# Copyright (c) 2018-2020 Gabriele N. Tornetta <phoenix1987@gmail.com>.
|
|
10
|
+
# All rights reserved.
|
|
11
|
+
#
|
|
12
|
+
# This program is free software: you can redistribute it and/or modify
|
|
13
|
+
# it under the terms of the GNU General Public License as published by
|
|
14
|
+
# the Free Software Foundation, either version 3 of the License, or
|
|
15
|
+
# (at your option) any later version.
|
|
16
|
+
#
|
|
17
|
+
# This program is distributed in the hope that it will be useful,
|
|
18
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
19
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
20
|
+
# GNU General Public License for more details.
|
|
21
|
+
# You should have received a copy of the GNU General Public License
|
|
22
|
+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
23
|
+
|
|
24
|
+
import typing as t
|
|
25
|
+
from io import BytesIO
|
|
26
|
+
|
|
27
|
+
from austin.events import AustinMetadata
|
|
28
|
+
from austin.events import AustinMetrics
|
|
29
|
+
from austin.events import AustinSample
|
|
30
|
+
from austin.format.mojo import MojoStreamReader
|
|
31
|
+
from austin.format.mojo import MojoStreamWriter
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def compress(source: t.BinaryIO, dest: t.BinaryIO) -> None:
|
|
35
|
+
"""Compress a MOJO source stream.
|
|
36
|
+
|
|
37
|
+
Aggregates all metrics on equal stacks, where equality is determined by
|
|
38
|
+
pid, interpreter id, thread, frames, gc flag, and idle flag.
|
|
39
|
+
"""
|
|
40
|
+
time_stats: t.Dict[AustinSample.Key, int] = {}
|
|
41
|
+
memory_stats: t.Dict[AustinSample.Key, int] = {}
|
|
42
|
+
metadata: t.List[AustinMetadata] = []
|
|
43
|
+
samples: t.Dict[AustinSample.Key, AustinSample] = {}
|
|
44
|
+
|
|
45
|
+
reader = MojoStreamReader(source)
|
|
46
|
+
|
|
47
|
+
for event in reader:
|
|
48
|
+
if isinstance(event, AustinMetadata):
|
|
49
|
+
metadata.append(event)
|
|
50
|
+
continue
|
|
51
|
+
|
|
52
|
+
assert isinstance(event, AustinSample)
|
|
53
|
+
|
|
54
|
+
key = event.key()
|
|
55
|
+
if key not in samples:
|
|
56
|
+
samples[key] = event
|
|
57
|
+
if event.metrics.time is not None:
|
|
58
|
+
time_stats[key] = time_stats.get(key, 0) + event.metrics.time
|
|
59
|
+
if event.metrics.memory is not None:
|
|
60
|
+
memory_stats[key] = memory_stats.get(key, 0) + event.metrics.memory
|
|
61
|
+
|
|
62
|
+
writer = MojoStreamWriter(dest)
|
|
63
|
+
|
|
64
|
+
for m in metadata:
|
|
65
|
+
writer.write(m)
|
|
66
|
+
|
|
67
|
+
for key in samples:
|
|
68
|
+
writer.write(
|
|
69
|
+
AustinSample.from_key_and_metrics(
|
|
70
|
+
key,
|
|
71
|
+
AustinMetrics(
|
|
72
|
+
time=time_stats.get(key),
|
|
73
|
+
memory=memory_stats.get(key),
|
|
74
|
+
),
|
|
75
|
+
)
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def main() -> None:
|
|
80
|
+
"""mojo-compress entry point."""
|
|
81
|
+
from argparse import ArgumentParser
|
|
82
|
+
|
|
83
|
+
arg_parser = ArgumentParser(
|
|
84
|
+
prog="mojo-compress",
|
|
85
|
+
description=(
|
|
86
|
+
"Compress a MOJO sample file by aggregating the collected samples."
|
|
87
|
+
),
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
arg_parser.add_argument(
|
|
91
|
+
"input",
|
|
92
|
+
type=str,
|
|
93
|
+
help="The input MOJO file.",
|
|
94
|
+
)
|
|
95
|
+
arg_parser.add_argument(
|
|
96
|
+
"output",
|
|
97
|
+
type=str,
|
|
98
|
+
help="The output MOJO file; defaults to the input file for in-place compression.",
|
|
99
|
+
nargs="?",
|
|
100
|
+
default=None,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
arg_parser.add_argument("-V", "--version", action="version", version="0.1.0")
|
|
104
|
+
|
|
105
|
+
args = arg_parser.parse_args()
|
|
106
|
+
|
|
107
|
+
try:
|
|
108
|
+
with open(args.input, "rb") as fin:
|
|
109
|
+
buffer = BytesIO()
|
|
110
|
+
compress(fin, buffer)
|
|
111
|
+
|
|
112
|
+
output_path = args.output or args.input
|
|
113
|
+
with open(output_path, "wb") as fout:
|
|
114
|
+
fout.write(buffer.getvalue())
|
|
115
|
+
|
|
116
|
+
except FileNotFoundError:
|
|
117
|
+
print(f"No such input file: {args.input}")
|
|
118
|
+
exit(1)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
if __name__ == "__main__":
|
|
122
|
+
main()
|
austin/format/pprof/__init__.py
CHANGED
austin/format/speedscope.py
CHANGED
|
@@ -38,8 +38,6 @@ from austin.events import AustinMetadata
|
|
|
38
38
|
from austin.events import AustinSample
|
|
39
39
|
from austin.format import Mode
|
|
40
40
|
from austin.format.collapsed_stack import AustinFileReader
|
|
41
|
-
from austin.format.collapsed_stack import InvalidSample
|
|
42
|
-
|
|
43
41
|
|
|
44
42
|
__version__ = "0.3.1"
|
|
45
43
|
|
austin/stats.py
CHANGED
|
@@ -42,7 +42,6 @@ from austin.events import AustinSample
|
|
|
42
42
|
from austin.events import ProcessId
|
|
43
43
|
from austin.format.collapsed_stack import AustinEventCollapsedStackFormatter
|
|
44
44
|
|
|
45
|
-
|
|
46
45
|
ThreadInfo = namedtuple("ThreadInfo", ["thread", "iid"])
|
|
47
46
|
|
|
48
47
|
|
|
@@ -123,9 +122,11 @@ class FrameStats(HierarchicalStats):
|
|
|
123
122
|
if self.own:
|
|
124
123
|
yield (
|
|
125
124
|
[frame],
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
125
|
+
(
|
|
126
|
+
AustinMetrics(time=self.own)
|
|
127
|
+
if stats_type in {AustinStatsType.CPU, AustinStatsType.WALL}
|
|
128
|
+
else AustinMetrics(memory=self.own)
|
|
129
|
+
),
|
|
129
130
|
)
|
|
130
131
|
|
|
131
132
|
if self.children:
|
|
@@ -146,14 +147,15 @@ class ThreadStats(HierarchicalStats):
|
|
|
146
147
|
self, stats_type: AustinStatsType
|
|
147
148
|
) -> Generator[Tuple[ThreadInfo, List[AustinFrame], AustinMetrics], None, None]:
|
|
148
149
|
"""Collapse the hierarchical statistics."""
|
|
149
|
-
|
|
150
150
|
if self.own:
|
|
151
151
|
yield (
|
|
152
152
|
self.label,
|
|
153
153
|
[],
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
154
|
+
(
|
|
155
|
+
AustinMetrics(time=self.own)
|
|
156
|
+
if stats_type in {AustinStatsType.CPU, AustinStatsType.WALL}
|
|
157
|
+
else AustinMetrics(memory=self.own)
|
|
158
|
+
),
|
|
157
159
|
)
|
|
158
160
|
|
|
159
161
|
if self.children:
|
austin/tools/diff.py
CHANGED
|
@@ -30,9 +30,11 @@ from typing import TextIO
|
|
|
30
30
|
from typing import Tuple
|
|
31
31
|
|
|
32
32
|
from austin.events import AustinFrame
|
|
33
|
+
from austin.events import AustinMetrics
|
|
34
|
+
from austin.format.collapsed_stack import InvalidSample
|
|
35
|
+
from austin.format.collapsed_stack import parse_collapsed_stack
|
|
33
36
|
from austin.format.compress import compress
|
|
34
37
|
|
|
35
|
-
|
|
36
38
|
FoldedStack = List[AustinFrame]
|
|
37
39
|
|
|
38
40
|
|
|
@@ -41,7 +43,7 @@ def _similarities(
|
|
|
41
43
|
) -> List[Tuple[Tuple[int, int], float]]:
|
|
42
44
|
"""O(n * log(n)), n = len(x) * len(y)."""
|
|
43
45
|
|
|
44
|
-
def score(a: List[
|
|
46
|
+
def score(a: List[AustinFrame], b: List[AustinFrame]) -> float:
|
|
45
47
|
"""Score two folded stacks."""
|
|
46
48
|
if not len(a) and not len(b):
|
|
47
49
|
return 1
|
|
@@ -90,12 +92,12 @@ def diff(a: TextIO, b: TextIO) -> str:
|
|
|
90
92
|
compress(source, _)
|
|
91
93
|
return _.getvalue()
|
|
92
94
|
|
|
93
|
-
def get_frames(text: str) -> List[Tuple[FoldedStack,
|
|
95
|
+
def get_frames(text: str) -> List[Tuple[FoldedStack, AustinMetrics]]:
|
|
94
96
|
"""Get the folded stacks and metrics from a string of samples."""
|
|
95
97
|
x = []
|
|
96
98
|
for _ in text.splitlines(keepends=False):
|
|
97
99
|
try:
|
|
98
|
-
sample =
|
|
100
|
+
sample = parse_collapsed_stack(_)
|
|
99
101
|
x.append((sample.frames, sample.metrics))
|
|
100
102
|
except InvalidSample:
|
|
101
103
|
continue
|
austin/tools/mojodbg.py
CHANGED
austin/tools/resolve.py
CHANGED
|
@@ -30,7 +30,6 @@ from austin.format.mojo import MojoStreamReader
|
|
|
30
30
|
from austin.format.mojo import MojoString
|
|
31
31
|
from austin.format.mojo import to_varint
|
|
32
32
|
|
|
33
|
-
|
|
34
33
|
__version__ = "0.2.0"
|
|
35
34
|
|
|
36
35
|
|
|
@@ -176,8 +175,10 @@ def resolve_mojo(input: str, output: str) -> None:
|
|
|
176
175
|
filename, scope, line = resolved
|
|
177
176
|
new_value = filename or scope
|
|
178
177
|
|
|
179
|
-
|
|
180
|
-
event
|
|
178
|
+
object.__setattr__(
|
|
179
|
+
event,
|
|
180
|
+
"raw",
|
|
181
|
+
event.raw.replace(event.value.encode(), new_value.encode()),
|
|
181
182
|
)
|
|
182
183
|
|
|
183
184
|
if filename is not None:
|
|
@@ -185,9 +186,11 @@ def resolve_mojo(input: str, output: str) -> None:
|
|
|
185
186
|
|
|
186
187
|
elif isinstance(event, MojoFrame):
|
|
187
188
|
if event.filename.key in maps.lines:
|
|
188
|
-
|
|
189
|
+
object.__setattr__(
|
|
190
|
+
event,
|
|
191
|
+
"raw",
|
|
189
192
|
event.raw[: -len(to_varint(event.line))]
|
|
190
|
-
+ maps.lines[event.filename.key]
|
|
193
|
+
+ maps.lines[event.filename.key],
|
|
191
194
|
)
|
|
192
195
|
|
|
193
196
|
fout.write(event.raw)
|
|
@@ -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
|
|
@@ -15,12 +15,12 @@ Keywords: development,performance,profiling,testing
|
|
|
15
15
|
Classifier: Development Status :: 5 - Production/Stable
|
|
16
16
|
Classifier: Intended Audience :: Developers
|
|
17
17
|
Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
|
|
18
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
19
18
|
Classifier: Programming Language :: Python :: 3.10
|
|
20
19
|
Classifier: Programming Language :: Python :: 3.11
|
|
21
20
|
Classifier: Programming Language :: Python :: 3.12
|
|
22
21
|
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
-
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
23
|
+
Requires-Python: >=3.10
|
|
24
24
|
Requires-Dist: protobuf~=3.12
|
|
25
25
|
Requires-Dist: psutil>=5.7.0
|
|
26
26
|
Requires-Dist: toml~=0.10
|
|
@@ -187,7 +187,7 @@ raw samples by aggregation.
|
|
|
187
187
|
# Compatibility
|
|
188
188
|
|
|
189
189
|
The latest `austin-python` package is tested on Linux, macOS and Windows with
|
|
190
|
-
Python 3.9-3.
|
|
190
|
+
Python 3.9-3.14.
|
|
191
191
|
|
|
192
192
|
|
|
193
193
|
# Documentation
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
austin/__init__.py,sha256=1SCBrKP0MCtuALvQ3tZ8N8-L7pzIzqrMfA3emxtpHkM,974
|
|
2
|
+
austin/aio.py,sha256=Cpq_W1YxL_fUPKBbkupLBHSKi3Sys9OQCcsyq8wYHKY,6594
|
|
3
|
+
austin/base.py,sha256=Xq734VKzE2ggShs6UThsGznBGazg-gCw_nZEJDJKMlM,7553
|
|
4
|
+
austin/cli.py,sha256=--PKm2I4NBBSSSBV5l-YC0mcmLjrvb1FHzxgY4dkby4,7826
|
|
5
|
+
austin/config.py,sha256=PEwdG2Cy8ay1SrFr2N6YfQSZwseYBA1BA5dMfocKR3A,2091
|
|
6
|
+
austin/errors.py,sha256=lCDSTANW_95Az_eDXbn__JRBllB9yw7tGnFo1-tw09Q,1046
|
|
7
|
+
austin/events.py,sha256=6d-rNYG_yXx6VOqyyMWalcBHvstriCZNWWPttDhO_zM,3579
|
|
8
|
+
austin/simple.py,sha256=iTmJGAjSBeUv82SgdMTwdNlAQwuK33RyU8FkBCE8rRg,5579
|
|
9
|
+
austin/stats.py,sha256=xY4BZtiE1Z4Bfucah1da6jwkEREvd1jLfmW4WuCgbP0,11084
|
|
10
|
+
austin/threads.py,sha256=AvcoOkr3FeFAGBcqgXukNIt9xOZUipv7pXMqPIL_Qts,3649
|
|
11
|
+
austin/format/__init__.py,sha256=ZtBJCLpTiZaLFwCN1UbMXLQvYjOSpwmtyGwqgK8yruQ,405
|
|
12
|
+
austin/format/collapsed_stack.py,sha256=vD0CFkUgnRqHaA7dYsh4QYRZubE5IxyhVFnuPa37BSs,7245
|
|
13
|
+
austin/format/compress.py,sha256=by6i79o0pEmGf2Py8edC6RWHVdr5RGsG2FWT6xQjUvk,3797
|
|
14
|
+
austin/format/mojo.py,sha256=mEmMdRreh60p_-rKTCecqVSd7bCxkHssa-pK2_vHKrM,45734
|
|
15
|
+
austin/format/mojo_compress.py,sha256=qhzXsenUThxwAz1lX1Z8xtGgBpPEIbD3cqZz2Iar3ws,3711
|
|
16
|
+
austin/format/speedscope.py,sha256=hIuB1A8wbGFFLQUpZqdM7Tlf4dqFwyy01zFHDybs2Iw,8324
|
|
17
|
+
austin/format/pprof/__init__.py,sha256=E2JJDaw_k_m18OZeITKtepUUIo3n09UHdkpvabSSABc,6804
|
|
18
|
+
austin/format/pprof/__main__.py,sha256=pzWoSmlFrjfmAAq5I4SIIDmxavdLStCnlw3NlBxgyDo,2652
|
|
19
|
+
austin/format/pprof/profile.proto,sha256=fPfBgzXIEmFdrrwgTQWs-9GwKjt-TShRmJq-eaxDx88,8840
|
|
20
|
+
austin/format/pprof/profile_pb2.py,sha256=i_uM8lDExR95yz7qnEVikhpkpNvetKaYzoiH7AbMAt8,28214
|
|
21
|
+
austin/tools/diff.py,sha256=5yrHlniofIeMSqzOUD55l2h6Ps0GqEpfisFRj65gpdQ,5008
|
|
22
|
+
austin/tools/mojodbg.py,sha256=l8Lsa4rTQLXfkau8RyGziyz8ZyPEgacFVXafbHT9FN4,2816
|
|
23
|
+
austin/tools/resolve.py,sha256=bTXkll68nOtXqa0XHW79mp_seAJW8_bhd6ej6wa2_Oc,7816
|
|
24
|
+
austin_python-2.3.0.dist-info/METADATA,sha256=K7EAem3JpnpvVlVY0XQ3WyV-sSfkY5UcAP-hPKLZYF8,7537
|
|
25
|
+
austin_python-2.3.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
|
|
26
|
+
austin_python-2.3.0.dist-info/entry_points.txt,sha256=xmPVC_6UkSOYu7-YMYm3BwIcMFYEhRw0qlF05Cq5gf0,378
|
|
27
|
+
austin_python-2.3.0.dist-info/licenses/AUTHORS,sha256=pARRmpJQYH8iWy_0SonXsxOiV02AFitowle4AtURxUI,45
|
|
28
|
+
austin_python-2.3.0.dist-info/licenses/COPYING,sha256=jOtLnuWt7d5Hsx6XXB2QxzrSe2sWWh3NgMfFRetluQM,35147
|
|
29
|
+
austin_python-2.3.0.dist-info/licenses/LICENSE.md,sha256=edVDhmEiPf2W5lWzqLkrj6EYLSrCeYg9MJy28YsiMFU,34937
|
|
30
|
+
austin_python-2.3.0.dist-info/RECORD,,
|
|
@@ -4,5 +4,6 @@ austin-diff = austin.tools.diff:main
|
|
|
4
4
|
austin2pprof = austin.format.pprof.__main__:main
|
|
5
5
|
austin2speedscope = austin.format.speedscope:main
|
|
6
6
|
austinp-resolve = austin.tools.resolve:main
|
|
7
|
+
mojo-compress = austin.format.mojo_compress:main
|
|
7
8
|
mojo2austin = austin.format.collapsed_stack:main
|
|
8
9
|
mojodbg = austin.tools.mojodbg:main
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
austin/__init__.py,sha256=1SCBrKP0MCtuALvQ3tZ8N8-L7pzIzqrMfA3emxtpHkM,974
|
|
2
|
-
austin/aio.py,sha256=Cpq_W1YxL_fUPKBbkupLBHSKi3Sys9OQCcsyq8wYHKY,6594
|
|
3
|
-
austin/base.py,sha256=wAYVZQi3hcgJA9cVg3mfc3f3E0qwFXmeX7UX6qgn2Pk,7554
|
|
4
|
-
austin/cli.py,sha256=--PKm2I4NBBSSSBV5l-YC0mcmLjrvb1FHzxgY4dkby4,7826
|
|
5
|
-
austin/config.py,sha256=PEwdG2Cy8ay1SrFr2N6YfQSZwseYBA1BA5dMfocKR3A,2091
|
|
6
|
-
austin/errors.py,sha256=lCDSTANW_95Az_eDXbn__JRBllB9yw7tGnFo1-tw09Q,1046
|
|
7
|
-
austin/events.py,sha256=NppffZg_lbKIin2TyvIGgfJXnOmBKzB2e0nQV4_94JA,2160
|
|
8
|
-
austin/simple.py,sha256=iTmJGAjSBeUv82SgdMTwdNlAQwuK33RyU8FkBCE8rRg,5579
|
|
9
|
-
austin/stats.py,sha256=4kVeZ5NJsoSEnogqiDyjAbLeuf5DPiELHfCEbjFb0zk,10990
|
|
10
|
-
austin/threads.py,sha256=AvcoOkr3FeFAGBcqgXukNIt9xOZUipv7pXMqPIL_Qts,3649
|
|
11
|
-
austin/format/__init__.py,sha256=ZtBJCLpTiZaLFwCN1UbMXLQvYjOSpwmtyGwqgK8yruQ,405
|
|
12
|
-
austin/format/collapsed_stack.py,sha256=VCkTJssgcF4IHGP6kzV5dT9AC9Qjbq8eGx9AVOp6cyU,7246
|
|
13
|
-
austin/format/compress.py,sha256=YEceebWzVB7eSCntnJ89_rIcbzBskvZGy86D5QgZli0,3741
|
|
14
|
-
austin/format/mojo.py,sha256=mEcKgaZkVkH4YGVLj4m31_MAPKfYK-BNI7oWyiPub0U,28824
|
|
15
|
-
austin/format/speedscope.py,sha256=RvlFugm0S3ATMf9lmzH8jrX4oBt8PJ0j9EsuXWayOQc,8381
|
|
16
|
-
austin/format/pprof/__init__.py,sha256=DvY1CDnPqBc-GUv9fidtMplshIIXj_HWZ3SlWOJoLxc,6805
|
|
17
|
-
austin/format/pprof/__main__.py,sha256=pzWoSmlFrjfmAAq5I4SIIDmxavdLStCnlw3NlBxgyDo,2652
|
|
18
|
-
austin/format/pprof/profile.proto,sha256=fPfBgzXIEmFdrrwgTQWs-9GwKjt-TShRmJq-eaxDx88,8840
|
|
19
|
-
austin/format/pprof/profile_pb2.py,sha256=i_uM8lDExR95yz7qnEVikhpkpNvetKaYzoiH7AbMAt8,28214
|
|
20
|
-
austin/tools/diff.py,sha256=KnY9kD5mmSKALCNxnSe-dSObC4LgkoxLGnoTiklKa5M,4821
|
|
21
|
-
austin/tools/mojodbg.py,sha256=BGdngj-r1c-GAQDrs_oJ_x4QZxuL4t_OQ194fvJJm7A,2817
|
|
22
|
-
austin/tools/resolve.py,sha256=pEOBiP8FNkaJqDTj3fOK8dx5-EocroqZDXLL3CzCWLY,7677
|
|
23
|
-
austin_python-2.1.2.dist-info/METADATA,sha256=wdZJGpu-uI1aH1NwbKstmCYnYdsXzX2UZ-6c-gFzjTg,7535
|
|
24
|
-
austin_python-2.1.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
25
|
-
austin_python-2.1.2.dist-info/entry_points.txt,sha256=YatHOUQsHQ3WABcrNKPf4RWatQ0CzbR-1ClXjMjZys8,329
|
|
26
|
-
austin_python-2.1.2.dist-info/licenses/AUTHORS,sha256=pARRmpJQYH8iWy_0SonXsxOiV02AFitowle4AtURxUI,45
|
|
27
|
-
austin_python-2.1.2.dist-info/licenses/COPYING,sha256=jOtLnuWt7d5Hsx6XXB2QxzrSe2sWWh3NgMfFRetluQM,35147
|
|
28
|
-
austin_python-2.1.2.dist-info/licenses/LICENSE.md,sha256=edVDhmEiPf2W5lWzqLkrj6EYLSrCeYg9MJy28YsiMFU,34937
|
|
29
|
-
austin_python-2.1.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|