lib/trace/src/cli.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const pretty = @import("pretty");
3 const pretty_usage = @import("pretty_usage");
4 const sys = @import("sys");
5 const trace_mod = @import("trace");
6 const event = trace_mod.event;
7 const replay = trace_mod.replay;
8 const session_mod = trace_mod.session;
9 const store_mod = trace_mod.store;
10
11 const Allocator = std.mem.Allocator;
12 const timepoint_string_capacity = 54;
13
14 const usage_commands = [_][]const u8{
15 "trace record --trace <path> -- <program> [args...]",
16 "trace verify <trace>",
17 "trace summary <trace> [--format jsonl]",
18 "trace events <trace> [--format jsonl]",
19 "trace timepoints <trace> [--format jsonl]",
20 "trace user-events <trace> [--label <label>] [--protocol <protocol>] [--payload <key=value>]... [--format jsonl]",
21 "trace user-event-stats <trace> --group-by <payload-field> --metric <payload-field> [--sort-by total|count|min|max|mean|group] [--label <label>] [--protocol <protocol>] [--payload <key=value>]... [--format jsonl]",
22 "trace threads <trace> [--format jsonl]",
23 "trace objects <trace> [--format jsonl]",
24 "trace export <trace> [--format jsonl]",
25 "trace replay <trace> --to <timepoint>",
26 "trace checkpoints <trace>",
27 "trace frames <trace> --at <timepoint>",
28 "trace reverse-watch <trace> --from <timepoint> --object <id>",
29 };
30
31 pub fn dispatch(allocator: Allocator, args: []const []const u8) !u8 {
32 if (args.len < 2) {
33 try writeUsage(allocator, sys.stdio.stderr());
34 return 1;
35 }
36
37 const command = args[1];
38 if (std.mem.eql(u8, command, "record")) return try record(allocator, args[2..]);
39 if (std.mem.eql(u8, command, "verify")) return try verify(allocator, args[2..]);
40 if (std.mem.eql(u8, command, "summary")) return try summary(allocator, args[2..]);
41 if (std.mem.eql(u8, command, "events")) return try dumpEvents(allocator, args[2..]);
42 if (std.mem.eql(u8, command, "timepoints")) return try timepoints(allocator, args[2..]);
43 if (std.mem.eql(u8, command, "user-events")) return try userEvents(allocator, args[2..]);
44 if (std.mem.eql(u8, command, "user-event-stats")) return try userEventStats(allocator, args[2..]);
45 if (std.mem.eql(u8, command, "threads")) return try threads(allocator, args[2..]);
46 if (std.mem.eql(u8, command, "objects")) return try objects(allocator, args[2..]);
47 if (std.mem.eql(u8, command, "export")) return try dumpEvents(allocator, args[2..]);
48 if (std.mem.eql(u8, command, "replay")) return try replayCommand(allocator, args[2..]);
49 if (std.mem.eql(u8, command, "checkpoints")) return try checkpoints(allocator, args[2..]);
50 if (std.mem.eql(u8, command, "frames")) return try frames(allocator, args[2..]);
51 if (std.mem.eql(u8, command, "reverse-watch")) return try reverseWatch(allocator, args[2..]);
52 if (std.mem.eql(u8, command, "help") or std.mem.eql(u8, command, "--help")) {
53 try writeUsage(allocator, sys.stdio.stdout());
54 return 0;
55 }
56 return error.UnknownCommand;
57 }
58
59 pub fn printError(allocator: Allocator, file: sys.stdio.File, text: []const u8) !void {
60 try pretty_usage.Terminal.init(allocator, file, .{}).writeErrorText("trace", text);
61 }
62
63 fn record(allocator: Allocator, args: []const []const u8) !u8 {
64 var trace_path: ?[]const u8 = null;
65 var separator_index: ?usize = null;
66
67 var index: usize = 0;
68 while (index < args.len) : (index += 1) {
69 if (std.mem.eql(u8, args[index], "--trace")) {
70 index += 1;
71 if (index >= args.len) return error.MissingTracePath;
72 trace_path = args[index];
73 } else if (std.mem.eql(u8, args[index], "--")) {
74 separator_index = index;
75 break;
76 } else {
77 return error.UnknownArgument;
78 }
79 }
80
81 const trace = trace_path orelse return error.MissingTracePath;
82 const sep = separator_index orelse return error.MissingProgram;
83 const child_argv = args[sep + 1 ..];
84 if (child_argv.len == 0) return error.MissingProgram;
85
86 const manifest = manifestForRecord();
87 const limits = store_mod.Limits{};
88 var writer_storage = try store_mod.WriterStorage.init(
89 allocator,
90 store_mod.WriterLimits.fromOpen(trace, manifest, limits),
91 );
92 defer writer_storage.deinit(allocator);
93 writer_storage.activate();
94 var trace_writer: store_mod.Writer = .{};
95 try trace_writer.open(&writer_storage, trace, manifest, limits);
96 defer trace_writer.deinit();
97 var session = session_mod.Session.initRecord(allocator, trace_writer.sink());
98 defer session.deinit();
99
100 _ = try session.sessionStart(1, child_argv[0]);
101 const argv_bytes = try joinArgs(allocator, child_argv);
102 defer allocator.free(argv_bytes);
103 _ = try session.userEvent(1, "process.argv", argv_bytes);
104 const exit_code = try runChild(allocator, child_argv);
105 _ = try session.sessionEnd(1, exit_code);
106 try trace_writer.finish();
107
108 return if (exit_code == 0) 0 else 1;
109 }
110
111 fn verify(allocator: Allocator, args: []const []const u8) !u8 {
112 if (args.len != 1) return error.InvalidArguments;
113 const result = try verifyTrace(allocator, args[0]);
114
115 var stdout_buffer: [4096]u8 = undefined;
116 var stdout = sys.stdio.stdout().writer(sys.stdio.debugIo(), &stdout_buffer);
117 var stream = pretty.json.Writer.init(&stdout.interface, .minified);
118 const object = try stream.object();
119 try object.field("event", "verify");
120 try object.field("status", "ok");
121 try object.field("chunk_count", result.chunk_count);
122 try object.field("event_count", result.event_count);
123 try object.field("event_bytes", result.event_bytes);
124 try object.field("checksum", result.checksum);
125 try object.endLine();
126 try stdout.interface.flush();
127 return 0;
128 }
129
130 fn dumpEvents(allocator: Allocator, args: []const []const u8) !u8 {
131 if (args.len != 1 and args.len != 3) return error.InvalidArguments;
132 if (args.len == 3 and (!std.mem.eql(u8, args[1], "--format") or !std.mem.eql(u8, args[2], "jsonl"))) {
133 return error.UnsupportedFormat;
134 }
135
136 var reader_storage: store_mod.ReaderStorage = undefined;
137 var trace_reader: store_mod.Reader = .{};
138 try openReader(&reader_storage, &trace_reader, allocator, args[0]);
139 defer reader_storage.deinit(allocator);
140 defer trace_reader.deinit();
141
142 var stdout_buffer: [8192]u8 = undefined;
143 var stdout = sys.stdio.stdout().writer(sys.stdio.debugIo(), &stdout_buffer);
144 const source = trace_reader.source();
145 while (try source.peek()) |item| {
146 try item.writeJsonLine(&stdout.interface);
147 source.advance();
148 }
149 try stdout.interface.flush();
150 return 0;
151 }
152
153 fn summary(allocator: Allocator, args: []const []const u8) !u8 {
154 if (args.len != 1 and args.len != 3) return error.InvalidArguments;
155 if (args.len == 3 and (!std.mem.eql(u8, args[1], "--format") or !std.mem.eql(u8, args[2], "jsonl"))) {
156 return error.UnsupportedFormat;
157 }
158
159 var reader_storage: store_mod.ReaderStorage = undefined;
160 var trace_reader: store_mod.Reader = .{};
161 try openReader(&reader_storage, &trace_reader, allocator, args[0]);
162 defer reader_storage.deinit(allocator);
163 defer trace_reader.deinit();
164
165 var stdout_buffer: [8192]u8 = undefined;
166 var stdout = sys.stdio.stdout().writer(sys.stdio.debugIo(), &stdout_buffer);
167 try replay.writeSummaryJsonl(allocator, trace_reader.source(), &stdout.interface);
168 try stdout.interface.flush();
169 return 0;
170 }
171
172 fn timepoints(allocator: Allocator, args: []const []const u8) !u8 {
173 if (args.len != 1 and args.len != 3) return error.InvalidArguments;
174 if (args.len == 3 and (!std.mem.eql(u8, args[1], "--format") or !std.mem.eql(u8, args[2], "jsonl"))) {
175 return error.UnsupportedFormat;
176 }
177
178 var reader_storage: store_mod.ReaderStorage = undefined;
179 var trace_reader: store_mod.Reader = .{};
180 try openReader(&reader_storage, &trace_reader, allocator, args[0]);
181 defer reader_storage.deinit(allocator);
182 defer trace_reader.deinit();
183
184 var stdout_buffer: [8192]u8 = undefined;
185 var stdout = sys.stdio.stdout().writer(sys.stdio.debugIo(), &stdout_buffer);
186 try replay.writeTimepointsJsonl(trace_reader.source(), &stdout.interface);
187 try stdout.interface.flush();
188 return 0;
189 }
190
191 const UserEventsArgs = struct {
192 trace_path: []const u8,
193 filter: replay.UserEventFilter = .{},
194
195 fn deinit(self: *UserEventsArgs, allocator: Allocator) void {
196 if (self.filter.payload_filters.len != 0) allocator.free(self.filter.payload_filters);
197 self.* = undefined;
198 }
199 };
200
201 const UserEventStatsArgs = struct {
202 trace_path: []const u8,
203 filter: replay.UserEventFilter = .{},
204 group_field: ?[]const u8 = null,
205 metric_field: ?[]const u8 = null,
206 sort: replay.UserEventStatSort = .total,
207
208 fn deinit(self: *UserEventStatsArgs, allocator: Allocator) void {
209 if (self.filter.payload_filters.len != 0) allocator.free(self.filter.payload_filters);
210 self.* = undefined;
211 }
212 };
213
214 fn userEvents(allocator: Allocator, args: []const []const u8) !u8 {
215 var parsed = try parseUserEventsArgs(allocator, args);
216 defer parsed.deinit(allocator);
217 var reader_storage: store_mod.ReaderStorage = undefined;
218 var trace_reader: store_mod.Reader = .{};
219 try openReader(&reader_storage, &trace_reader, allocator, parsed.trace_path);
220 defer reader_storage.deinit(allocator);
221 defer trace_reader.deinit();
222
223 var stdout_buffer: [8192]u8 = undefined;
224 var stdout = sys.stdio.stdout().writer(sys.stdio.debugIo(), &stdout_buffer);
225 try replay.writeUserEventsJsonl(allocator, trace_reader.source(), parsed.filter, &stdout.interface);
226 try stdout.interface.flush();
227 return 0;
228 }
229
230 fn userEventStats(allocator: Allocator, args: []const []const u8) !u8 {
231 var parsed = try parseUserEventStatsArgs(allocator, args);
232 defer parsed.deinit(allocator);
233 var reader_storage: store_mod.ReaderStorage = undefined;
234 var trace_reader: store_mod.Reader = .{};
235 try openReader(&reader_storage, &trace_reader, allocator, parsed.trace_path);
236 defer reader_storage.deinit(allocator);
237 defer trace_reader.deinit();
238
239 var stdout_buffer: [8192]u8 = undefined;
240 var stdout = sys.stdio.stdout().writer(sys.stdio.debugIo(), &stdout_buffer);
241 try replay.writeUserEventStatsJsonl(
242 allocator,
243 trace_reader.source(),
244 parsed.filter,
245 parsed.group_field.?,
246 parsed.metric_field.?,
247 parsed.sort,
248 &stdout.interface,
249 );
250 try stdout.interface.flush();
251 return 0;
252 }
253
254 fn parseUserEventsArgs(allocator: Allocator, args: []const []const u8) !UserEventsArgs {
255 if (args.len == 0) return error.InvalidArguments;
256 var parsed = UserEventsArgs{ .trace_path = args[0] };
257 var payload_filters: std.ArrayListUnmanaged(replay.UserPayloadFilter) = .empty;
258 errdefer payload_filters.deinit(allocator);
259
260 var index: usize = 1;
261 while (index < args.len) : (index += 1) {
262 if (std.mem.eql(u8, args[index], "--format")) {
263 index += 1;
264 if (index >= args.len) return error.InvalidArguments;
265 if (!std.mem.eql(u8, args[index], "jsonl")) return error.UnsupportedFormat;
266 } else if (std.mem.eql(u8, args[index], "--label")) {
267 index += 1;
268 if (index >= args.len) return error.InvalidArguments;
269 parsed.filter.label = args[index];
270 } else if (std.mem.eql(u8, args[index], "--protocol")) {
271 index += 1;
272 if (index >= args.len) return error.InvalidArguments;
273 parsed.filter.protocol = args[index];
274 } else if (std.mem.eql(u8, args[index], "--payload")) {
275 index += 1;
276 if (index >= args.len) return error.InvalidArguments;
277 appendPayloadFilter(allocator, &payload_filters, args[index]) catch return error.InvalidArguments;
278 } else {
279 return error.UnknownArgument;
280 }
281 }
282
283 parsed.filter.payload_filters = try payloadFiltersSlice(allocator, &payload_filters);
284 return parsed;
285 }
286
287 fn parseUserEventStatsArgs(allocator: Allocator, args: []const []const u8) !UserEventStatsArgs {
288 if (args.len == 0) return error.InvalidArguments;
289 var parsed = UserEventStatsArgs{ .trace_path = args[0] };
290 var payload_filters: std.ArrayListUnmanaged(replay.UserPayloadFilter) = .empty;
291 errdefer payload_filters.deinit(allocator);
292
293 var index: usize = 1;
294 while (index < args.len) : (index += 1) {
295 if (std.mem.eql(u8, args[index], "--format")) {
296 index += 1;
297 if (index >= args.len) return error.InvalidArguments;
298 if (!std.mem.eql(u8, args[index], "jsonl")) return error.UnsupportedFormat;
299 } else if (std.mem.eql(u8, args[index], "--label")) {
300 index += 1;
301 if (index >= args.len) return error.InvalidArguments;
302 parsed.filter.label = args[index];
303 } else if (std.mem.eql(u8, args[index], "--protocol")) {
304 index += 1;
305 if (index >= args.len) return error.InvalidArguments;
306 parsed.filter.protocol = args[index];
307 } else if (std.mem.eql(u8, args[index], "--payload")) {
308 index += 1;
309 if (index >= args.len) return error.InvalidArguments;
310 appendPayloadFilter(allocator, &payload_filters, args[index]) catch return error.InvalidArguments;
311 } else if (std.mem.eql(u8, args[index], "--group-by")) {
312 index += 1;
313 if (index >= args.len) return error.InvalidArguments;
314 parsed.group_field = args[index];
315 } else if (std.mem.eql(u8, args[index], "--metric")) {
316 index += 1;
317 if (index >= args.len) return error.InvalidArguments;
318 parsed.metric_field = args[index];
319 } else if (std.mem.eql(u8, args[index], "--sort-by")) {
320 index += 1;
321 if (index >= args.len) return error.InvalidArguments;
322 parsed.sort = replay.UserEventStatSort.parse(args[index]) orelse return error.InvalidArguments;
323 } else {
324 return error.UnknownArgument;
325 }
326 }
327
328 if (parsed.group_field == null or parsed.metric_field == null) return error.InvalidArguments;
329 parsed.filter.payload_filters = try payloadFiltersSlice(allocator, &payload_filters);
330 return parsed;
331 }
332
333 fn appendPayloadFilter(
334 allocator: Allocator,
335 payload_filters: *std.ArrayListUnmanaged(replay.UserPayloadFilter),
336 text: []const u8,
337 ) !void {
338 const split = std.mem.indexOfScalar(u8, text, '=') orelse return error.InvalidPayloadFilter;
339 if (split == 0 or split + 1 >= text.len) return error.InvalidPayloadFilter;
340 try payload_filters.append(allocator, .{
341 .field = text[0..split],
342 .value = text[split + 1 ..],
343 });
344 }
345
346 fn payloadFiltersSlice(
347 allocator: Allocator,
348 payload_filters: *std.ArrayListUnmanaged(replay.UserPayloadFilter),
349 ) ![]const replay.UserPayloadFilter {
350 if (payload_filters.items.len == 0) return &.{};
351 return try payload_filters.toOwnedSlice(allocator);
352 }
353
354 fn threads(allocator: Allocator, args: []const []const u8) !u8 {
355 if (args.len != 1 and args.len != 3) return error.InvalidArguments;
356 if (args.len == 3 and (!std.mem.eql(u8, args[1], "--format") or !std.mem.eql(u8, args[2], "jsonl"))) {
357 return error.UnsupportedFormat;
358 }
359
360 var reader_storage: store_mod.ReaderStorage = undefined;
361 var trace_reader: store_mod.Reader = .{};
362 try openReader(&reader_storage, &trace_reader, allocator, args[0]);
363 defer reader_storage.deinit(allocator);
364 defer trace_reader.deinit();
365
366 var stdout_buffer: [8192]u8 = undefined;
367 var stdout = sys.stdio.stdout().writer(sys.stdio.debugIo(), &stdout_buffer);
368 try replay.writeThreadsJsonl(allocator, trace_reader.source(), &stdout.interface);
369 try stdout.interface.flush();
370 return 0;
371 }
372
373 fn objects(allocator: Allocator, args: []const []const u8) !u8 {
374 if (args.len != 1 and args.len != 3) return error.InvalidArguments;
375 if (args.len == 3 and (!std.mem.eql(u8, args[1], "--format") or !std.mem.eql(u8, args[2], "jsonl"))) {
376 return error.UnsupportedFormat;
377 }
378
379 var reader_storage: store_mod.ReaderStorage = undefined;
380 var trace_reader: store_mod.Reader = .{};
381 try openReader(&reader_storage, &trace_reader, allocator, args[0]);
382 defer reader_storage.deinit(allocator);
383 defer trace_reader.deinit();
384
385 var stdout_buffer: [8192]u8 = undefined;
386 var stdout = sys.stdio.stdout().writer(sys.stdio.debugIo(), &stdout_buffer);
387 try replay.writeObjectsJsonl(allocator, trace_reader.source(), &stdout.interface);
388 try stdout.interface.flush();
389 return 0;
390 }
391
392 fn replayCommand(allocator: Allocator, args: []const []const u8) !u8 {
393 if (args.len != 3 or !std.mem.eql(u8, args[1], "--to")) return error.InvalidArguments;
394 const target = try parseTimepoint(args[2]);
395 var reader_storage: store_mod.ReaderStorage = undefined;
396 var trace_reader: store_mod.Reader = .{};
397 try openReader(&reader_storage, &trace_reader, allocator, args[0]);
398 defer reader_storage.deinit(allocator);
399 defer trace_reader.deinit();
400 const checkpoint = try replay.nearestCheckpointTimepoint(trace_reader.source(), target);
401
402 var stdout_buffer: [4096]u8 = undefined;
403 var stdout = sys.stdio.stdout().writer(sys.stdio.debugIo(), &stdout_buffer);
404 var stream = pretty.json.Writer.init(&stdout.interface, .minified);
405 const object = try stream.object();
406 try object.field("event", "replay");
407 try object.formattedString(
408 "target",
409 timepoint_string_capacity,
410 "{d}:{d}:{d}",
411 .{ target.epoch, target.thread_id, target.seq },
412 );
413 if (checkpoint) |item| {
414 try object.formattedString(
415 "checkpoint",
416 timepoint_string_capacity,
417 "{d}:{d}:{d}",
418 .{ item.epoch, item.thread_id, item.seq },
419 );
420 } else {
421 try object.field("checkpoint", null);
422 }
423 try object.endLine();
424 try stdout.interface.flush();
425 return 0;
426 }
427
428 fn checkpoints(allocator: Allocator, args: []const []const u8) !u8 {
429 if (args.len != 1) return error.InvalidArguments;
430 var reader_storage: store_mod.ReaderStorage = undefined;
431 var trace_reader: store_mod.Reader = .{};
432 try openReader(&reader_storage, &trace_reader, allocator, args[0]);
433 defer reader_storage.deinit(allocator);
434 defer trace_reader.deinit();
435
436 var stdout_buffer: [8192]u8 = undefined;
437 var stdout = sys.stdio.stdout().writer(sys.stdio.debugIo(), &stdout_buffer);
438 const source = trace_reader.source();
439 while (try source.peek()) |item| {
440 if (item.kind == .checkpoint) try item.writeJsonLine(&stdout.interface);
441 source.advance();
442 }
443 try stdout.interface.flush();
444 return 0;
445 }
446
447 fn frames(allocator: Allocator, args: []const []const u8) !u8 {
448 if (args.len != 3 or !std.mem.eql(u8, args[1], "--at")) return error.InvalidArguments;
449 const target = try parseTimepoint(args[2]);
450 var reader_storage: store_mod.ReaderStorage = undefined;
451 var trace_reader: store_mod.Reader = .{};
452 try openReader(&reader_storage, &trace_reader, allocator, args[0]);
453 defer reader_storage.deinit(allocator);
454 defer trace_reader.deinit();
455 const stack = try replay.framesAtAlloc(allocator, trace_reader.source(), target);
456 defer allocator.free(stack);
457
458 var stdout_buffer: [8192]u8 = undefined;
459 var stdout = sys.stdio.stdout().writer(sys.stdio.debugIo(), &stdout_buffer);
460 for (stack) |frame| {
461 try frame.writeJsonLine(&stdout.interface);
462 }
463 try stdout.interface.flush();
464 return 0;
465 }
466
467 fn reverseWatch(allocator: Allocator, args: []const []const u8) !u8 {
468 if (args.len != 5 or
469 !std.mem.eql(u8, args[1], "--from") or
470 !std.mem.eql(u8, args[3], "--object"))
471 {
472 return error.InvalidArguments;
473 }
474 const target = try parseTimepoint(args[2]);
475 const object_id = try std.fmt.parseUnsigned(u64, args[4], 0);
476 var reader_storage: store_mod.ReaderStorage = undefined;
477 var trace_reader: store_mod.Reader = .{};
478 try openReader(&reader_storage, &trace_reader, allocator, args[0]);
479 defer reader_storage.deinit(allocator);
480 defer trace_reader.deinit();
481
482 var stdout_buffer: [8192]u8 = undefined;
483 var stdout = sys.stdio.stdout().writer(sys.stdio.debugIo(), &stdout_buffer);
484 var matched: ?event.Event = null;
485 defer if (matched) |*item| item.deinit(allocator);
486 const source = trace_reader.source();
487 while (try source.peek()) |item| {
488 if (item.timepoint.beforeOrEqual(target) and item.object_id == object_id and
489 (item.kind == .allocation or item.kind == .free))
490 {
491 const owned = try item.cloneAlloc(allocator);
492 if (matched) |*previous| previous.deinit(allocator);
493 matched = owned;
494 }
495 source.advance();
496 }
497 if (matched) |item| try item.writeJsonLine(&stdout.interface);
498 try stdout.interface.flush();
499 return 0;
500 }
501
502 fn maximumReaderLimits(path: []const u8) store_mod.ReaderLimits {
503 return store_mod.ReaderLimits.fromOpen(path, .{
504 .max_event_bytes = store_mod.Limits.maximum_event_bytes,
505 .max_chunk_bytes = store_mod.Limits.maximum_chunk_bytes,
506 });
507 }
508
509 fn openReader(
510 reader_storage: *store_mod.ReaderStorage,
511 trace_reader: *store_mod.Reader,
512 allocator: Allocator,
513 path: []const u8,
514 ) !void {
515 reader_storage.* = try store_mod.ReaderStorage.init(allocator, maximumReaderLimits(path));
516 errdefer reader_storage.deinit(allocator);
517 reader_storage.activate();
518 try trace_reader.open(reader_storage, path);
519 }
520
521 fn verifyTrace(allocator: Allocator, path: []const u8) !store_mod.VerifyResult {
522 var reader_storage = try store_mod.ReaderStorage.init(allocator, maximumReaderLimits(path));
523 defer reader_storage.deinit(allocator);
524 reader_storage.activate();
525 return try store_mod.verify(&reader_storage, path);
526 }
527
528 fn runChild(allocator: Allocator, argv: []const []const u8) !i64 {
529 var io_state = sys.thread.initThreadedIo(allocator, .{});
530 defer io_state.deinit();
531 const io = io_state.io();
532
533 var child = try sys.process.spawn(io, .{
534 .argv = argv,
535 .stdin = .inherit,
536 .stdout = .inherit,
537 .stderr = .inherit,
538 });
539 const term = try sys.process.wait(&child, io);
540 return sys.process.exitCode(term);
541 }
542
543 fn joinArgs(allocator: Allocator, args: []const []const u8) ![]u8 {
544 var out = std.Io.Writer.Allocating.init(allocator);
545 errdefer out.deinit();
546 for (args, 0..) |arg, index| {
547 if (index != 0) try out.writer.writeByte(0);
548 try out.writer.writeAll(arg);
549 }
550 return try out.toOwnedSlice();
551 }
552
553 fn manifestForRecord() store_mod.Manifest {
554 return .{
555 .target_triple = sys.capabilities.current.archName(),
556 .build_id = "trace-cli",
557 .mode = "record",
558 .status = "open",
559 };
560 }
561
562 fn parseTimepoint(text: []const u8) !event.Timepoint {
563 if (std.mem.indexOfScalar(u8, text, ':') == null) {
564 return .{ .thread_id = 1, .seq = try std.fmt.parseUnsigned(u64, text, 0) };
565 }
566
567 var parts = std.mem.splitScalar(u8, text, ':');
568 const epoch_text = parts.next() orelse return error.InvalidTimepoint;
569 const thread_text = parts.next() orelse return error.InvalidTimepoint;
570 const seq_text = parts.next() orelse return error.InvalidTimepoint;
571 if (parts.next() != null) return error.InvalidTimepoint;
572
573 return .{
574 .epoch = try std.fmt.parseUnsigned(u64, epoch_text, 0),
575 .thread_id = @intCast(try std.fmt.parseUnsigned(u64, thread_text, 0)),
576 .seq = try std.fmt.parseUnsigned(u64, seq_text, 0),
577 };
578 }
579
580 fn writeUsage(allocator: Allocator, file: sys.stdio.File) !void {
581 var arena_state = std.heap.ArenaAllocator.init(allocator);
582 defer arena_state.deinit();
583 const builder = pretty.Builder.init(arena_state.allocator());
584 try pretty_usage.Terminal.init(allocator, file, .{}).writeDoc(try usageDocAlloc(builder));
585 }
586
587 fn renderUsageAlloc(allocator: Allocator, options: pretty.LayoutOptions) ![]u8 {
588 var arena_state = std.heap.ArenaAllocator.init(allocator);
589 defer arena_state.deinit();
590 const builder = pretty.Builder.init(arena_state.allocator());
591 const doc = try usageDocAlloc(builder);
592 return try pretty.renderAlloc(allocator, doc, options);
593 }
594
595 fn usageDocAlloc(builder: pretty.Builder) !pretty.Doc {
596 var parts: std.ArrayListUnmanaged(pretty.Doc) = .empty;
597 try parts.append(builder.allocator, try builder.styledText(.keyword, "usage"));
598 try parts.append(builder.allocator, try builder.punct(":"));
599 try parts.append(builder.allocator, pretty.hardline);
600 for (usage_commands) |command| {
601 try parts.append(builder.allocator, try builder.spaces(2));
602 try parts.append(builder.allocator, try builder.nest(4, try builder.styledWords(.name, command)));
603 try parts.append(builder.allocator, pretty.hardline);
604 }
605 return try builder.concat(parts.items);
606 }
607
608 test "timepoint parser accepts full and seq-only forms" {
609 try std.testing.expect(event.Timepoint.eql(
610 .{ .epoch = 1, .thread_id = 2, .seq = 3 },
611 try parseTimepoint("1:2:3"),
612 ));
613 try std.testing.expect(event.Timepoint.eql(
614 .{ .epoch = 0, .thread_id = 1, .seq = 99 },
615 try parseTimepoint("99"),
616 ));
617 }
618
619 test "usage renders through pretty plain and colored output" {
620 const plain = try renderUsageAlloc(std.testing.allocator, .{ .width = 88 });
621 defer std.testing.allocator.free(plain);
622 try std.testing.expect(std.mem.indexOf(u8, plain, "\x1b[") == null);
623 try std.testing.expect(std.mem.indexOf(u8, plain, "usage:\n") != null);
624 try std.testing.expect(std.mem.indexOf(u8, plain, "trace user-events <trace>") != null);
625
626 const colored = try renderUsageAlloc(std.testing.allocator, .{ .width = 88, .color = .ansi });
627 defer std.testing.allocator.free(colored);
628 try std.testing.expect(std.mem.indexOf(u8, colored, "\x1b[") != null);
629 }
630
631 test "usage wraps long command lines at terminal width" {
632 const rendered = try renderUsageAlloc(std.testing.allocator, .{ .width = 48 });
633 defer std.testing.allocator.free(rendered);
634 try std.testing.expect(std.mem.indexOf(u8, rendered, "trace user-event-stats <trace> --group-by\n <payload-field>") != null);
635 }
636
637 test "errors render through pretty plain and colored output" {
638 const plain = try pretty_usage.renderErrorTextAlloc(
639 std.testing.allocator,
640 "trace",
641 "InvalidArguments",
642 .{ .layout = .{ .width = 88 } },
643 );
644 defer std.testing.allocator.free(plain);
645 try std.testing.expectEqualStrings("trace: InvalidArguments\n", plain);
646
647 const colored = try pretty_usage.renderErrorTextAlloc(
648 std.testing.allocator,
649 "trace",
650 "InvalidArguments",
651 .{ .layout = .{ .width = 88, .color = .ansi } },
652 );
653 defer std.testing.allocator.free(colored);
654 try std.testing.expect(std.mem.indexOf(u8, colored, "\x1b[") != null);
655 try std.testing.expect(std.mem.indexOf(u8, colored, "InvalidArguments") != null);
656 }
657
658 test "user event argument parser accepts filters" {
659 var parsed = try parseUserEventsArgs(std.testing.allocator, &.{
660 "run.trace",
661 "--label",
662 "compiler.query.hit",
663 "--protocol",
664 "tiny.compiler.query/v1",
665 "--payload",
666 "product_kind=function_summary",
667 "--payload",
668 "product_identity=8",
669 "--format",
670 "jsonl",
671 });
672 defer parsed.deinit(std.testing.allocator);
673
674 try std.testing.expectEqualStrings("run.trace", parsed.trace_path);
675 try std.testing.expectEqualStrings("compiler.query.hit", parsed.filter.label.?);
676 try std.testing.expectEqualStrings("tiny.compiler.query/v1", parsed.filter.protocol.?);
677 try std.testing.expectEqual(@as(usize, 2), parsed.filter.payload_filters.len);
678 try std.testing.expectEqualStrings("product_kind", parsed.filter.payload_filters[0].field);
679 try std.testing.expectEqualStrings("function_summary", parsed.filter.payload_filters[0].value);
680 try std.testing.expectEqualStrings("product_identity", parsed.filter.payload_filters[1].field);
681 try std.testing.expectEqualStrings("8", parsed.filter.payload_filters[1].value);
682 try std.testing.expectError(error.UnsupportedFormat, parseUserEventsArgs(std.testing.allocator, &.{ "run.trace", "--format", "text" }));
683 try std.testing.expectError(error.InvalidArguments, parseUserEventsArgs(std.testing.allocator, &.{ "run.trace", "--payload", "product_kind" }));
684 }
685
686 test "user event stats argument parser requires group and metric" {
687 var parsed = try parseUserEventStatsArgs(std.testing.allocator, &.{
688 "run.trace",
689 "--label",
690 "compiler.query.finish",
691 "--protocol",
692 "tiny.compiler.query/v1",
693 "--payload",
694 "event=query_finish",
695 "--payload",
696 "product_kind=partial_evaluation",
697 "--group-by",
698 "product_kind",
699 "--metric",
700 "elapsed_ns",
701 "--sort-by",
702 "mean",
703 "--format",
704 "jsonl",
705 });
706 defer parsed.deinit(std.testing.allocator);
707
708 try std.testing.expectEqualStrings("run.trace", parsed.trace_path);
709 try std.testing.expectEqualStrings("compiler.query.finish", parsed.filter.label.?);
710 try std.testing.expectEqualStrings("tiny.compiler.query/v1", parsed.filter.protocol.?);
711 try std.testing.expectEqual(@as(usize, 2), parsed.filter.payload_filters.len);
712 try std.testing.expectEqualStrings("event", parsed.filter.payload_filters[0].field);
713 try std.testing.expectEqualStrings("query_finish", parsed.filter.payload_filters[0].value);
714 try std.testing.expectEqualStrings("product_kind", parsed.filter.payload_filters[1].field);
715 try std.testing.expectEqualStrings("partial_evaluation", parsed.filter.payload_filters[1].value);
716 try std.testing.expectEqualStrings("product_kind", parsed.group_field.?);
717 try std.testing.expectEqualStrings("elapsed_ns", parsed.metric_field.?);
718 try std.testing.expectEqual(replay.UserEventStatSort.mean, parsed.sort);
719 try std.testing.expectError(error.InvalidArguments, parseUserEventStatsArgs(std.testing.allocator, &.{ "run.trace", "--group-by", "product_kind" }));
720 try std.testing.expectError(error.InvalidArguments, parseUserEventStatsArgs(std.testing.allocator, &.{ "run.trace", "--metric", "elapsed_ns" }));
721 try std.testing.expectError(error.InvalidArguments, parseUserEventStatsArgs(std.testing.allocator, &.{ "run.trace", "--group-by", "product_kind", "--metric", "elapsed_ns", "--sort-by", "median" }));
722 }
723
724 test "record command creates a verifiable trace" {
725 if (!sys.capabilities.current.isLinux()) return error.SkipZigTest;
726
727 const allocator = std.testing.allocator;
728 var tmp = std.testing.tmpDir(.{});
729 defer tmp.cleanup();
730
731 const tmp_root = try tmp.parent_dir.realPathFileAlloc(sys.fs.debugIo(), tmp.sub_path[0..], allocator);
732 defer allocator.free(tmp_root);
733 const trace_path = try sys.path.join(allocator, &.{ tmp_root, "cli-record.trace" });
734 defer allocator.free(trace_path);
735
736 const args = [_][]const u8{
737 "trace",
738 "record",
739 "--trace",
740 trace_path,
741 "--",
742 "/bin/true",
743 };
744 try std.testing.expectEqual(@as(u8, 0), try dispatch(allocator, &args));
745
746 const result = try verifyTrace(allocator, trace_path);
747 try std.testing.expectEqual(@as(u64, 3), result.event_count);
748 }