diagram: add program to generate various visual representations of the simulation trace
This commit is contained in:
@@ -0,0 +1,260 @@
|
||||
const std = @import("std");
|
||||
|
||||
const Interval = struct {
|
||||
node: u32,
|
||||
task: u64,
|
||||
state: []const u8,
|
||||
start: u64,
|
||||
end: u64,
|
||||
};
|
||||
|
||||
const StateTick = struct {
|
||||
node: u32,
|
||||
task: u64,
|
||||
state: []const u8,
|
||||
time: u64,
|
||||
};
|
||||
|
||||
const TraceEvent = struct {
|
||||
time: u64,
|
||||
event: []const u8,
|
||||
node: ?u32 = null,
|
||||
task: ?u64 = null,
|
||||
state: ?[]const u8 = null,
|
||||
};
|
||||
|
||||
const TaskKey = struct {
|
||||
node: u32,
|
||||
task: u64,
|
||||
};
|
||||
|
||||
const ActiveState = struct {
|
||||
state: []const u8,
|
||||
start: u64,
|
||||
};
|
||||
|
||||
pub fn renderFile(io: std.Io, gpa: std.mem.Allocator, trace_path: []const u8, output_path: []const u8, tick_us: u64) !void {
|
||||
try validateTickSize(tick_us);
|
||||
|
||||
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, tick_us, &writer.interface);
|
||||
try writer.interface.flush();
|
||||
}
|
||||
|
||||
pub fn render(io: std.Io, gpa: std.mem.Allocator, trace_path: []const u8, tick_us: u64, writer: *std.Io.Writer) !void {
|
||||
try validateTickSize(tick_us);
|
||||
|
||||
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 ticks: std.ArrayList(StateTick) = .empty;
|
||||
var lanes: std.ArrayList(TaskKey) = .empty;
|
||||
const max_time = try inferIntervals(arena_alloc, trace_bytes, &intervals, &ticks, &lanes);
|
||||
sortLanes(lanes.items);
|
||||
|
||||
try writeAscii(gpa, writer, intervals.items, ticks.items, lanes.items, max_time, tick_us);
|
||||
}
|
||||
|
||||
fn validateTickSize(tick_us: u64) !void {
|
||||
if (tick_us == 0) return error.InvalidTickSize;
|
||||
}
|
||||
|
||||
fn inferIntervals(
|
||||
arena: std.mem.Allocator,
|
||||
trace_bytes: []const u8,
|
||||
intervals: *std.ArrayList(Interval),
|
||||
ticks: *std.ArrayList(StateTick),
|
||||
lanes: *std.ArrayList(TaskKey),
|
||||
) !u64 {
|
||||
var active: std.AutoHashMap(TaskKey, ActiveState) = .init(arena);
|
||||
var seen_lanes: std.AutoHashMap(TaskKey, void) = .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.? };
|
||||
try rememberLane(arena, lanes, &seen_lanes, key);
|
||||
|
||||
const state = try arena.dupe(u8, event.state.?);
|
||||
if (try active.fetchPut(key, .{ .state = state, .start = event.time })) |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.? };
|
||||
try rememberLane(arena, lanes, &seen_lanes, key);
|
||||
|
||||
if (active.fetchRemove(key)) |previous_entry| {
|
||||
try appendInterval(arena, intervals, ticks, key, previous_entry.value, event.time);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const final_time = max_time + 1;
|
||||
var active_iter = active.iterator();
|
||||
while (active_iter.next()) |entry| {
|
||||
try appendInterval(arena, intervals, ticks, entry.key_ptr.*, entry.value_ptr.*, final_time);
|
||||
}
|
||||
return final_time;
|
||||
}
|
||||
|
||||
fn rememberLane(arena: std.mem.Allocator, lanes: *std.ArrayList(TaskKey), seen_lanes: *std.AutoHashMap(TaskKey, void), key: TaskKey) !void {
|
||||
if (seen_lanes.contains(key)) return;
|
||||
try seen_lanes.put(key, {});
|
||||
try lanes.append(arena, key);
|
||||
}
|
||||
|
||||
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,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try intervals.append(arena, .{
|
||||
.node = key.node,
|
||||
.task = key.task,
|
||||
.state = state.state,
|
||||
.start = state.start,
|
||||
.end = interval_end,
|
||||
});
|
||||
}
|
||||
|
||||
fn sortLanes(lanes: []TaskKey) void {
|
||||
if (lanes.len < 2) return;
|
||||
|
||||
var i: usize = 1;
|
||||
while (i < lanes.len) : (i += 1) {
|
||||
const current = lanes[i];
|
||||
var j = i;
|
||||
while (j > 0 and laneLess(current, lanes[j - 1])) : (j -= 1) {
|
||||
lanes[j] = lanes[j - 1];
|
||||
}
|
||||
lanes[j] = current;
|
||||
}
|
||||
}
|
||||
|
||||
fn laneLess(a: TaskKey, b: TaskKey) bool {
|
||||
if (a.node != b.node) return a.node < b.node;
|
||||
return a.task < b.task;
|
||||
}
|
||||
|
||||
fn writeAscii(
|
||||
allocator: std.mem.Allocator,
|
||||
writer: *std.Io.Writer,
|
||||
intervals: []const Interval,
|
||||
ticks: []const StateTick,
|
||||
lanes: []const TaskKey,
|
||||
max_time: u64,
|
||||
tick_us: u64,
|
||||
) !void {
|
||||
try writer.writeAll("legend: x=running r=ready b=blocked/sleeping w=waiting R=returned f=failed .=idle\n");
|
||||
try writer.print("tick: {}us; duplicate rows suppressed\n", .{tick_us});
|
||||
try writer.writeAll("columns:\n");
|
||||
for (lanes, 0..) |lane, index| {
|
||||
try writer.print(" {}: node {} task {}\n", .{ index, lane.node, lane.task });
|
||||
}
|
||||
try writer.writeAll("\n");
|
||||
|
||||
try writer.writeAll("time_us | ");
|
||||
for (lanes, 0..) |_, index| {
|
||||
try writer.writeByte(indexChar(index));
|
||||
}
|
||||
try writer.writeAll("\n");
|
||||
try writer.writeAll("---------+-");
|
||||
for (lanes) |_| {
|
||||
try writer.writeByte('-');
|
||||
}
|
||||
try writer.writeAll("\n");
|
||||
|
||||
const previous = try allocator.alloc(u8, lanes.len);
|
||||
defer allocator.free(previous);
|
||||
const current = try allocator.alloc(u8, lanes.len);
|
||||
defer allocator.free(current);
|
||||
@memset(previous, 0);
|
||||
|
||||
var have_previous = false;
|
||||
var row_start: u64 = 0;
|
||||
while (row_start <= max_time) : (row_start += tick_us) {
|
||||
const row_end = row_start + tick_us;
|
||||
for (lanes, 0..) |lane, index| {
|
||||
current[index] = stateAt(intervals, ticks, lane, row_start, row_end);
|
||||
}
|
||||
|
||||
if (!have_previous or !std.mem.eql(u8, previous, current)) {
|
||||
try writeRow(writer, row_start, current);
|
||||
@memcpy(previous, current);
|
||||
have_previous = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn writeRow(writer: *std.Io.Writer, row_start: u64, states: []const u8) !void {
|
||||
try writer.print("{d: >8} | ", .{row_start});
|
||||
try writer.writeAll(states);
|
||||
try writer.writeAll("\n");
|
||||
}
|
||||
|
||||
fn stateAt(intervals: []const Interval, ticks: []const StateTick, lane: TaskKey, row_start: u64, row_end: u64) u8 {
|
||||
for (intervals) |interval| {
|
||||
if (interval.node != lane.node or interval.task != lane.task)
|
||||
continue;
|
||||
if (interval.start < row_end and interval.end > row_start)
|
||||
return stateChar(interval.state);
|
||||
}
|
||||
|
||||
for (ticks) |tick| {
|
||||
if (tick.node == lane.node and tick.task == lane.task and tick.time >= row_start and tick.time < row_end)
|
||||
return stateChar(tick.state);
|
||||
}
|
||||
|
||||
return '.';
|
||||
}
|
||||
|
||||
fn stateChar(state: []const u8) u8 {
|
||||
if (std.mem.eql(u8, state, "running")) return 'x';
|
||||
if (std.mem.eql(u8, state, "ready")) return 'r';
|
||||
if (std.mem.eql(u8, state, "sleeping") or std.mem.eql(u8, state, "blocked")) return 'b';
|
||||
if (std.mem.startsWith(u8, state, "waiting")) return 'w';
|
||||
if (std.mem.eql(u8, state, "returned")) return 'R';
|
||||
if (std.mem.eql(u8, state, "failed")) return 'f';
|
||||
if (std.mem.eql(u8, state, "polling")) return 'p';
|
||||
return '?';
|
||||
}
|
||||
|
||||
fn indexChar(index: usize) u8 {
|
||||
const alphabet = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
return alphabet[index % alphabet.len];
|
||||
}
|
||||
Reference in New Issue
Block a user