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( \\ \\ \\ \\ \\ \\ Zigmulator Task Timeline \\ \\ \\ \\
\\
\\

Zigmulator Task Timeline

\\
\\ \\ \\ \\ \\
\\
\\
\\
\\
\\
\\
\\
\\
\\
\\
\\ \\ \\ \\ ); }