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