lib/tracy/src/csv.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const sys = @import("sys");
3 const event = @import("event.zig");
4 const record_mod = @import("record.zig");
5
6 pub const Export = enum {
7 zones,
8 messages,
9 plots,
10 allocs,
11
12 pub fn fromName(text: []const u8) ?Export {
13 if (std.mem.eql(u8, text, "zones")) return .zones;
14 if (std.mem.eql(u8, text, "messages")) return .messages;
15 if (std.mem.eql(u8, text, "plots")) return .plots;
16 if (std.mem.eql(u8, text, "allocs")) return .allocs;
17 return null;
18 }
19 };
20
21 const ActiveZone = struct {
22 name: []u8,
23 text: ?[]u8 = null,
24 file: ?[]u8 = null,
25 line: u32 = 0,
26 start_ns: u64 = 0,
27 thread: u64 = 0,
28 value: ?u64 = null,
29
30 fn deinit(self: *ActiveZone, allocator: std.mem.Allocator) void {
31 allocator.free(self.name);
32 if (self.text) |text| allocator.free(text);
33 if (self.file) |file| allocator.free(file);
34 self.* = undefined;
35 }
36 };
37
38 pub fn writeFromJsonlPath(
39 allocator: std.mem.Allocator,
40 path: []const u8,
41 writer: *std.Io.Writer,
42 mode: Export,
43 ) !void {
44 var file = try sys.fs.cwd().openFile(sys.fs.debugIo(), path, .{});
45 defer file.close(sys.fs.debugIo());
46
47 switch (mode) {
48 .zones => try writer.writeAll("name,src_file,src_line,ns_since_start,exec_time_ns,thread,value,text\n"),
49 .messages => try writer.writeAll("ns_since_start,time_ns,thread,text\n"),
50 .plots => try writer.writeAll(
51 "event,name,ns_since_start,time_ns,thread,value,kind,unit,step,fill,color\n",
52 ),
53 .allocs => try writer.writeAll("kind,name,ns_since_start,time_ns,thread,address,size\n"),
54 }
55
56 var active_zones: std.AutoHashMapUnmanaged(u64, ActiveZone) = .{};
57 defer {
58 var iter = active_zones.valueIterator();
59 while (iter.next()) |zone| zone.deinit(allocator);
60 active_zones.deinit(allocator);
61 }
62
63 var base_ns: ?u64 = null;
64 var buffer: [64 * 1024]u8 = undefined;
65 var reader = file.reader(sys.fs.debugIo(), &buffer);
66 while (true) {
67 const line = reader.interface.takeDelimiter('\n') catch |err| switch (err) {
68 error.ReadFailed => return reader.err.?,
69 else => return err,
70 };
71 const actual = line orelse break;
72 const text = std.mem.trim(u8, actual, " \t\r\n");
73 if (text.len == 0) continue;
74 var parsed = (try record_mod.parseEventLine(allocator, text)) orelse continue;
75 defer parsed.deinit();
76 if (base_ns == null and parsed.time_ns != 0) base_ns = parsed.time_ns;
77 switch (mode) {
78 .zones => try handleZoneEvent(allocator, writer, &active_zones, base_ns orelse parsed.time_ns, parsed),
79 .messages => if (parsed.kind == .message) try writeMessage(writer, base_ns orelse parsed.time_ns, parsed),
80 .plots => if (parsed.kind == .plot or parsed.kind == .plot_config) {
81 try writePlot(writer, base_ns orelse parsed.time_ns, parsed);
82 },
83 .allocs => if (parsed.kind == .alloc or parsed.kind == .free) try writeAlloc(writer, base_ns orelse parsed.time_ns, parsed),
84 }
85 }
86 }
87
88 fn handleZoneEvent(
89 allocator: std.mem.Allocator,
90 writer: *std.Io.Writer,
91 active_zones: *std.AutoHashMapUnmanaged(u64, ActiveZone),
92 base_ns: u64,
93 parsed: event.Parsed,
94 ) !void {
95 switch (parsed.kind) {
96 .zone_begin => {
97 if (parsed.id == 0) return;
98 if (active_zones.fetchRemove(parsed.id)) |removed| {
99 var old = removed.value;
100 old.deinit(allocator);
101 }
102 try active_zones.put(allocator, parsed.id, .{
103 .name = try allocator.dupe(u8, parsed.name orelse "<zone>"),
104 .file = try dupeOptional(allocator, parsed.file),
105 .line = parsed.line,
106 .start_ns = parsed.time_ns,
107 .thread = parsed.thread,
108 });
109 },
110 .zone_name => {
111 const name = parsed.name orelse return;
112 const zone = active_zones.getPtr(parsed.id) orelse return;
113 allocator.free(zone.name);
114 zone.name = try allocator.dupe(u8, name);
115 },
116 .zone_value => {
117 const value = parsed.value_u64 orelse return;
118 const zone = active_zones.getPtr(parsed.id) orelse return;
119 zone.value = value;
120 },
121 .zone_text => {
122 const text = parsed.text orelse return;
123 const zone = active_zones.getPtr(parsed.id) orelse return;
124 zone.text = try appendText(allocator, zone.text, text);
125 },
126 .zone_end => {
127 const removed = active_zones.fetchRemove(parsed.id) orelse return;
128 var zone = removed.value;
129 defer zone.deinit(allocator);
130 const duration = if (parsed.time_ns >= zone.start_ns) parsed.time_ns - zone.start_ns else 0;
131 try writeCsvString(writer, zone.name);
132 try writer.writeByte(',');
133 if (zone.file) |file| try writeCsvString(writer, file);
134 try writer.print(",{d},{d},{d},{d},", .{
135 zone.line,
136 nsSince(base_ns, zone.start_ns),
137 duration,
138 zone.thread,
139 });
140 if (zone.value) |value| try writer.print("{d}", .{value});
141 try writer.writeByte(',');
142 if (zone.text) |text| try writeCsvString(writer, text);
143 try writer.writeByte('\n');
144 },
145 else => {},
146 }
147 }
148
149 fn writeMessage(writer: *std.Io.Writer, base_ns: u64, parsed: event.Parsed) !void {
150 try writer.print("{d},{d},{d},", .{ nsSince(base_ns, parsed.time_ns), parsed.time_ns, parsed.thread });
151 try writeCsvString(writer, parsed.text orelse "");
152 try writer.writeByte('\n');
153 }
154
155 fn writePlot(writer: *std.Io.Writer, base_ns: u64, parsed: event.Parsed) !void {
156 try writeCsvString(writer, parsed.kind.tag());
157 try writer.writeByte(',');
158 try writeCsvString(writer, parsed.name orelse "");
159 try writer.print(",{d},{d},{d},", .{ nsSince(base_ns, parsed.time_ns), parsed.time_ns, parsed.thread });
160 if (parsed.value_f64) |value| {
161 try writer.print("{d}", .{value});
162 } else if (parsed.value_i64) |value| {
163 try writer.print("{d}", .{value});
164 }
165 try writer.writeByte(',');
166 try writeCsvString(writer, parsed.plot_kind orelse "");
167 try writer.writeByte(',');
168 try writeCsvString(writer, parsed.plot_unit orelse "");
169 try writer.writeByte(',');
170 if (parsed.kind == .plot_config) try writer.print("{}", .{parsed.plot_step});
171 try writer.writeByte(',');
172 if (parsed.kind == .plot_config) try writer.print("{}", .{parsed.plot_fill});
173 try writer.writeByte(',');
174 if (parsed.kind == .plot_config) {
175 if (parsed.color) |color| try writer.print("{d}", .{color});
176 }
177 try writer.writeByte('\n');
178 }
179
180 fn writeAlloc(writer: *std.Io.Writer, base_ns: u64, parsed: event.Parsed) !void {
181 try writeCsvString(writer, parsed.kind.tag());
182 try writer.writeByte(',');
183 try writeCsvString(writer, parsed.name orelse "");
184 try writer.print(",{d},{d},{d},{d},{d}\n", .{
185 nsSince(base_ns, parsed.time_ns),
186 parsed.time_ns,
187 parsed.thread,
188 parsed.address,
189 parsed.size,
190 });
191 }
192
193 fn writeCsvString(writer: *std.Io.Writer, text: []const u8) !void {
194 try writer.writeByte('"');
195 for (text) |byte| {
196 switch (byte) {
197 '"' => try writer.writeAll("\"\""),
198 '\n' => try writer.writeAll("\\n"),
199 '\r' => try writer.writeAll("\\r"),
200 '\t' => try writer.writeAll("\\t"),
201 else => try writer.writeByte(byte),
202 }
203 }
204 try writer.writeByte('"');
205 }
206
207 fn dupeOptional(allocator: std.mem.Allocator, text: ?[]const u8) !?[]u8 {
208 const actual = text orelse return null;
209 return try allocator.dupe(u8, actual);
210 }
211
212 fn appendText(allocator: std.mem.Allocator, existing: ?[]u8, addition: []const u8) ![]u8 {
213 if (existing) |old| {
214 const joined = try allocator.alloc(u8, old.len + 1 + addition.len);
215 @memcpy(joined[0..old.len], old);
216 joined[old.len] = '\n';
217 @memcpy(joined[old.len + 1 ..], addition);
218 allocator.free(old);
219 return joined;
220 }
221 return try allocator.dupe(u8, addition);
222 }
223
224 fn nsSince(base_ns: u64, time_ns: u64) u64 {
225 if (time_ns <= base_ns) return 0;
226 return time_ns - base_ns;
227 }
228
229 test "csv export emits completed zone rows" {
230 var trace = std.Io.Writer.Allocating.init(std.testing.allocator);
231 defer trace.deinit();
232 try (event.TraceEvent{ .seq = 1, .kind = .start, .time_ns = 100, .thread = 1, .name = "test" }).writeJsonLine(&trace.writer);
233 try (event.TraceEvent{ .seq = 2, .kind = .zone_begin, .time_ns = 110, .thread = 1, .id = 7, .name = "phase", .file = "phase.zig", .line = 9 }).writeJsonLine(&trace.writer);
234 try (event.TraceEvent{ .seq = 3, .kind = .zone_value, .time_ns = 120, .thread = 1, .id = 7, .value_u64 = 42 }).writeJsonLine(&trace.writer);
235 try (event.TraceEvent{ .seq = 4, .kind = .zone_text, .time_ns = 130, .thread = 1, .id = 7, .text = "note" }).writeJsonLine(&trace.writer);
236 try (event.TraceEvent{ .seq = 5, .kind = .zone_end, .time_ns = 160, .thread = 1, .id = 7 }).writeJsonLine(&trace.writer);
237
238 var tmp = std.testing.tmpDir(.{});
239 defer tmp.cleanup();
240 const root = try tmp.parent_dir.realPathFileAlloc(sys.fs.debugIo(), tmp.sub_path[0..], std.testing.allocator);
241 defer std.testing.allocator.free(root);
242 const path = try std.fs.path.join(std.testing.allocator, &.{ root, "trace.jsonl" });
243 defer std.testing.allocator.free(path);
244 try sys.fs.cwd().writeFile(sys.fs.debugIo(), .{ .sub_path = path, .data = trace.written() });
245
246 var out = std.Io.Writer.Allocating.init(std.testing.allocator);
247 defer out.deinit();
248 try writeFromJsonlPath(std.testing.allocator, path, &out.writer, .zones);
249 try std.testing.expect(std.mem.indexOf(u8, out.written(), "\"phase\",\"phase.zig\",9,10,50,1,42,\"note\"") != null);
250 }
251
252 test "csv export includes allocation names and sized frees" {
253 var trace = std.Io.Writer.Allocating.init(std.testing.allocator);
254 defer trace.deinit();
255 try (event.TraceEvent{ .seq = 1, .kind = .start, .time_ns = 100, .thread = 1, .name = "test" }).writeJsonLine(&trace.writer);
256 try (event.TraceEvent{ .seq = 2, .kind = .alloc, .time_ns = 110, .thread = 1, .name = "arena", .address = 4096, .size = 64 }).writeJsonLine(&trace.writer);
257 try (event.TraceEvent{ .seq = 3, .kind = .free, .time_ns = 120, .thread = 1, .name = "arena", .address = 4096, .size = 64 }).writeJsonLine(&trace.writer);
258
259 var tmp = std.testing.tmpDir(.{});
260 defer tmp.cleanup();
261 const root = try tmp.parent_dir.realPathFileAlloc(sys.fs.debugIo(), tmp.sub_path[0..], std.testing.allocator);
262 defer std.testing.allocator.free(root);
263 const path = try std.fs.path.join(std.testing.allocator, &.{ root, "trace.jsonl" });
264 defer std.testing.allocator.free(path);
265 try sys.fs.cwd().writeFile(sys.fs.debugIo(), .{ .sub_path = path, .data = trace.written() });
266
267 var out = std.Io.Writer.Allocating.init(std.testing.allocator);
268 defer out.deinit();
269 try writeFromJsonlPath(std.testing.allocator, path, &out.writer, .allocs);
270 try std.testing.expect(std.mem.indexOf(u8, out.written(), "kind,name,ns_since_start") != null);
271 try std.testing.expect(std.mem.indexOf(u8, out.written(), "\"alloc\",\"arena\",10,110,1,4096,64") != null);
272 try std.testing.expect(std.mem.indexOf(u8, out.written(), "\"free\",\"arena\",20,120,1,4096,64") != null);
273 }
274
275 test "csv plot export includes configuration metadata" {
276 var trace = std.Io.Writer.Allocating.init(std.testing.allocator);
277 defer trace.deinit();
278 try (event.TraceEvent{
279 .seq = 1,
280 .kind = .plot_config,
281 .time_ns = 100,
282 .thread = 1,
283 .name = "heap.live",
284 .color = 7,
285 .plot_unit = "bytes",
286 .plot_step = true,
287 .plot_fill = true,
288 }).writeJsonLine(&trace.writer);
289 try (event.TraceEvent{
290 .seq = 2,
291 .kind = .plot,
292 .time_ns = 110,
293 .thread = 1,
294 .name = "heap.live",
295 .value_i64 = 64,
296 .plot_kind = "int",
297 }).writeJsonLine(&trace.writer);
298
299 var tmp = std.testing.tmpDir(.{});
300 defer tmp.cleanup();
301 const root = try tmp.parent_dir.realPathFileAlloc(
302 sys.fs.debugIo(),
303 tmp.sub_path[0..],
304 std.testing.allocator,
305 );
306 defer std.testing.allocator.free(root);
307 const path = try std.fs.path.join(std.testing.allocator, &.{ root, "trace.jsonl" });
308 defer std.testing.allocator.free(path);
309 try sys.fs.cwd().writeFile(sys.fs.debugIo(), .{ .sub_path = path, .data = trace.written() });
310
311 var out = std.Io.Writer.Allocating.init(std.testing.allocator);
312 defer out.deinit();
313 try writeFromJsonlPath(std.testing.allocator, path, &out.writer, .plots);
314 const csv = out.written();
315 try std.testing.expect(std.mem.indexOf(u8, csv, "event,name,ns_since_start") != null);
316 const config = "\"plot.config\",\"heap.live\",0,100,1,,\"\",\"bytes\",true,true,7";
317 try std.testing.expect(std.mem.indexOf(u8, csv, config) != null);
318 const sample = "\"plot\",\"heap.live\",10,110,1,64,\"int\",\"\",,,";
319 try std.testing.expect(std.mem.indexOf(u8, csv, sample) != null);
320 }