lib/memtrace/src/context.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

  1 const std = @import("std");
  2 const tracer_mod = @import("tracer.zig");
  3 const census_mod = @import("census/root.zig");
  4 
  5 pub const Span = struct {
  6     scope: ?tracer_mod.Scope = null,
  7 
  8     pub fn exit(self: *Span) void {
  9         if (self.scope) |*scope| scope.exit();
 10     }
 11 };
 12 
 13 /// A pair of borrowed callbacks that report the beginning and the end of a
 14 /// phase, identified by a category byte. A caller with its own profiler uses
 15 /// them to report memtrace's phase boundaries into that profiler. `begin` takes
 16 /// the category and answers with a token, `end` takes that token back, and
 17 /// neither call can fail. The owner keeps `context` alive for as long as the
 18 /// sink is in use, because both callbacks receive it unchanged. A token is
 19 /// closed from the thread that opened it, before any token opened after it,
 20 /// which is the order `Context.enterInterval` and `IntervalSpan.exit` produce
 21 /// when the spans nest lexically. The callbacks carry no synchronization of
 22 /// their own, so one thread at a time uses a sink. `begin` answers with
 23 /// `invalid_token` to decline a span, and the matching exit then calls nothing.
 24 pub const IntervalSink = struct {
 25     pub const invalid_token = std.math.maxInt(u16);
 26 
 27     context: *anyopaque,
 28     begin_fn: *const fn (*anyopaque, u8) u16,
 29     end_fn: *const fn (*anyopaque, u16) void,
 30 
 31     pub fn begin(self: IntervalSink, category: u8) u16 {
 32         return self.begin_fn(self.context, category);
 33     }
 34 
 35     pub fn end(self: IntervalSink, token: u16) void {
 36         self.end_fn(self.context, token);
 37     }
 38 };
 39 
 40 pub const IntervalSpan = struct {
 41     sink: ?IntervalSink = null,
 42     token: u16 = IntervalSink.invalid_token,
 43 
 44     pub fn exit(self: *IntervalSpan) void {
 45         const sink = self.sink orelse return;
 46         if (self.token != IntervalSink.invalid_token) sink.end(self.token);
 47         self.sink = null;
 48         self.token = IntervalSink.invalid_token;
 49     }
 50 };
 51 
 52 pub const Context = struct {
 53     tracer: ?*tracer_mod.Tracer = null,
 54     census: ?*census_mod.Census = null,
 55     intervals: ?IntervalSink = null,
 56 
 57     pub fn init(tracer: ?*tracer_mod.Tracer, census: ?*census_mod.Census) Context {
 58         return .{
 59             .tracer = tracer,
 60             .census = census,
 61         };
 62     }
 63 
 64     pub fn disabled() Context {
 65         return .{};
 66     }
 67 
 68     pub fn tracesAllocations(self: Context) bool {
 69         return self.tracer != null;
 70     }
 71 
 72     pub fn recordsCensus(self: Context) bool {
 73         return self.census != null;
 74     }
 75 
 76     pub fn enabled(self: Context) bool {
 77         return self.tracesAllocations() or self.recordsCensus() or
 78             self.intervals != null;
 79     }
 80 
 81     pub fn withIntervals(self: Context, intervals: ?IntervalSink) Context {
 82         var result = self;
 83         result.intervals = intervals;
 84         return result;
 85     }
 86 
 87     pub fn enter(self: Context, label: []const u8) !Span {
 88         const actual = self.tracer orelse return .{};
 89         return .{ .scope = try actual.enter(label) };
 90     }
 91 
 92     pub fn enterInterval(self: Context, category: u8) IntervalSpan {
 93         const sink = self.intervals orelse return .{};
 94         const token = sink.begin(category);
 95         if (token == IntervalSink.invalid_token) return .{};
 96         return .{ .sink = sink, .token = token };
 97     }
 98 
 99     pub fn record(self: Context, label: []const u8, bytes: usize) !void {
100         const actual = self.census orelse return;
101         try actual.record(label, bytes);
102     }
103 
104     pub fn recordPrefixed(self: Context, prefix: []const u8, label: []const u8, bytes: usize) !void {
105         const actual = self.census orelse return;
106         try actual.recordPrefixed(prefix, label, bytes);
107     }
108 
109     pub fn recordCount(self: Context, label: []const u8, count: usize, bytes: usize) !void {
110         const actual = self.census orelse return;
111         try actual.recordCount(label, count, bytes);
112     }
113 
114     pub fn recordCountPrefixed(self: Context, prefix: []const u8, label: []const u8, count: usize, bytes: usize) !void {
115         const actual = self.census orelse return;
116         try actual.recordCountPrefixed(prefix, label, count, bytes);
117     }
118 };
119 
120 const IntervalTestRecorder = struct {
121     began: [8]u8 = undefined,
122     ended: [8]u16 = undefined,
123     begin_count: usize = 0,
124     end_count: usize = 0,
125 
126     fn sink(self: *IntervalTestRecorder) IntervalSink {
127         return .{
128             .context = self,
129             .begin_fn = begin,
130             .end_fn = end,
131         };
132     }
133 
134     fn begin(opaque_context: *anyopaque, category: u8) u16 {
135         const self: *IntervalTestRecorder = @ptrCast(@alignCast(opaque_context));
136         if (category == 255) return IntervalSink.invalid_token;
137         std.debug.assert(self.begin_count < self.began.len);
138         const token = std.math.cast(u16, self.begin_count).?;
139         self.began[self.begin_count] = category;
140         self.begin_count += 1;
141         return token;
142     }
143 
144     fn end(opaque_context: *anyopaque, token: u16) void {
145         const self: *IntervalTestRecorder = @ptrCast(@alignCast(opaque_context));
146         std.debug.assert(self.end_count < self.ended.len);
147         self.ended[self.end_count] = token;
148         self.end_count += 1;
149     }
150 };
151 
152 test "disabled context is a no-op participation surface" {
153     const context = Context.disabled();
154     try std.testing.expect(!context.enabled());
155 
156     var span = try context.enter("phase");
157     defer span.exit();
158     try context.record("runtime.value.string", 32);
159     try context.recordPrefixed("global.page.", "runtime.value.record", 16);
160 }
161 
162 test "context records allocation scopes and census entries" {
163     var tracer = try tracer_mod.Tracer.init(std.testing.allocator, .{});
164     defer tracer.deinit();
165     var census_storage = try census_mod.Storage.init(std.testing.allocator, .{
166         .categories = 4,
167         .label_bytes = 128,
168         .joined_label_bytes = 64,
169     });
170     defer census_storage.deinit(std.testing.allocator);
171     census_storage.activate();
172     var census = try census_mod.Census.init(&census_storage);
173     defer census.deinit();
174 
175     var traced = try tracer.tracedAllocator(std.testing.allocator, "test.heap");
176     const allocator = traced.allocator();
177     const context = Context.init(&tracer, &census);
178 
179     var span = try context.enter("phase");
180     const bytes = try allocator.alloc(u8, 24);
181     try context.record("runtime.value.bytes", bytes.len);
182     span.exit();
183     allocator.free(bytes);
184 
185     var trace_out = std.Io.Writer.Allocating.init(std.testing.allocator);
186     defer trace_out.deinit();
187     try tracer.writeSummary(&trace_out.writer, .{ .top = 4, .include_zero_live = true });
188     try std.testing.expect(std.mem.indexOf(u8, trace_out.written(), "root/phase") != null);
189 
190     var census_out = std.Io.Writer.Allocating.init(std.testing.allocator);
191     defer census_out.deinit();
192     try census.writeSummary(&census_out.writer, .{ .top = 4 });
193     try std.testing.expect(std.mem.indexOf(u8, census_out.written(), "runtime.value.bytes items=1 bytes=24") != null);
194 }
195 
196 test "interval context preserves nested repeated and ignored spans" {
197     var recorder: IntervalTestRecorder = .{};
198     const context = Context.disabled().withIntervals(recorder.sink());
199     try std.testing.expect(context.enabled());
200     try std.testing.expect(!context.tracesAllocations());
201 
202     var outer = context.enterInterval(3);
203     var inner = context.enterInterval(3);
204     var ignored = context.enterInterval(255);
205     ignored.exit();
206     inner.exit();
207     outer.exit();
208 
209     try std.testing.expectEqualSlices(u8, &.{ 3, 3 }, recorder.began[0..recorder.begin_count]);
210     try std.testing.expectEqualSlices(u16, &.{ 1, 0 }, recorder.ended[0..recorder.end_count]);
211 }