@@ -219,6 +219,59 @@ pub(crate) struct TaskDumpEvent {
219219 pub callchain : InternedStackFrames ,
220220}
221221
222+ /// Wire-format event for a sampled memory allocation.
223+ ///
224+ /// Emitted from the consolidator (flush thread) for allocations that tripped
225+ /// the geometric sampling counter. The sampling rate that produced this event
226+ /// lives in the segment metadata, not on each event.
227+ #[ derive( Debug , TraceEvent ) ]
228+ #[ cfg_attr( not( feature = "unstable-events" ) , non_exhaustive) ]
229+ pub struct AllocEvent {
230+ /// Wall-clock timestamp in nanoseconds (monotonic).
231+ #[ traceevent( timestamp) ]
232+ pub timestamp_ns : u64 ,
233+ /// OS thread ID of the allocating thread. Same source as `WorkerParkEvent.tid`
234+ /// and `CpuSampleEvent.tid`. Use this to join against worker park/unpark
235+ /// history to recover worker_id when the allocation happened on a tokio
236+ /// worker thread.
237+ pub tid : u32 ,
238+ /// Allocation size in bytes. The actual size requested by the allocating
239+ /// code; the underlying allocator may have rounded up, but that's not
240+ /// recorded here.
241+ pub size : u64 ,
242+ /// Returned pointer. Only meaningful when liveset tracking is on; otherwise 0.
243+ /// Always present so the schema is stable across track_liveset on/off.
244+ pub addr : u64 ,
245+ /// Stack at the allocation site. Frame 0 is the most-recent caller.
246+ pub callchain : InternedStackFrames ,
247+ }
248+
249+ /// Wire-format event for a deallocation paired with a previously-sampled
250+ /// `AllocEvent`. Only emitted when liveset tracking is on.
251+ ///
252+ /// `size` and `alloc_timestamp_ns` are denormalized from the matching
253+ /// `AllocEvent` so the free stays analytically useful when the corresponding
254+ /// `AllocEvent` has been evicted by trace rotation. See design §3
255+ /// "Why denormalize size and alloc_timestamp_ns?" for the rationale.
256+ #[ derive( Debug , TraceEvent ) ]
257+ #[ cfg_attr( not( feature = "unstable-events" ) , non_exhaustive) ]
258+ pub struct FreeEvent {
259+ /// Wall-clock timestamp in nanoseconds (monotonic) of the free.
260+ #[ traceevent( timestamp) ]
261+ pub timestamp_ns : u64 ,
262+ /// OS thread ID of the freeing thread.
263+ pub tid : u32 ,
264+ /// Pointer that was freed. Matches a previously-seen `AllocEvent.addr`.
265+ pub addr : u64 ,
266+ /// Size of the allocation being freed. Denormalized from the matching
267+ /// `AllocEvent` for rotation robustness.
268+ pub size : u64 ,
269+ /// Monotonic-ns timestamp of the original `AllocEvent`. Allows leak
270+ /// analysis to bucket frees by generation without needing the
271+ /// `AllocEvent` in the same (unrotated) trace.
272+ pub alloc_timestamp_ns : u64 ,
273+ }
274+
222275/// Wire-format event for a wake notification.
223276#[ derive( Debug , TraceEvent ) ]
224277pub struct WakeEventEvent {
@@ -291,6 +344,8 @@ pub(crate) enum TelemetryEventRef<'a> {
291344 TaskTerminate ( TaskTerminateEventRef < ' a > ) ,
292345 CpuSample ( CpuSampleEventRef < ' a > ) ,
293346 TaskDump ( TaskDumpEventRef < ' a > ) ,
347+ Alloc ( AllocEventRef < ' a > ) ,
348+ Free ( FreeEventRef < ' a > ) ,
294349 WakeEvent ( WakeEventEventRef < ' a > ) ,
295350 SegmentMetadata ( SegmentMetadataEventRef < ' a > ) ,
296351 ClockSync ( ClockSyncEventRef < ' a > ) ,
@@ -311,6 +366,8 @@ impl<'a> TelemetryEventRef<'a> {
311366 Self :: TaskTerminate ( e) => Some ( e. timestamp_ns ) ,
312367 Self :: CpuSample ( e) => Some ( e. timestamp_ns ) ,
313368 Self :: TaskDump ( e) => Some ( e. timestamp_ns ) ,
369+ Self :: Alloc ( e) => Some ( e. timestamp_ns ) ,
370+ Self :: Free ( e) => Some ( e. timestamp_ns ) ,
314371 Self :: WakeEvent ( e) => Some ( e. timestamp_ns ) ,
315372 Self :: SegmentMetadata ( e) => Some ( e. timestamp_ns ) ,
316373 Self :: ClockSync ( e) => Some ( e. timestamp_ns ) ,
@@ -365,6 +422,12 @@ pub(crate) fn decode_ref<'a>(
365422 "TaskDumpEvent" => {
366423 TelemetryEventRef :: TaskDump ( TaskDumpEvent :: decode ( timestamp_ns, fields, field_defs) ?)
367424 }
425+ "AllocEvent" => {
426+ TelemetryEventRef :: Alloc ( AllocEvent :: decode ( timestamp_ns, fields, field_defs) ?)
427+ }
428+ "FreeEvent" => {
429+ TelemetryEventRef :: Free ( FreeEvent :: decode ( timestamp_ns, fields, field_defs) ?)
430+ }
368431 "WakeEventEvent" => {
369432 TelemetryEventRef :: WakeEvent ( WakeEventEvent :: decode ( timestamp_ns, fields, field_defs) ?)
370433 }
@@ -453,6 +516,23 @@ pub(crate) fn to_owned_event(
453516 . expect ( "stack pool entry must exist for TaskDump callchain" )
454517 . to_vec ( ) ,
455518 } ,
519+ TelemetryEventRef :: Alloc ( e) => TelemetryEvent :: Alloc {
520+ timestamp_nanos : e. timestamp_ns ,
521+ tid : e. tid ,
522+ size : e. size ,
523+ addr : e. addr ,
524+ callchain : stack_pool
525+ . get ( e. callchain )
526+ . expect ( "stack pool entry must exist for AllocEvent callchain" )
527+ . to_vec ( ) ,
528+ } ,
529+ TelemetryEventRef :: Free ( e) => TelemetryEvent :: Free {
530+ timestamp_nanos : e. timestamp_ns ,
531+ tid : e. tid ,
532+ addr : e. addr ,
533+ size : e. size ,
534+ alloc_timestamp_nanos : e. alloc_timestamp_ns ,
535+ } ,
456536 TelemetryEventRef :: WakeEvent ( e) => TelemetryEvent :: WakeEvent {
457537 timestamp_nanos : e. timestamp_ns ,
458538 waker_task_id : e. waker_task_id ,
@@ -473,3 +553,74 @@ pub(crate) fn to_owned_event(
473553 } ,
474554 }
475555}
556+
557+ #[ cfg( test) ]
558+ mod tests {
559+ use super :: * ;
560+ use dial9_trace_format:: encoder:: Encoder ;
561+
562+ #[ test]
563+ fn alloc_event_round_trip ( ) {
564+ let mut enc = Encoder :: new_to ( Vec :: new ( ) ) . unwrap ( ) ;
565+ let callchain = enc. intern_stack_frames ( & [ 0x1000 , 0x2000 , 0x3000 ] ) . unwrap ( ) ;
566+ enc. write_infallible ( & AllocEvent {
567+ timestamp_ns : 123_456_789 ,
568+ tid : 42 ,
569+ size : 4096 ,
570+ addr : 0xDEAD_BEEF_CAFE ,
571+ callchain,
572+ } ) ;
573+ let buf = enc. into_inner ( ) ;
574+
575+ let events = decode_events ( & buf) . unwrap ( ) ;
576+ assert_eq ! ( events. len( ) , 1 ) ;
577+ match & events[ 0 ] {
578+ TelemetryEvent :: Alloc {
579+ timestamp_nanos,
580+ tid,
581+ size,
582+ addr,
583+ callchain,
584+ } => {
585+ assert_eq ! ( * timestamp_nanos, 123_456_789 ) ;
586+ assert_eq ! ( * tid, 42 ) ;
587+ assert_eq ! ( * size, 4096 ) ;
588+ assert_eq ! ( * addr, 0xDEAD_BEEF_CAFE ) ;
589+ assert_eq ! ( callchain, & [ 0x1000 , 0x2000 , 0x3000 ] ) ;
590+ }
591+ other => panic ! ( "expected Alloc event, got {other:?}" ) ,
592+ }
593+ }
594+
595+ #[ test]
596+ fn free_event_round_trip ( ) {
597+ let mut enc = Encoder :: new_to ( Vec :: new ( ) ) . unwrap ( ) ;
598+ enc. write_infallible ( & FreeEvent {
599+ timestamp_ns : 999_000_000 ,
600+ tid : 7 ,
601+ addr : 0xCAFE_BABE ,
602+ size : 2048 ,
603+ alloc_timestamp_ns : 100_000_000 ,
604+ } ) ;
605+ let buf = enc. into_inner ( ) ;
606+
607+ let events = decode_events ( & buf) . unwrap ( ) ;
608+ assert_eq ! ( events. len( ) , 1 ) ;
609+ match & events[ 0 ] {
610+ TelemetryEvent :: Free {
611+ timestamp_nanos,
612+ tid,
613+ addr,
614+ size,
615+ alloc_timestamp_nanos,
616+ } => {
617+ assert_eq ! ( * timestamp_nanos, 999_000_000 ) ;
618+ assert_eq ! ( * tid, 7 ) ;
619+ assert_eq ! ( * addr, 0xCAFE_BABE ) ;
620+ assert_eq ! ( * size, 2048 ) ;
621+ assert_eq ! ( * alloc_timestamp_nanos, 100_000_000 ) ;
622+ }
623+ other => panic ! ( "expected Free event, got {other:?}" ) ,
624+ }
625+ }
626+ }
0 commit comments