const std = @import("std"); const Interval = struct { node: u32, // Stable simulator node identifier. Intervals with the same node are grouped together. task: u64, // Stable scheduler task identifier. Each unique node/task pair gets one timeline lane. state: []const u8, // Visual state name. Must match one of the keys in the generated `colors` map. start: u64, // Inclusive start time of this state interval, in trace units. end: u64, // Exclusive end time of this state interval, in trace units. reason: []const u8, // Human-readable explanation shown in the hover tooltip. }; const DiskInterval = struct { node: u32, op: []const u8, start: u64, end: u64, detail: []const u8, }; const StateTick = struct { node: u32, task: u64, state: []const u8, time: u64, reason: []const u8, }; const Marker = struct { time: u64, label: []const u8, }; const TraceEvent = struct { time: u64, event: []const u8, node: ?u32 = null, task: ?u64 = null, state: ?[]const u8 = null, reason: ?[]const u8 = null, from: ?u64 = null, to: ?u64 = null, op: ?[]const u8 = null, start: ?u64 = null, end: ?u64 = null, detail: ?[]const u8 = null, }; const TaskKey = struct { node: u32, task: u64, }; const ActiveState = struct { state: []const u8, start: u64, reason: []const u8, }; pub fn renderFile(io: std.Io, gpa: std.mem.Allocator, trace_path: []const u8, output_path: []const u8) !void { const file = try std.Io.Dir.cwd().createFile(io, output_path, .{}); defer file.close(io); var writer = file.writerStreaming(io, &.{}); try render(io, gpa, trace_path, &writer.interface); try writer.interface.flush(); } pub fn render(io: std.Io, gpa: std.mem.Allocator, trace_path: []const u8, writer: *std.Io.Writer) !void { const trace_bytes = try std.Io.Dir.cwd().readFileAlloc(io, trace_path, gpa, .limited(64 * 1024 * 1024)); defer gpa.free(trace_bytes); var arena = std.heap.ArenaAllocator.init(gpa); defer arena.deinit(); const arena_alloc = arena.allocator(); var intervals: std.ArrayList(Interval) = .empty; var disk_intervals: std.ArrayList(DiskInterval) = .empty; var ticks: std.ArrayList(StateTick) = .empty; var markers: std.ArrayList(Marker) = .empty; try inferIntervals(arena_alloc, trace_bytes, &intervals, &disk_intervals, &ticks, &markers); try writeHtml(writer, intervals.items, disk_intervals.items, ticks.items, markers.items); } fn inferIntervals( arena: std.mem.Allocator, trace_bytes: []const u8, intervals: *std.ArrayList(Interval), disk_intervals: *std.ArrayList(DiskInterval), ticks: *std.ArrayList(StateTick), markers: *std.ArrayList(Marker), ) !void { _ = markers; var active: std.AutoHashMap(TaskKey, ActiveState) = .init(arena); var max_time: u64 = 0; var lines = std.mem.splitScalar(u8, trace_bytes, '\n'); while (lines.next()) |line| { const trimmed = std.mem.trim(u8, line, " \t\r"); if (trimmed.len == 0) continue; var parsed = try std.json.parseFromSlice(TraceEvent, arena, trimmed, .{ .ignore_unknown_fields = true }); defer parsed.deinit(); const event = parsed.value; max_time = @max(max_time, event.time); if (std.mem.eql(u8, event.event, "state")) { const key = TaskKey{ .node = event.node.?, .task = event.task.? }; const state = try arena.dupe(u8, event.state.?); const reason = try arena.dupe(u8, event.reason orelse event.state.?); if (try active.fetchPut(key, .{ .state = state, .start = event.time, .reason = reason })) |previous_entry| { try appendInterval(arena, intervals, ticks, key, previous_entry.value, event.time); } } else if (std.mem.eql(u8, event.event, "task_removed")) { const key = TaskKey{ .node = event.node.?, .task = event.task.? }; if (active.fetchRemove(key)) |previous_entry| { try appendInterval(arena, intervals, ticks, key, previous_entry.value, event.time); } } else if (std.mem.eql(u8, event.event, "disk")) { try disk_intervals.append(arena, .{ .node = event.node.?, .op = try arena.dupe(u8, event.op.?), .start = event.start.?, .end = event.end.?, .detail = try arena.dupe(u8, event.detail orelse ""), }); } } const final_time = max_time + 1; var active_iter = active.iterator(); while (active_iter.next()) |entry| { const key = entry.key_ptr.*; const state = entry.value_ptr.*; try appendInterval(arena, intervals, ticks, key, state, final_time); } } fn appendInterval( arena: std.mem.Allocator, intervals: *std.ArrayList(Interval), ticks: *std.ArrayList(StateTick), key: TaskKey, state: ActiveState, end: u64, ) !void { var interval_end = end; if (state.start == interval_end and (std.mem.eql(u8, state.state, "returned") or std.mem.eql(u8, state.state, "failed"))) { interval_end += 1; } if (state.start >= interval_end) { try ticks.append(arena, .{ .node = key.node, .task = key.task, .state = state.state, .time = state.start, .reason = state.reason, }); return; } try intervals.append(arena, .{ .node = key.node, .task = key.task, .state = state.state, .start = state.start, .end = interval_end, .reason = state.reason, }); } fn writeJsonString(writer: *std.Io.Writer, text: []const u8) !void { try writer.writeByte('"'); for (text) |byte| { switch (byte) { '"' => try writer.writeAll("\\\""), '\\' => try writer.writeAll("\\\\"), '\n' => try writer.writeAll("\\n"), '\r' => try writer.writeAll("\\r"), '\t' => try writer.writeAll("\\t"), else => try writer.writeByte(byte), } } try writer.writeByte('"'); } fn writeTraceJson(writer: *std.Io.Writer, intervals: []const Interval, disk_intervals: []const DiskInterval, ticks: []const StateTick, markers: []const Marker) !void { try writer.writeAll( \\{ \\ unit: "us", \\ intervals: [ \\ ); for (intervals, 0..) |interval, index| { if (index != 0) try writer.writeAll(",\n"); try writer.print(" {{ node: {}, task: {}, state: ", .{ interval.node, interval.task }); try writeJsonString(writer, interval.state); try writer.print(", start: {}, end: {}, reason: ", .{ interval.start, interval.end }); try writeJsonString(writer, interval.reason); try writer.writeAll(" }"); } try writer.writeAll( \\ \\ ], \\ disk: [ \\ ); for (disk_intervals, 0..) |interval, index| { if (index != 0) try writer.writeAll(",\n"); try writer.print(" {{ node: {}, op: ", .{interval.node}); try writeJsonString(writer, interval.op); try writer.print(", start: {}, end: {}, detail: ", .{ interval.start, interval.end }); try writeJsonString(writer, interval.detail); try writer.writeAll(" }"); } try writer.writeAll( \\ \\ ], \\ ticks: [ \\ ); for (ticks, 0..) |tick, index| { if (index != 0) try writer.writeAll(",\n"); try writer.print(" {{ node: {}, task: {}, state: ", .{ tick.node, tick.task }); try writeJsonString(writer, tick.state); try writer.print(", time: {}, reason: ", .{tick.time}); try writeJsonString(writer, tick.reason); try writer.writeAll(" }"); } try writer.writeAll( \\ \\ ], \\ markers: [ \\ ); for (markers, 0..) |marker, index| { if (index != 0) try writer.writeAll(",\n"); try writer.print(" {{ time: {}, label: ", .{marker.time}); try writeJsonString(writer, marker.label); try writer.writeAll(" }"); } try writer.writeAll( \\ \\ ], \\} ); } fn writeHtml(writer: *std.Io.Writer, intervals: []const Interval, disk_intervals: []const DiskInterval, ticks: []const StateTick, markers: []const Marker) !void { try writer.writeAll( \\ \\ \\
\\ \\ \\