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 }