lib/bench/src/instrumentation.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

  1 const std = @import("std");
  2 const pretty_json = @import("pretty").json;
  3 const coz = @import("coz");
  4 const metadata = @import("metadata.zig");
  5 const stabilizer = @import("stabilizer");
  6 const sys = @import("sys");
  7 const tracy = @import("tracy");
  8 
  9 const Allocator = std.mem.Allocator;
 10 const SourceLocation = std.builtin.SourceLocation;
 11 
 12 pub const CozSession = struct {
 13     runtime: coz.Runtime = .{},
 14     profile_file: coz.ProfileFile = .{},
 15     enabled: bool = false,
 16 
 17     pub fn start(self: *CozSession, alloc: Allocator) !void {
 18         try self.startWithOptions(alloc, .{});
 19     }
 20 
 21     pub fn startWithOptions(self: *CozSession, alloc: Allocator, options: coz.RuntimeOptions) !void {
 22         self.enabled = try self.runtime.startIfAvailable(alloc, options);
 23     }
 24 
 25     pub fn startWithOutputPath(self: *CozSession, alloc: Allocator, path: []const u8) !void {
 26         try self.startWithOutputPathAndOptions(alloc, path, .{});
 27     }
 28 
 29     pub fn startWithOutputPathAndOptions(
 30         self: *CozSession,
 31         alloc: Allocator,
 32         path: []const u8,
 33         options: coz.RuntimeOptions,
 34     ) !void {
 35         try self.profile_file.open(path);
 36         errdefer self.profile_file.close();
 37 
 38         var runtime_options = options;
 39         runtime_options.profile_writer = self.profile_file.interface().?;
 40         self.enabled = try self.runtime.startIfAvailable(alloc, runtime_options);
 41         if (!self.enabled) self.profile_file.close();
 42     }
 43 
 44     pub fn deinit(self: *CozSession) void {
 45         self.runtime.shutdown();
 46         self.profile_file.close();
 47         self.enabled = false;
 48     }
 49 
 50     pub fn finish(self: *CozSession) !void {
 51         var first_error: ?anyerror = null;
 52         self.runtime.stop() catch |err| rememberSessionError(&first_error, err);
 53         self.profile_file.finish() catch |err| rememberSessionError(&first_error, err);
 54         self.enabled = false;
 55         if (first_error) |err| return err;
 56     }
 57 };
 58 
 59 pub const CapabilityName = enum {
 60     coz,
 61     tracy,
 62     stabilizer,
 63     memtrace,
 64     perf,
 65 
 66     pub fn text(self: CapabilityName) []const u8 {
 67         return switch (self) {
 68             .coz => "coz",
 69             .tracy => "tracy",
 70             .stabilizer => "stabilizer",
 71             .memtrace => "memtrace",
 72             .perf => "perf",
 73         };
 74     }
 75 };
 76 
 77 pub const CapabilityState = enum {
 78     enabled,
 79     disabled,
 80     unavailable,
 81 
 82     pub fn text(self: CapabilityState) []const u8 {
 83         return switch (self) {
 84             .enabled => "enabled",
 85             .disabled => "disabled",
 86             .unavailable => "unavailable",
 87         };
 88     }
 89 };
 90 
 91 pub const Capability = struct {
 92     name: CapabilityName,
 93     state: CapabilityState,
 94     requested: bool = false,
 95     artifact: ?[]const u8 = null,
 96     reason: ?[]const u8 = null,
 97     stabilizer_config: ?stabilizer.Config = null,
 98 };
 99 
