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 }