lib/alloc/observe/src/protocol.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

  1 const std = @import("std");
  2 const build_options = @import("build_options");
  3 
  4 /// A compile-time boolean indicating whether allocation observation is active.
  5 /// Build options determine this setting: standalone builds default to `false`
  6 /// unless configured with `-Denabled=true`, while monorepo consumers typically
  7 /// forward `-Dobserve-allocations=true` through build scripts. When this flag
  8 /// is `false`, installation succeeds inertly, identifier queries return zero,
  9 /// suppression checks return `false`, and operations such as span tracking or
 10 /// suppression perform no work. Public data structures and types differ in
 11 /// concrete field layout between enabled and disabled builds, so they offer no
 12 /// layout equivalence guarantee across build configurations.
 13 pub const enabled: bool = build_options.allocation_observation_enabled;
 14 
 15 /// Classifies the origin of an observed allocation event through tags: arena,
 16 /// bump, buffer_first, fixed_buffer, debug, pool, process, phase, limit,
 17 /// boundary, sys_memory, and custom. Each tag labels the reporting component
 18 /// rather than guaranteeing how that component manages memory or whether its
 19 /// reported operations cover all process allocations. For instance, an
 20 /// allocator tagged as an arena describes its own interface layer rather than
 21 /// certifying backing page reclamation or absence of uninstrumented memory
 22 /// traffic.
 23 pub const Producer = enum(u8) {
 24     arena,
 25     bump,
 26     buffer_first,
 27     fixed_buffer,
 28     debug,
 29     pool,
 30     process,
 31     phase,
 32     limit,
 33     boundary,
 34     sys_memory,
 35     custom,
 36 };
 37 
 38 /// Identifies raw allocator virtual table operations (alloc, resize, remap, and
 39 /// free) alongside lifecycle transitions. Values represent logical actions
 40 /// reported by an allocator interface rather than physical memory changes in
 41 /// the operating system. A successful free event records that an allocator
 42 /// finished its logical deallocation procedure, which does not guarantee that
 43 /// underlying virtual address pages or physical bytes were returned to the
 44 /// kernel.
 45 pub const Operation = enum(u8) {
 46     alloc,
 47     resize,
 48     remap,
 49     free,
 50     lifecycle,
 51 };
 52 
 53 /// Specifies how a lifecycle event affects allocations belonging to the
 54 /// emitting producer: none indicates a standard allocator operation, invalidate
 55 /// marks earlier allocations as having lost validity, and end signals that the
 56 /// producer owner lifetime has completed. The emitting allocator supplies this
 57 /// disposition directly. The observation protocol forwards the value to the
 58 /// installed sink without validating owner state or enforcing lifetime
 59 /// guarantees.
 60 pub const LifecycleDisposition = enum(u8) {
 61     none,
 62     invalidate,
 63     end,
 64 };
 65 
 66 /// Describes the motive for an allocator lifecycle transition: none, reset,
 67 /// clear_and_free, clear_retaining_current, clear_retaining_largest,
 68 /// clear_retaining_capacity, clear_retaining_capacity_limit, and deinit. Each
 69 /// value represents an owner-supplied description of its action. The
 70 /// enumeration conveys descriptive intent rather than implementing memory
 71 /// retention, so actual buffer preservation or release depends entirely on the
 72 /// owner allocator implementation.
 73 pub const LifecycleReason = enum(u8) {
 74     none,
 75     reset,
 76     clear_and_free,
 77     clear_retaining_current,
 78     clear_retaining_largest,
 79     clear_retaining_capacity,
 80     clear_retaining_capacity_limit,
 81     deinit,
 82 };
 83 
 84 /// Identifies an allocation source by recording a producer identifier, a
 85 /// producer kind, an owner-supplied generation number, and an owner-supplied
 86 /// cookie value. When observation is enabled, starting a span requires a
 87 /// nonzero producer identifier, and specifying a nonzero generation number
 88 /// requires a nonzero cookie. The observation layer stores and forwards these
 89 /// values as provided by the caller without automatically tracking lifetime
 90 /// state or advancing generation numbers.
 91 pub const Identity = struct {
 92     producer_id: u64,
 93     producer: Producer,
 94     generation: u64,
 95     owner_cookie: usize,
 96 
 97     /// Derives an identity whose cookie is computed from a nonzero producer
 98     /// identifier rather than a memory address. The constructor requires a
 99     /// nonzero producer identifier, folds the identifier into a `usize` by
100     /// XORing the high word on narrower targets and truncating, and substitutes
101     /// one whenever the folded result is zero. Deriving the cookie from an
102     /// identifier allows owner structs to move in memory without altering their
103     /// identity. This derivation does not guarantee collision-free cookie
104     /// values and does not update existing allocator handles that continue to
105     /// borrow old owner addresses.
106     pub fn movable(
107         producer_id: u64,
108         producer: Producer,
109         generation: u64,
110     ) Identity {
111         std.debug.assert(producer_id != 0);
112         const folded = if (comptime @bitSizeOf(usize) < @bitSizeOf(u64))
113             producer_id ^ (producer_id >> @bitSizeOf(usize))
114         else
115             producer_id;
116         const cookie: usize = @truncate(folded);
117         return .{
118             .producer_id = producer_id,
119             .producer = producer,
120             .generation = generation,
121             .owner_cookie = if (cookie == 0) 1 else cookie,
122         };
123     }
124 };
125 
126 /// An `Outcome` carries the result of an operation as supplied by the producer.
127 /// It contains a boolean succeeded flag and a usize address that defaults to
128 /// zero. This value represents an operation-specific result rather than a
129 /// simple return without an exception, and it provides no proof of physical
130 /// reclamation. Within the generic `ObservedAllocator`, a successful allocation
131 /// or remap produces the resulting address, a successful resize produces the
132 /// old address, a deallocation produces the old address, and an operation
133 /// failure produces zero. A caller invoking a lifecycle span may omit the
134 /// address so that it defaults to zero, but the protocol does not enforce that
135 /// a lifecycle result address must be zero. The `beginLifecycle` function
136 /// zeroes the old address and length metadata when opening the span, while
137 /// finish accepts the address supplied in the final Outcome.
138 pub const Outcome = struct {
139     address: usize = 0,
140     succeeded: bool,
141 };
142 
143 /// An `Event` is a by-value notification delivered at span finish. This
144 /// notification conveys caller-supplied data rather than proof of actual
145 /// storage. Numeric addresses are diagnostic, providing no ownership,
146 /// capability, or dereference guarantee after an allocation expires. Ordinary
147 /// spans set lifecycle tags to none.
148 ///
149 /// The exact fields and their meanings are:
150 ///
151 /// - `operation_id`: operation identifier assigned at begin.
152 /// - `parent_operation_id`: identifier of the enclosing span or context on the
153 ///   current thread, or zero if none exists.
154 /// - `producer_id`: numeric identifier of the producer.
155 /// - `producer`: kind of producer.
156 /// - `generation`: owner-supplied lifetime number.
157 /// - `owner_cookie`: owner association value.
158 /// - `operation`: executed allocation or lifecycle operation.
159 /// - `lifecycle_disposition`: lifecycle disposition tag, set to none for
160 ///   ordinary spans.
161 /// - `lifecycle_reason`: lifecycle reason tag, set to none for ordinary spans.
162 /// - `address`: result address supplied to finish.
163 /// - `old_address`: prior address before the operation.
164 /// - `len`: requested or new length.
165 /// - `old_len`: prior length before the operation.
166 /// - `alignment`: alignment in bytes.
167 /// - `return_address`: caller instruction address.
168 /// - `succeeded`: producer result flag indicating success or failure.
169 ///
170 /// When `beginLifecycle` opens a span, it sets old_address, old_len, and len to
171 /// zero, and alignment to one byte. That initialization does not dictate the
172 /// final result address supplied to finish. Nested spans on the same thread
173 /// deliver inner spans before outer spans, so operation identifiers do not
174 /// increase in delivery order. Concurrent callbacks have no globally serialized
175 /// order across threads. A silent context reserves an operation identifier but
176 /// emits no event, so a parent operation identifier can lack a matching event.
177 pub const Event = struct {
178     operation_id: u64,
179     parent_operation_id: u64,
180     producer_id: u64,
181     producer: Producer,
182     generation: u64,
183     owner_cookie: usize,
184     operation: Operation,
185     lifecycle_disposition: LifecycleDisposition,
186     lifecycle_reason: LifecycleReason,
187     address: usize,
188     old_address: usize,
189     len: usize,
190     old_len: usize,
191     alignment: usize,
192     return_address: usize,
193     succeeded: bool,
194 };
195 
196 /// A delivery target holding a type-erased context pointer and a function
197 /// pointer that accepts an event by value and returns `void`. Delivery occurs
198 /// synchronously on the thread finishing an observed span, directly in the
199 /// caller execution path with no intermediate queue. Because events from
200 /// distinct threads can arrive concurrently, the callback implementation must
201 /// synchronize access to shared state. The sink provides no internal buffer,
202 /// capacity management, dropping strategy, or error reporting mechanism. The
203 /// caller must guarantee that the `Sink` struct storage itself, the callback
204 /// context, and the callback code remain stable and valid in memory until
205 /// session shutdown returns.
206 pub const Sink = struct {
207     context: *anyopaque,
208     record: *const fn (context: *anyopaque, event: Event) void,
209 };
210 
211 const EnabledSpan = struct {
212     sink: *const Sink,
213     operation_id: u64,
214     parent_operation_id: u64,
215     producer_id: u64,
216     producer: Producer,
217     generation: u64,
218     owner_cookie: usize,
219     operation: Operation,
220     lifecycle_disposition: LifecycleDisposition,
221     lifecycle_reason: LifecycleReason,
222     old_address: usize,
223     old_len: usize,
224     len: usize,
225     alignment: usize,
226     return_address: usize,
227     active: bool = true,
228 
229     /// An inactive or disabled token does nothing when finish is called. An
230     /// active token must finish on its originating thread in reverse nesting
231     /// order. Completes the observed span and delivers an event carrying the
232     /// supplied outcome to the sink captured at initiation. During delivery,
233     /// observation is automatically suppressed on the calling thread to prevent
234     /// recursive callbacks. Once delivery concludes, this method restores the
235     /// previous thread parent operation ID and decrements the shared active
236     /// operation counter. Repeated calls on the same token after finish do
237     /// nothing, and a copied active token is not independently finishable.
238     /// Callers must finish all same-thread tokens before `Session.deinit`,
239     /// because shutdown cannot progress while active tokens remain.
240     pub fn finish(self: *EnabledSpan, outcome: Outcome) void {
241         if (!self.active) return;
242         std.debug.assert(self.active);
243         std.debug.assert(current_operation_id == self.operation_id);
244         callback_depth += 1;
245         self.sink.record(self.sink.context, .{
246             .operation_id = self.operation_id,
247             .parent_operation_id = self.parent_operation_id,
248             .producer_id = self.producer_id,
249             .producer = self.producer,
250             .generation = self.generation,
251             .owner_cookie = self.owner_cookie,
252             .operation = self.operation,
253             .lifecycle_disposition = self.lifecycle_disposition,
254             .lifecycle_reason = self.lifecycle_reason,
255             .address = outcome.address,
256             .old_address = self.old_address,
257             .len = self.len,
258             .old_len = self.old_len,
259             .alignment = self.alignment,
260             .return_address = self.return_address,
261             .succeeded = outcome.succeeded,
262         });
263         callback_depth -= 1;
264         current_operation_id = self.parent_operation_id;
265         self.active = false;
266         _ = active_operations.fetchSub(1, .release);
267     }
268 };
269 
270 const DisabledSpan = struct {
271     /// An inactive or disabled token does nothing when finish is called. An
272     /// active token must finish on its originating thread in reverse nesting
273     /// order. Completes the observed span and delivers an event carrying the
274     /// supplied outcome to the sink captured at initiation. During delivery,
275     /// observation is automatically suppressed on the calling thread to prevent
276     /// recursive callbacks. Once delivery concludes, this method restores the
277     /// previous thread parent operation ID and decrements the shared active
278     /// operation counter. Repeated calls on the same token after finish do
279     /// nothing, and a copied active token is not independently finishable.
280     /// Callers must finish all same-thread tokens before `Session.deinit`,
281     /// because shutdown cannot progress while active tokens remain.
282     pub inline fn finish(_: *DisabledSpan, outcome: Outcome) void {
283         _ = outcome;
284     }
285 };
286 
287 /// A thread-bound tracking token representing an active observation interval.
288 /// When created while observation is enabled, unsuppressed, and backed by an
289 /// installed sink, an active span records the current thread parent operation
290 /// ID, updates the thread current operation ID, increments the active operation
291 /// count, and captures the sink reference. Active spans must be finished
292 /// strictly in last-in, first-out order on their originating thread, and each
293 /// token must be owned and finished by a single authoritative holder. When
294 /// observation is disabled, suppressed, or running without an installed sink,
295 /// the token is inert. Concrete field layouts differ between enabled and
296 /// disabled builds.
297 pub const Span = if (enabled) EnabledSpan else DisabledSpan;
298 
299 const EnabledContext = struct {
300     operation_id: u64,
301     parent_operation_id: u64,
302     active: bool = true,
303 
304     /// Returns the stored operation identifier for this context, which remains
305     /// readable after `finish` marks the context inactive. This method returns
306     /// zero only if the context was created inactive or allocation observation
307     /// is disabled.
308     pub fn id(self: EnabledContext) u64 {
309         return self.operation_id;
310     }
311 
312     /// Returns the parent operation identifier active on the originating thread
313     /// when this context began, which remains readable after `finish` marks the
314     /// context inactive. This method returns zero if no parent operation was
315     /// active at creation, the context was created inactive, or allocation
316     /// observation is disabled.
317     pub fn parentId(self: EnabledContext) u64 {
318         return self.parent_operation_id;
319     }
320 
321     /// Concludes the silent context, restoring the enclosing parent operation
322     /// identifier on the originating thread and decrementing the shared active
323     /// operation count without delivering an event. Tokens must be finished in
324     /// reverse order of creation on their originating thread. Calling finish
325     /// more than once on the same token is safe and produces no further state
326     /// changes, but copies of an active token must not be finished
327     /// independently.
328     pub fn finish(self: *EnabledContext) void {
329         if (!self.active) return;
330         std.debug.assert(current_operation_id == self.operation_id);
331         current_operation_id = self.parent_operation_id;
332         self.active = false;
333         _ = active_operations.fetchSub(1, .release);
334     }
335 };
336 
337 const DisabledContext = struct {
338     /// Returns the stored operation identifier for this context, which remains
339     /// readable after `finish` marks the context inactive. This method returns
340     /// zero only if the context was created inactive or allocation observation
341     /// is disabled.
342     pub inline fn id(_: DisabledContext) u64 {
343         return 0;
344     }
345 
346     /// Returns the parent operation identifier active on the originating thread
347     /// when this context began, which remains readable after `finish` marks the
348     /// context inactive. This method returns zero if no parent operation was
349     /// active at creation, the context was created inactive, or allocation
350     /// observation is disabled.
351     pub inline fn parentId(_: DisabledContext) u64 {
352         return 0;
353     }
354 
355     /// Concludes the silent context, restoring the enclosing parent operation
356     /// identifier on the originating thread and decrementing the shared active
357     /// operation count without delivering an event. Tokens must be finished in
358     /// reverse order of creation on their originating thread. Calling finish
359     /// more than once on the same token is safe and produces no further state
360     /// changes, but copies of an active token must not be finished
361     /// independently.
362     pub inline fn finish(_: *DisabledContext) void {}
363 };
364 
365 /// A silent thread-bound nesting token that reserves an operation identifier
366 /// and participates in thread hierarchy tracking without publishing an event
367 /// upon completion. An active context updates the thread current operation ID,
368 /// links to the prior parent, and increments the shared active operation
369 /// counter. Callers must finish contexts in last-in, first-out order on the
370 /// originating thread, treating each token as having a single authoritative
371 /// owner. When observation is disabled, suppressed, or executed without an
372 /// installed sink, the returned context is inactive and reports an identifier
373 /// of zero.
374 pub const Context = if (enabled) EnabledContext else DisabledContext;
375 
376 const EnabledSuppression = struct {
377     active: bool = true,
378 
379     /// Restores observation depth by decrementing the current thread
380     /// suppression counter by one level. The token must be deinitialized on its
381     /// originating thread. Calling this method repeatedly on the same token is
382     /// an idempotent no-op after the first invocation, but duplicate copies of
383     /// an active token must not be deinitialized independently.
384     pub fn deinit(self: *EnabledSuppression) void {
385         if (!self.active) return;
386         std.debug.assert(callback_depth > 0);
387         callback_depth -= 1;
388         self.active = false;
389     }
390 };
391 
392 const DisabledSuppression = struct {
393     /// Restores observation depth by decrementing the current thread
394     /// suppression counter by one level. The token must be deinitialized on its
395     /// originating thread. Calling this method repeatedly on the same token is
396     /// an idempotent no-op after the first invocation, but duplicate copies of
397     /// an active token must not be deinitialized independently.
398     pub inline fn deinit(_: *DisabledSuppression) void {}
399 };
400 
401 /// A nesting depth token that suspends the creation of new spans and silent
402 /// contexts on the current thread while active. `Suppression` does not prevent
403 /// underlying memory allocations from proceeding, does not mute observation on
404 /// other threads, and does not cancel or modify spans that were already
405 /// initiated prior to suppression.
406 pub const Suppression = if (enabled)
407     EnabledSuppression
408 else
409     DisabledSuppression;
410 
411 /// Represents an installed observation interval, managing session duration
412 /// while borrowing caller-owned sink and context storage. Callers must ensure
413 /// sink and context storage remain allocated at stable memory addresses
414 /// throughout the session. The session must be explicitly terminated by calling
415 /// deinit exactly once, as no automatic teardown is performed on drop. In
416 /// disabled builds, the session is inert. Callers must serialize session
417 /// lifecycle transitions externally, because the protocol does not support
418 /// overlapping the shutdown of an old session with the installation of a new
419 /// one.
420 pub const Session = struct {
421     sink: if (enabled) ?*const Sink else void =
422         if (enabled) null else {},
423     active: if (enabled) bool else void =
424         if (enabled) false else {},
425     producer_id_floor: if (enabled) u64 else void =
426         if (enabled) 0 else {},
427 
428     /// Terminates an active session by unregistering the sink and spinning
429     /// until the shared count of active spans and contexts drains to zero. The
430     /// spin loop contains no timeout or cancellation path, requiring all
431     /// concurrent operations to finish. Callers must invoke deinit outside
432     /// callbacks and outside active suppression scopes (which trigger
433     /// assertions if violated), and must ensure no outstanding spans or
434     /// contexts remain open on the calling thread, or shutdown will spin
435     /// indefinitely. Callers must also avoid holding locks required by
436     /// in-flight callbacks. `Sink` memory and callback context pointers must
437     /// remain valid and stable until this function returns. An enabled session
438     /// must be deinitialized exactly once. In disabled builds, this call is an
439     /// inert no-op.
440     pub fn deinit(self: *Session) void {
441         if (comptime !enabled) return;
442         std.debug.assert(callback_depth == 0);
443         std.debug.assert(self.active);
444         const sink = self.sink.?;
445         const address = @intFromPtr(sink);
446         const previous = active_sink.cmpxchgStrong(
447             address,
448             0,
449             .acq_rel,
450             .acquire,
451         );
452         std.debug.assert(previous == null);
453         while (active_operations.load(.acquire) != 0) {
454             std.atomic.spinLoopHint();
455         }
456         self.active = false;
457         self.sink = null;
458     }
459 
460     /// Reports the next producer identifier captured during session
461     /// installation under the registry lock, or zero when observation is
462     /// disabled. This boundary value separates producers registered prior to
463     /// installation from those registered subsequently. Falling at or above
464     /// this threshold indicates registration after the session began, but does
465     /// not prove that an emitter event history is complete, because thread
466     /// suppression or incomplete sink coverage may have omitted events.
467     pub fn producerIdFloor(self: *const Session) u64 {
468         if (comptime !enabled) return 0;
469         return self.producer_id_floor;
470     }
471 };
472 
473 var active_sink = std.atomic.Value(usize).init(0);
474 var active_operations = std.atomic.Value(usize).init(0);
475 var next_operation_id = std.atomic.Value(u64).init(1);
476 var next_producer_id = std.atomic.Value(u64).init(1);
477 var producer_registry_lock: std.atomic.Mutex = .unlocked;
478 threadlocal var current_operation_id: u64 = 0;
479 threadlocal var callback_depth: u8 = 0;
480 
481 /// Installs a borrowed sink and context as the active observation target,
482 /// returning an active session handle. If an enabled sink is already installed,
483 /// the function returns AlreadyInstalled. In disabled builds, installation
484 /// succeeds inertly. The caller must guarantee that sink storage and context
485 /// pointers remain valid and stationary, because registered producers on other
486 /// threads may deliver callbacks before install returns. The function must be
487 /// called outside existing callbacks and suppression scopes. Callers must
488 /// serialize session lifecycles externally, ensuring that prior session
489 /// teardown completes fully before a new session is installed.
490 pub fn install(sink: *const Sink) error{AlreadyInstalled}!Session {
491     if (comptime !enabled) {
492         return .{};
493     }
494     std.debug.assert(callback_depth == 0);
495     lockProducerRegistry();
496     defer producer_registry_lock.unlock();
497     const address = @intFromPtr(sink);
498     std.debug.assert(address != 0);
499     const producer_id_floor = next_producer_id.load(.acquire);
500     if (active_sink.cmpxchgStrong(
501         0,
502         address,
503         .acq_rel,
504         .acquire,
505     ) != null) {
506         return error.AlreadyInstalled;
507     }
508     return .{
509         .sink = sink,
510         .active = true,
511         .producer_id_floor = producer_id_floor,
512     };
513 }
514 
515 /// Allocates a nonzero, monotonically increasing identifier for an allocator
516 /// producer when observation is enabled, or returns zero in disabled builds.
517 /// Identifiers are allocated independently of active sessions, protected by a
518 /// registry lock that serializes allocation with session floor sampling.
519 /// Exhaustion of the identifier space causes a panic rather than a recoverable
520 /// error. Generated values are unique only within the linked module instance,
521 /// providing no global uniqueness across separate processes or independently
522 /// loaded module binaries.
523 pub inline fn producerId() u64 {
524     if (comptime !enabled) return 0;
525     lockProducerRegistry();
526     defer producer_registry_lock.unlock();
527     return takeId(&next_producer_id);
528 }
529 
530 /// Returns the operation identifier currently active on the calling thread,
531 /// which belongs to either an active span or a silent context, or zero if no
532 /// operation is active or observation is disabled. Active suppression does not
533 /// clear an already established identifier.
534 pub inline fn currentOperationId() u64 {
535     if (comptime !enabled) return 0;
536     return current_operation_id;
537 }
538 
539 /// Reports whether observation is currently suspended on the calling thread due
540 /// to an active callback execution or an explicit suppression scope, returning
541 /// false in disabled builds. This query reflects thread suppression depth
542 /// rather than indicating whether a sink is installed.
543 pub inline fn suppressed() bool {
544     if (comptime !enabled) return false;
545     return callback_depth != 0;
546 }
547 
548 /// Initiates an ordinary observation span with generation and owner cookie
549 /// defaulted to zero, forwarding previous address, previous length, requested
550 /// length, byte alignment, and caller return address to the resulting token.
551 /// When observation is enabled, the caller must supply a nonzero producer
552 /// identifier even if observation is currently suppressed or no sink is
553 /// installed. The caller must finish the returned token with an outcome to
554 /// conclude the operation.
555 pub inline fn begin(
556     producer_id: u64,
557     producer: Producer,
558     operation: Operation,
559     old_address: usize,
560     old_len: usize,
561     len: usize,
562     alignment: usize,
563     return_address: usize,
564 ) Span {
565     return beginOwned(
566         .{
567             .producer_id = producer_id,
568             .producer = producer,
569             .generation = 0,
570             .owner_cookie = 0,
571         },
572         operation,
573         old_address,
574         old_len,
575         len,
576         alignment,
577         return_address,
578     );
579 }
580 
581 /// Initiates an ordinary observation span using an explicit caller identity,
582 /// forwarding prior and requested buffer sizes, addresses, alignment, and
583 /// return address arguments. In enabled builds, this function asserts identity
584 /// validity rules by requiring a nonzero producer identifier and requiring a
585 /// nonzero cookie whenever the generation number is nonzero, but it performs no
586 /// automatic generation advancement or lifecycle state validation. If
587 /// observation is suppressed or no sink is present, the function returns an
588 /// inactive span. In disabled builds, it returns an inert span.
589 pub inline fn beginOwned(
590     identity: Identity,
591     operation: Operation,
592     old_address: usize,
593     old_len: usize,
594     len: usize,
595     alignment: usize,
596     return_address: usize,
597 ) Span {
598     return beginEvent(
599         identity,
600         operation,
601         .none,
602         .none,
603         old_address,
604         old_len,
605         len,
606         alignment,
607         return_address,
608     );
609 }
610 
611 /// Initiates a lifecycle observation span for an allocator transition,
612 /// requiring disposition and reason values other than none even though disabled
613 /// builds ultimately discard the event. The span records a lifecycle operation
614 /// kind, sets prior address, prior length, and requested length to zero, sets
615 /// alignment to one, and forwards the supplied identity and return address. The
616 /// caller supplies the eventual outcome upon finishing the span. The
617 /// observation layer records this transition without modifying or managing
618 /// owner lifetime state.
619 pub inline fn beginLifecycle(
620     identity: Identity,
621     disposition: LifecycleDisposition,
622     reason: LifecycleReason,
623     return_address: usize,
624 ) Span {
625     std.debug.assert(disposition != .none);
626     std.debug.assert(reason != .none);
627     return beginEvent(
628         identity,
629         .lifecycle,
630         disposition,
631         reason,
632         0,
633         0,
634         0,
635         1,
636         return_address,
637     );
638 }
639 
640 inline fn beginEvent(
641     identity: Identity,
642     operation: Operation,
643     lifecycle_disposition: LifecycleDisposition,
644     lifecycle_reason: LifecycleReason,
645     old_address: usize,
646     old_len: usize,
647     len: usize,
648     alignment: usize,
649     return_address: usize,
650 ) Span {
651     if (comptime !enabled) {
652         return .{};
653     }
654     std.debug.assert(identity.producer_id != 0);
655     if (identity.owner_cookie == 0) {
656         std.debug.assert(identity.generation == 0);
657     }
658     if (callback_depth != 0) return inactiveSpan();
659     _ = active_operations.fetchAdd(1, .acquire);
660     const sink_address = active_sink.load(.acquire);
661     if (sink_address == 0) {
662         _ = active_operations.fetchSub(1, .release);
663         return inactiveSpan();
664     }
665     const operation_id = takeId(&next_operation_id);
666     const parent_operation_id = current_operation_id;
667     current_operation_id = operation_id;
668     return .{
669         .sink = @ptrFromInt(sink_address),
670         .operation_id = operation_id,
671         .parent_operation_id = parent_operation_id,
672         .producer_id = identity.producer_id,
673         .producer = identity.producer,
674         .generation = identity.generation,
675         .owner_cookie = identity.owner_cookie,
676         .operation = operation,
677         .lifecycle_disposition = lifecycle_disposition,
678         .lifecycle_reason = lifecycle_reason,
679         .old_address = old_address,
680         .old_len = old_len,
681         .len = len,
682         .alignment = alignment,
683         .return_address = return_address,
684     };
685 }
686 
687 /// Reserves an operation identifier and establishes thread nesting without
688 /// publishing an event when finished. In enabled builds with an installed sink
689 /// and no active suppression, the context increments the active operation count
690 /// and links into the thread parent stack. In all other circumstances, it
691 /// returns an inert token with an identifier of zero. Callers must finish the
692 /// returned context on its originating thread, as active contexts contribute to
693 /// the session drain count during shutdown.
694 pub inline fn beginContext() Context {
695     if (comptime !enabled) return .{};
696     if (callback_depth != 0) return inactiveContext();
697     _ = active_operations.fetchAdd(1, .acquire);
698     if (active_sink.load(.acquire) == 0) {
699         _ = active_operations.fetchSub(1, .release);
700         return inactiveContext();
701     }
702     const operation_id = takeId(&next_operation_id);
703     const parent_operation_id = current_operation_id;
704     current_operation_id = operation_id;
705     return .{
706         .operation_id = operation_id,
707         .parent_operation_id = parent_operation_id,
708     };
709 }
710 
711 /// Increments the observation suppression depth counter on the current thread,
712 /// asserting that depth does not overflow an eight-bit integer, and returns a
713 /// suppression token that decrements the counter upon deinitialization. In
714 /// disabled builds, the call returns an inert token. Active suppression
715 /// prevents the creation of new spans and silent contexts on the calling thread
716 /// without altering spans that are already in progress.
717 pub inline fn suppress() Suppression {
718     if (comptime !enabled) return .{};
719     std.debug.assert(callback_depth < std.math.maxInt(u8));
720     callback_depth += 1;
721     return .{};
722 }
723 
724 fn inactiveSpan() EnabledSpan {
725     return .{
726         .sink = undefined,
727         .operation_id = 0,
728         .parent_operation_id = 0,
729         .producer_id = 0,
730         .producer = .custom,
731         .generation = 0,
732         .owner_cookie = 0,
733         .operation = .alloc,
734         .lifecycle_disposition = .none,
735         .lifecycle_reason = .none,
736         .old_address = 0,
737         .old_len = 0,
738         .len = 0,
739         .alignment = 0,
740         .return_address = 0,
741         .active = false,
742     };
743 }
744 
745 fn lockProducerRegistry() void {
746     while (!producer_registry_lock.tryLock()) std.atomic.spinLoopHint();
747 }
748 
749 fn inactiveContext() EnabledContext {
750     return .{
751         .operation_id = 0,
752         .parent_operation_id = 0,
753         .active = false,
754     };
755 }
756 
757 fn takeId(counter: *std.atomic.Value(u64)) u64 {
758     const id = counter.fetchAdd(1, .monotonic);
759     if (id == 0 or id == std.math.maxInt(u64)) {
760         @panic("allocator observation identifier exhausted");
761     }
762     return id;
763 }