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 }