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 CHANGED
@@ -43,7 +43,6 @@ from austin.errors import AustinError
43
43
  from austin.events import AustinMetadata
44
44
  from austin.events import AustinSample
45
45
 
46
-
47
46
  SemVer = Tuple[int, int, int]
48
47
 
49
48
 
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,
@@ -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 '{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 '{sample}'") from None
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 '{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
- AustinMetrics(memory=metrics)
66
- if is_memory
67
- else AustinMetrics(time=metrics),
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
- AustinFileReader(args.input) as fin,
113
+ open(args.input) as _fin,
112
114
  open(args.output or args.input, "w") as fout,
113
115
  ):
114
- compress(fin, fout, args.counts)
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.__args__[0]
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
- MojoEvents.METRIC_TIME
131
- if self.metric_type is MojoMetricType.TIME
132
- else MojoEvents.METRIC_MEMORY
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=tuple(
385
- AustinFrame(
386
- filename=mf.filename.value,
387
- function=mf.scope.value,
388
- line=mf.line,
389
- line_end=mf.line_end,
390
- column=mf.column,
391
- column_end=mf.column_end,
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
- for mf in self._running_sample.frames
394
- )
395
- if self._running_sample.frames
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
- assert self._running_sample is not None, self._running_sample
452
- self._running_sample.frames.append(frame)
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._parse_metric(MojoMetricType.TIME)
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(self, start: int, end: int, highlight: t.Set[int] = set()) -> None: # noqa: B006
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._parse_metric(MojoMetricType.TIME)
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
- HEADER = b"MOJ\x03"
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
- def __init__(self, mojo: io.BytesIO):
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 self._gc:
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()
@@ -33,7 +33,6 @@ from austin.format import Mode
33
33
  from austin.format.pprof.profile_pb2 import Profile
34
34
  from austin.format.pprof.profile_pb2 import Sample
35
35
 
36
-
37
36
  PROCESS_ID_LABEL = "Process ID"
38
37
  THREAD_ID_LABEL = "Thread ID"
39
38
  INTERPRETER_ID_LABEL = "Interpreter ID"
@@ -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
- AustinMetrics(time=self.own)
127
- if stats_type in {AustinStatsType.CPU, AustinStatsType.WALL}
128
- else AustinMetrics(memory=self.own),
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
- AustinMetrics(time=self.own)
155
- if stats_type in {AustinStatsType.CPU, AustinStatsType.WALL}
156
- else AustinMetrics(memory=self.own),
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[Frame], b: List[Frame]) -> float:
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, Metric]]:
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 = Sample.parse(_)
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
@@ -29,7 +29,6 @@ from traceback import print_exc
29
29
  from austin.format.mojo import MojoEvent
30
30
  from austin.format.mojo import MojoStreamReader
31
31
 
32
-
33
32
  __version__ = "0.1.0"
34
33
 
35
34
 
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
- event.raw = event.raw.replace(
180
- event.value.encode(), new_value.encode()
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
- event.raw = (
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.4
1
+ Metadata-Version: 2.5
2
2
  Name: austin-python
3
- Version: 2.1.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
- Requires-Python: >=3.9
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.13.
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,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.27.0
2
+ Generator: hatchling 1.32.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -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,,