100 pub const Artifact = struct {
101     name: []const u8,
102     path: []const u8,
103 };
104 
105 pub const ProfilingSessionOptions = struct {
106     name: []const u8,
107     coz_output: ?[]const u8 = null,
108     coz_summary: ?[]const u8 = null,
109     tracy_output: ?[]const u8 = null,
110     tracy_summary: ?[]const u8 = null,
111     tracy_summary_options: tracy.summary.Options = .{},
112     stabilizer_config: ?stabilizer.Config = null,
113     memtrace_summary: ?[]const u8 = null,
114     memtrace_events: ?[]const u8 = null,
115     perf_artifact: ?[]const u8 = null,
116     perf_counter: ?sys.perf.CounterSelector = null,
117     perf_counter_options: sys.perf.CounterOptions = .{
118         .read_format = sys.perf.ReadFormat.total_time_enabled |
119             sys.perf.ReadFormat.total_time_running,
120     },
121     require_tracy: bool = false,
122 };
123 
124 pub const ProfilingReport = struct {
125     capabilities: []const Capability,
126     artifacts: []const Artifact,
127 };
128 
129 pub const ProfilingSession = struct {
130     coz_session: CozSession = .{},
131     tracy_file: tracy.File = .{},
132     tracy_started: bool = false,
133     tracy_output: ?[]const u8 = null,
134     tracy_summary: ?[]const u8 = null,
135     tracy_summary_options: tracy.summary.Options = .{},
136     perf_region: ?sys.perf.CounterRegion = null,
137     perf_artifact: ?[]const u8 = null,
138     perf_result: ?sys.perf.CountResult = null,
139     allocator: ?Allocator = null,
140     capabilities: [5]Capability = undefined,
141     capability_count: usize = 0,
142     artifacts: [8]Artifact = undefined,
143     artifact_count: usize = 0,
144 
145     pub fn start(self: *ProfilingSession, allocator: Allocator, options: ProfilingSessionOptions) !void {
146         self.deinit();
147         self.reset();
148         self.allocator = allocator;
149         errdefer self.deinit();
150 
151         if (options.coz_output) |path| {
152             try ensureParent(path);
153             try self.coz_session.startWithOutputPath(allocator, path);
154             self.addCapability(.{
155                 .name = .coz,
156                 .state = if (self.coz_session.enabled) .enabled else .unavailable,
157                 .requested = true,
158                 .artifact = path,
159                 .reason = if (self.coz_session.enabled) null else "coz runtime unavailable",
160             });
161             self.addArtifact("coz_output", path);
162         } else {
163             self.addCapability(.{
164                 .name = .coz,
165                 .state = .disabled,
166                 .reason = "no coz output path configured",
167             });
168         }
169         if (options.coz_summary) |path| self.addArtifact("coz_summary", path);
170 
171         if (options.tracy_output) |path| {
172             self.tracy_output = path;
173             self.tracy_summary = options.tracy_summary;
174             self.tracy_summary_options = options.tracy_summary_options;
175             if (!tracy.enabled) {
176                 self.addCapability(.{
177                     .name = .tracy,
178                     .state = .unavailable,
179                     .requested = true,
180                     .artifact = path,
181                     .reason = "tracy build option disabled",
182                 });
183                 if (options.require_tracy) return error.TracyUnavailable;
184             } else {
185                 try ensureParent(path);
186                 try self.tracy_file.open(path);
187                 self.tracy_started = try tracy.start(self.tracy_file.interface().?, .{ .name = options.name });
188                 self.addCapability(.{
189                     .name = .tracy,
190                     .state = if (self.tracy_started) .enabled else .unavailable,
191                     .requested = true,
192                     .artifact = path,
193                     .reason = if (self.tracy_started) null else "tracy runtime unavailable",
194                 });
195             }
196             self.addArtifact("tracy_output", path);
197             if (options.tracy_summary) |summary_path| self.addArtifact("tracy_summary", summary_path);
198         } else {
199             self.addCapability(.{
200                 .name = .tracy,
201                 .state = if (tracy.enabled) .disabled else .unavailable,
202                 .reason = if (tracy.enabled) "no tracy output path configured" else "tracy build option disabled",
203             });
204         }
205 
206         self.addCapability(.{
207             .name = .stabilizer,
208             .state = if (options.stabilizer_config != null) .enabled else .disabled,
209             .requested = options.stabilizer_config != null,
210             .reason = if (options.stabilizer_config != null) null else "layout randomization disabled",
211             .stabilizer_config = options.stabilizer_config,
212         });
213 
214         if (options.memtrace_summary) |path| self.addArtifact("memtrace_summary", path);
215         if (options.memtrace_events) |path| self.addArtifact("memtrace_events", path);
216         self.addCapability(.{
217             .name = .memtrace,
218             .state = if (options.memtrace_summary != null or options.memtrace_events != null) .enabled else .disabled,
219             .requested = options.memtrace_summary != null or options.memtrace_events != null,
220             .artifact = options.memtrace_summary orelse options.memtrace_events,
221             .reason = if (options.memtrace_summary != null or options.memtrace_events != null) null else "no memtrace artifact configured",
222         });
223 
224         if (options.perf_artifact) |path| self.addArtifact("perf_counter", path);
225         self.perf_artifact = options.perf_artifact;
226         if (options.perf_counter) |counter| {
227             self.perf_region = sys.perf.CounterRegion.start(counter, options.perf_counter_options) catch |err| {
228                 self.addCapability(.{
229                     .name = .perf,
230                     .state = .unavailable,
231                     .requested = true,
232                     .artifact = options.perf_artifact,
233                     .reason = @errorName(err),
234                 });
235                 return;
236             };
237             self.addCapability(.{
238                 .name = .perf,
239                 .state = .enabled,
240                 .requested = true,
241                 .artifact = options.perf_artifact,
242             });
243         } else {
244             self.addCapability(.{
245                 .name = .perf,
246                 .state = .disabled,
247                 .requested = options.perf_artifact != null,
248                 .artifact = options.perf_artifact,
249                 .reason = if (options.perf_artifact != null) "no perf counter configured" else "no perf counter artifact configured",
250             });
251         }
252     }
253 
254     pub fn finish(self: *ProfilingSession) !void {
255         var first_error: ?anyerror = null;
256         self.finishPerfCounter() catch |err| rememberSessionError(&first_error, err);
257         const write_tracy_summary = self.tracy_started;
258         if (self.tracy_started) {
259             tracy.stop();
260             self.tracy_started = false;
261         }
262         self.tracy_file.finish() catch |err| rememberSessionError(&first_error, err);
263         if (write_tracy_summary) self.writeTracySummary() catch |err| rememberSessionError(&first_error, err);
264         self.coz_session.finish() catch |err| rememberSessionError(&first_error, err);
265         if (first_error) |err| return err;
266     }
267 
268     pub fn deinit(self: *ProfilingSession) void {
269         if (self.perf_region) |*region| {
270             if (region.active) _ = region.stop() catch {};
271             region.deinit();
272             self.perf_region = null;
273         }
274         if (self.tracy_started) {
275             tracy.stop();
276             self.tracy_started = false;
277         }
278         self.tracy_file.close();
279         self.coz_session.deinit();
280     }
281 
282     pub fn report(self: *const ProfilingSession) ProfilingReport {
283         return .{
284             .capabilities = self.capabilities[0..self.capability_count],
285             .artifacts = self.artifacts[0..self.artifact_count],
286         };
287     }
288 
289     fn reset(self: *ProfilingSession) void {
290         self.capability_count = 0;
291         self.artifact_count = 0;
292         self.tracy_started = false;
293         self.tracy_output = null;
294         self.tracy_summary = null;
295         self.tracy_summary_options = .{};
296         self.perf_region = null;
297         self.perf_artifact = null;
298         self.perf_result = null;
299         self.allocator = null;
300     }
301 
302     fn addCapability(self: *ProfilingSession, capability: Capability) void {
303         self.capabilities[self.capability_count] = capability;
304         self.capability_count += 1;
305     }
306 
307     fn addArtifact(self: *ProfilingSession, name: []const u8, path: []const u8) void {
308         self.artifacts[self.artifact_count] = .{ .name = name, .path = path };
309         self.artifact_count += 1;
310     }
311 
312     fn setCapability(self: *ProfilingSession, capability: Capability) void {
313         for (self.capabilities[0..self.capability_count]) |*existing| {
314             if (existing.name == capability.name) {
315                 existing.* = capability;
316                 return;
317             }
318         }
319         self.addCapability(capability);
320     }
321 
322     fn finishPerfCounter(self: *ProfilingSession) !void {
323         var region = self.perf_region orelse return;
324         self.perf_region = null;
325         defer region.deinit();
326         if (!region.active) return;
327         const result = region.stop() catch |err| {
328             self.setCapability(.{
329                 .name = .perf,
330                 .state = .unavailable,
331                 .requested = true,
332                 .artifact = self.perf_artifact,
333                 .reason = @errorName(err),
334             });
335             return err;
336         };
337         self.perf_result = result;
338         if (self.perf_artifact) |path| try writePerfCounterResultFile(path, result);
339     }
340 
341     fn writeTracySummary(self: *ProfilingSession) !void {
342         const trace_path = self.tracy_output orelse return;
343         const summary_path = self.tracy_summary orelse return;
344         const allocator = self.allocator orelse return error.ProfilingSessionNotStarted;
345         try writeTracySummaryJsonlFile(allocator, summary_path, trace_path, self.tracy_summary_options);
346     }
347 };
348 
349 pub fn writeProfilingReport(object: pretty_json.Object, report: ProfilingReport) !void {
350     const capabilities = try object.array("capabilities");
351     for (report.capabilities) |capability| {
352         const entry = try capabilities.object();
353         try entry.field("name", capability.name.text());
354         try entry.field("state", capability.state.text());
355         try entry.field("requested", capability.requested);
356         try entry.field("artifact", capability.artifact);
357         try entry.field("reason", capability.reason);
358         if (capability.stabilizer_config) |layout_config| {
359             try metadata.writeStabilizerConfig(
360                 try entry.object("stabilizer_config"),
361                 layout_config,
362             );
363         }
364         try entry.end();
365     }
366     try capabilities.end();
367     const artifacts = try object.object("artifacts");
368     for (report.artifacts) |artifact| {
369         try artifacts.field(artifact.name, artifact.path);
370     }
371     try artifacts.end();
372     try object.end();
373 }
374 
375 pub fn writePerfCounterResult(object: pretty_json.Object, result: sys.perf.CountResult) !void {
376     try object.field("protocol", "bench.perf-counter/v1");
377     try object.field("value", result.value);
378     try object.field("scaled_value", result.scaledValue());
379     try object.field("time_enabled", result.time_enabled);
380     try object.field("time_running", result.time_running);
381     try object.field("running_coverage", result.runningCoverage().text());
382     try object.field("running_ratio", result.runningRatio());
383     try object.field("id", result.id);
384     try object.field("lost", result.lost);
385     try object.end();
386 }
387 
388 fn writePerfCounterResultFile(path: []const u8, result: sys.perf.CountResult) !void {
389     try ensureParent(path);
390     var file = try sys.fs.createFile(path, .{ .truncate = true });
391     defer file.close(sys.fs.debugIo());
392 
393     var buffer: [8192]u8 = undefined;
394     var writer = file.writer(sys.fs.debugIo(), &buffer);
395     var stream = pretty_json.Writer.init(&writer.interface, .minified);
396     try writePerfCounterResult(try stream.object(), result);
397     try stream.newline();
398     try writer.interface.flush();
399 }
400 
401 fn writeTracySummaryJsonlFile(
402     allocator: Allocator,
403     path: []const u8,
404     trace_path: []const u8,
405     options: tracy.summary.Options,
406 ) !void {
407     try ensureParent(path);
408     var file = try sys.fs.createFile(path, .{ .truncate = true });
409     defer file.close(sys.fs.debugIo());
410 
411     var buffer: [8192]u8 = undefined;
412     var writer = file.writer(sys.fs.debugIo(), &buffer);
413     try tracy.summary.writeSummaryJsonlFromJsonlPath(allocator, trace_path, &writer.interface, options);
414     try writer.interface.flush();
415 }
416 
417 fn rememberSessionError(first_error: *?anyerror, err: anyerror) void {
418     if (first_error.* == null) first_error.* = err;
419 }
420 
421 fn ensureParent(path: []const u8) !void {
422     if (std.fs.path.dirname(path)) |dir| {
423         if (dir.len > 0) try sys.fs.createDirPath(dir);
424     }
425 }
426 
427 fn tracyName(comptime name: []const u8) [:0]const u8 {
428     if (std.mem.indexOfScalar(u8, name, 0) != null) {
429         @compileError("bench phase names must not contain NUL bytes");
430     }
431     const terminated = name ++ "\x00";
432     return terminated[0..name.len :0];
433 }
434 
435 pub fn Phase(comptime name: []const u8) type {
436     return struct {
437         coz_scope: coz.Scope(name),
438         tracy_zone: tracy.Zone,
439 
440         const Self = @This();
441 
442         pub inline fn init(comptime src: SourceLocation) Self {
443             return .{
444                 .coz_scope = coz.scope(name),
445                 .tracy_zone = tracy.zoneAt(tracyName(name), src),
446             };
447         }
448 
449         pub inline fn setName(self: Self, dynamic_name: []const u8) void {
450             self.tracy_zone.setName(dynamic_name);
451         }
452 
453         pub inline fn setValue(self: Self, value: u64) void {
454             self.tracy_zone.setValue(value);
455         }
456 
457         pub inline fn setText(self: Self, text: []const u8) void {
458             self.tracy_zone.setText(text);
459         }
460 
461         pub inline fn end(self: Self) void {
462             self.coz_scope.end();
463             self.tracy_zone.end();
464         }
465     };
466 }
467 
468 pub inline fn phaseAt(comptime name: []const u8, comptime src: SourceLocation) Phase(name) {
469     return Phase(name).init(src);
470 }
471 
472 pub fn rerandomizeLayout(runtime: *?stabilizer.Runtime, comptime progress_name: []const u8) void {
473     if (runtime.*) |*active| {
474         active.rerandomize();
475         coz.progressNamed(progress_name);
476     }
477 }
478 
479 pub fn callWithStackPad(unit: u8, callback: anytype, args: anytype) @TypeOf(@call(.auto, callback, args)) {
480     switch (unit) {
481         inline 0...stabilizer.max_stack_pad_unit => |pad_unit| {
482             return callWithExactStackPad(@as(usize, pad_unit) * stabilizer.stack_alignment, callback, args);
483         },
484     }
485 }
486 
487 fn callWithExactStackPad(comptime bytes: usize, callback: anytype, args: anytype) @TypeOf(@call(.auto, callback, args)) {
488     var pad: [bytes]u8 align(stabilizer.stack_alignment) = undefined;
489     touchStackPad(&pad);
490     return @call(.auto, callback, args);
491 }
492 
493 pub fn touchStackPad(pad: []u8) void {
494     for (pad, 0..) |*byte, index| {
495         const volatile_byte: *volatile u8 = @ptrCast(byte);
496         volatile_byte.* = @truncate(index);
497     }
498 }
499 
500 test "coz session starts without sampling resources" {
501     var session: CozSession = .{};
502 
503     try session.startWithOptions(std.testing.allocator, .{
504         .install_signal_handler = false,
505         .start_current_thread = false,
506     });
507     defer session.deinit();
508 
509     try std.testing.expect(session.enabled);
510     try std.testing.expect(session.runtime.profiler_installed);
511 }
512 
513 test "coz session writes owned profile output" {
514     const allocator = std.testing.allocator;
515     var tmp = std.testing.tmpDir(.{});
516     defer tmp.cleanup();
517 
518     const root = try tmp.parent_dir.realPathFileAlloc(std.Options.debug_io, tmp.sub_path[0..], allocator);
519     defer allocator.free(root);
520     const path = try std.fs.path.join(allocator, &.{ root, "bench.coz.jsonl" });
521     defer allocator.free(path);
522 
523     var session: CozSession = .{};
524     try session.startWithOutputPathAndOptions(allocator, path, .{
525         .install_signal_handler = false,
526         .start_current_thread = false,
527     });
528     try std.testing.expect(session.enabled);
529     session.deinit();
530 
531     const contents = try sys.fs.readFileAlloc(allocator, path, 1024);
532     defer allocator.free(contents);
533     try std.testing.expect(std.mem.indexOf(u8, contents, "\"event\":\"startup\"") != null);
534     try std.testing.expect(std.mem.indexOf(u8, contents, "\"event\":\"runtime\"") != null);
535 }
536 
537 test "coz session rejects invalid sampler configuration" {
538     var session: CozSession = .{};
539 
540     try std.testing.expectError(error.InvalidSamplePeriod, session.startWithOptions(std.testing.allocator, .{
541         .install_signal_handler = false,
542         .sampler_options = .{ .sample_period_ns = 0 },
543     }));
544 
545     try std.testing.expect(!session.enabled);
546     try std.testing.expect(!session.runtime.profiler_installed);
547 }
548 
549 test "profiling session reports enabled and disabled capabilities" {
550     const allocator = std.testing.allocator;
551     var tmp = std.testing.tmpDir(.{});
552     defer tmp.cleanup();
553 
554     const root = try tmp.parent_dir.realPathFileAlloc(std.Options.debug_io, tmp.sub_path[0..], allocator);
555     defer allocator.free(root);
556     const coz_path = try std.fs.path.join(allocator, &.{ root, "bench.coz.jsonl" });
557     defer allocator.free(coz_path);
558     const coz_summary = try std.fs.path.join(allocator, &.{ root, "bench.coz.analysis.json" });
559     defer allocator.free(coz_summary);
560 
561     var session: ProfilingSession = .{};
562     try session.start(allocator, .{
563         .name = "bench test",
564         .coz_output = coz_path,
565         .coz_summary = coz_summary,
566         .stabilizer_config = .{ .seed = 5, .code = .{ .enabled = false } },
567     });
568     try session.finish();
569     session.deinit();
570 
571     const report_value = session.report();
572     try std.testing.expectEqual(@as(usize, 5), report_value.capabilities.len);
573     const coz_capability = report_value.capabilities[0];
574     try std.testing.expectEqual(CapabilityName.coz, coz_capability.name);
575     switch (coz_capability.state) {
576         .enabled => try std.testing.expect(coz_capability.reason == null),
577         .unavailable => try std.testing.expect(coz_capability.reason != null),
578         .disabled => return error.TestUnexpectedResult,
579     }
580     try std.testing.expectEqual(CapabilityState.enabled, report_value.capabilities[2].state);
581     try std.testing.expectEqual(CapabilityName.stabilizer, report_value.capabilities[2].name);
582     try std.testing.expectEqual(@as(u64, 5), report_value.capabilities[2].stabilizer_config.?.seed);
583     try std.testing.expectEqual(CapabilityState.disabled, report_value.capabilities[3].state);
584     try std.testing.expectEqual(CapabilityName.memtrace, report_value.capabilities[3].name);
585     try std.testing.expectEqual(CapabilityState.disabled, report_value.capabilities[4].state);
586     try std.testing.expectEqual(CapabilityName.perf, report_value.capabilities[4].name);
587     try std.testing.expectEqual(@as(usize, 2), report_value.artifacts.len);
588 }
589 
590 test "profiling session reports requested perf artifact without counter as disabled" {
591     const allocator = std.testing.allocator;
592     var tmp = std.testing.tmpDir(.{});
593     defer tmp.cleanup();
594 
595     const root = try tmp.parent_dir.realPathFileAlloc(std.Options.debug_io, tmp.sub_path[0..], allocator);
596     defer allocator.free(root);
597     const perf_path = try std.fs.path.join(allocator, &.{ root, "bench.perf.json" });
598     defer allocator.free(perf_path);
599 
600     var session: ProfilingSession = .{};
601     try session.start(allocator, .{
602         .name = "bench test",
603         .perf_artifact = perf_path,
604     });
605     defer session.deinit();
606 
607     const report_value = session.report();
608     try std.testing.expectEqual(@as(usize, 5), report_value.capabilities.len);
609     const perf_capability = report_value.capabilities[4];
610     try std.testing.expectEqual(CapabilityName.perf, perf_capability.name);
611     try std.testing.expectEqual(CapabilityState.disabled, perf_capability.state);
612     try std.testing.expect(perf_capability.requested);
613     try std.testing.expectEqualStrings(perf_path, perf_capability.artifact.?);
614     try std.testing.expectEqualStrings("no perf counter configured", perf_capability.reason.?);
615     try std.testing.expectEqual(@as(usize, 1), report_value.artifacts.len);
616     try std.testing.expectEqualStrings("perf_counter", report_value.artifacts[0].name);
617     try session.finish();
618 }
619 
620 test "profiling report writes capability rows and artifact paths" {
621     const report_value = ProfilingReport{
622         .capabilities = &.{
623             .{ .name = .coz, .state = .enabled, .requested = true, .artifact = "bench.coz.jsonl" },
624             .{ .name = .tracy, .state = .unavailable, .requested = true, .artifact = "bench.tracy.jsonl", .reason = "tracy build option disabled" },
625             .{ .name = .stabilizer, .state = .enabled, .requested = true, .stabilizer_config = .{ .seed = 88, .heap = .{ .shuffle_slots = 7 }, .code = .{ .enabled = false } } },
626             .{ .name = .memtrace, .state = .disabled, .reason = "no memtrace artifact configured" },
627         },
628         .artifacts = &.{
629             .{ .name = "coz_output", .path = "bench.coz.jsonl" },
630             .{ .name = "coz_summary", .path = "bench.coz.analysis.json" },
631         },
632     };
633     var out = std.Io.Writer.Allocating.init(std.testing.allocator);
634     defer out.deinit();
635     var stream = pretty_json.Writer.init(&out.writer, .minified);
636     try writeProfilingReport(try stream.object(), report_value);
637 
638     const json = out.written();
639     try std.testing.expect(std.mem.indexOf(u8, json, "\"name\":\"coz\"") != null);
640     try std.testing.expect(std.mem.indexOf(u8, json, "\"state\":\"enabled\"") != null);
641     try std.testing.expect(std.mem.indexOf(u8, json, "\"name\":\"tracy\"") != null);
642     try std.testing.expect(std.mem.indexOf(u8, json, "\"state\":\"unavailable\"") != null);
643     try std.testing.expect(std.mem.indexOf(u8, json, "\"name\":\"stabilizer\"") != null);
644     try std.testing.expect(std.mem.indexOf(u8, json, "\"stabilizer_config\":{\"seed\":88") != null);
645     try std.testing.expect(std.mem.indexOf(u8, json, "\"shuffle_slots\":7") != null);
646     try std.testing.expect(std.mem.indexOf(u8, json, "\"code\":{\"enabled\":false") != null);
647     try std.testing.expect(std.mem.indexOf(u8, json, "\"name\":\"memtrace\"") != null);
648     try std.testing.expect(std.mem.indexOf(u8, json, "\"coz_summary\":\"bench.coz.analysis.json\"") != null);
649 }
650 
651 test "perf counter result writer records scaled counter fields" {
652     const result = sys.perf.CountResult{
653         .value = 10,
654         .time_enabled = 100,
655         .time_running = 50,
656         .id = 7,
657         .lost = 3,
658     };
659     var out = std.Io.Writer.Allocating.init(std.testing.allocator);
660     defer out.deinit();
661     var stream = pretty_json.Writer.init(&out.writer, .minified);
662     try writePerfCounterResult(try stream.object(), result);
663 
664     const json = out.written();
665     try std.testing.expect(std.mem.indexOf(u8, json, "\"protocol\":\"bench.perf-counter/v1\"") != null);
666     try std.testing.expect(std.mem.indexOf(u8, json, "\"value\":10") != null);
667     try std.testing.expect(std.mem.indexOf(u8, json, "\"scaled_value\":20") != null);
668     try std.testing.expect(std.mem.indexOf(u8, json, "\"time_enabled\":100") != null);
669     try std.testing.expect(std.mem.indexOf(u8, json, "\"time_running\":50") != null);
670     try std.testing.expect(
671         std.mem.indexOf(u8, json, "\"running_coverage\":\"multiplexed\"") != null,
672     );
673     try std.testing.expect(std.mem.indexOf(u8, json, "\"running_ratio\":") != null);
674     try std.testing.expect(std.mem.indexOf(u8, json, "\"id\":7") != null);
675     try std.testing.expect(std.mem.indexOf(u8, json, "\"lost\":3") != null);
676 }
677 
678 test "perf counter result writer withholds invalid evidence" {
679     const result = sys.perf.CountResult{ .value = 10, .time_enabled = 50, .time_running = 100 };
680     var out = std.Io.Writer.Allocating.init(std.testing.allocator);
681     defer out.deinit();
682     var stream = pretty_json.Writer.init(&out.writer, .minified);
683     try writePerfCounterResult(try stream.object(), result);
684 
685     const json = out.written();
686     try std.testing.expect(std.mem.indexOf(u8, json, "\"scaled_value\":null") != null);
687     try std.testing.expect(
688         std.mem.indexOf(u8, json, "\"running_coverage\":\"invalid\"") != null,
689     );
690     try std.testing.expect(std.mem.indexOf(u8, json, "\"running_ratio\":null") != null);
691     try std.testing.expect(std.mem.indexOf(u8, json, "\"lost\":null") != null);
692 }
693 
694 test "layout rerandomization emits Coz progress" {
695     const allocator = std.testing.allocator;
696     var profiler: coz.Profiler = .{};
697     defer profiler.deinit(allocator);
698 
699     coz.installProfiler(allocator, &profiler);
700     defer coz.uninstallProfiler(&profiler);
701 
702     var runtime: ?stabilizer.Runtime = try stabilizer.Runtime.init(allocator, .{
703         .seed = 17,
704         .code = .{ .enabled = false },
705     });
706     defer if (runtime) |*active| active.deinit();
707 
708     rerandomizeLayout(&runtime, "bench.test.stabilizer.rerandomize");
709 
710     const point = try profiler.registry.getThroughputPoint(allocator, "bench.test.stabilizer.rerandomize");
711     try std.testing.expectEqual(@as(usize, 1), point.getCount());
712 }
713 
714 test "phase pairs Coz latency with Tracy zone surface" {
715     const allocator = std.testing.allocator;
716     var profiler: coz.Profiler = .{};
717     defer profiler.deinit(allocator);
718 
719     coz.installProfiler(allocator, &profiler);
720     defer coz.uninstallProfiler(&profiler);
721 
722     {
723         const phase = phaseAt("bench.test.phase", @src());
724         phase.setName("dynamic bench phase");
725         phase.setText("phase metadata");
726         phase.setValue(123);
727         defer phase.end();
728     }
729 
730     const point = try profiler.registry.getLatencyPoint(allocator, "bench.test.phase");
731     try std.testing.expectEqual(@as(usize, 1), point.getBeginCount());
732     try std.testing.expectEqual(@as(usize, 1), point.getEndCount());
733 }
734 
735 fn testPhaseName(comptime suffix: []const u8) []const u8 {
736     return "bench.test." ++ suffix;
737 }
738 
739 test "phase accepts comptime constructed names" {
740     const allocator = std.testing.allocator;
741     var profiler: coz.Profiler = .{};
742     defer profiler.deinit(allocator);
743 
744     coz.installProfiler(allocator, &profiler);
745     defer coz.uninstallProfiler(&profiler);
746 
747     {
748         const phase = phaseAt(testPhaseName("constructed"), @src());
749         defer phase.end();
750     }
751 
752     const point = try profiler.registry.getLatencyPoint(allocator, "bench.test.constructed");
753     try std.testing.expectEqual(@as(usize, 1), point.getBeginCount());
754     try std.testing.expectEqual(@as(usize, 1), point.getEndCount());
755 }