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 }