tiny.profiling.report.chart
Defined in report.
API (11)
Actions
Public operations.
Types and contracts
Public types and contracts.
Source
Source: src/profiling/report/chart.zig
zig
const std = @import("std");const zen = @import("zen");const Allocator = std.mem.Allocator;pub const Format = *const fn (Allocator, f64) Allocator.Error![]u8;pub const Point = struct { label: []const u8, value: ?f64, low: ?f64 = null, high: ?f64 = null,};pub const LineOptions = struct { width: f64 = 260, height: f64 = 64, ticks: bool = false, format: Format,};pub const StripOptions = struct { width: f64 = 220, height: f64 = 40, format: Format,};pub const SequenceOptions = struct { width: f64 = 220, height: f64 = 40, index_origin: u64 = 0, format: Format,};pub const XyPoint = struct { x: f64, y: f64, label: []const u8,};pub const XyOptions = struct { width: f64 = 210, height: f64 = 72,};const max_strip_dots = 512;const max_sequence_points = 512;const Domain = struct { min: f64, max: f64, fn observe(self: *Domain, value: ?f64) void { const actual = value orelse return; if (!std.math.isFinite(actual)) return; if (actual < self.min) self.min = actual; if (actual > self.max) self.max = actual; } fn seen(self: Domain) bool { return self.min <= self.max; } fn span(self: Domain) f64 { return if (self.max > self.min) self.max - self.min else 1; }};const empty_domain = Domain{ .min = std.math.inf(f64), .max = -std.math.inf(f64) };pub fn line(allocator: Allocator, points: []const Point, options: LineOptions) Allocator.Error![]u8 { var out: std.ArrayList(u8) = .empty; var domain = empty_domain; var defined: usize = 0; for (points) |point| { if (point.value != null) defined += 1; domain.observe(point.value); domain.observe(point.low); domain.observe(point.high); } try openSvg(&out, allocator, options.width, options.height, "chart-line-figure"); if (defined == 0) { try emptyText(&out, allocator, options.width, options.height); try out.appendSlice(allocator, "</svg>"); return try out.toOwnedSlice(allocator); } const pad_left: f64 = if (options.ticks) 64 else 8; const pad_right: f64 = 8; const pad_y: f64 = 7; const plot = Plot{ .x0 = pad_left, .x1 = options.width - pad_right, .y0 = pad_y, .y1 = options.height - pad_y, .domain = domain, .count = points.len, }; if (options.ticks) try lineTicks(&out, allocator, plot, options.format); try lineBand(&out, allocator, points, plot); try linePath(&out, allocator, points, plot); try lineDots(&out, allocator, points, plot, options.format); try out.appendSlice(allocator, "</svg>"); return try out.toOwnedSlice(allocator);}pub fn strip( allocator: Allocator, samples: []const u64, median: ?f64, low: ?f64, high: ?f64, options: StripOptions,) Allocator.Error![]u8 { var out: std.ArrayList(u8) = .empty; var domain = empty_domain; for (samples) |sample| domain.observe(@floatFromInt(sample)); domain.observe(median); domain.observe(low); domain.observe(high); try openSvg(&out, allocator, options.width, options.height, "chart-strip-figure"); if (!domain.seen()) { try emptyText(&out, allocator, options.width, options.height); try out.appendSlice(allocator, "</svg>"); return try out.toOwnedSlice(allocator); } const plot = Plot{ .x0 = 8, .x1 = options.width - 8, .y0 = 5, .y1 = options.height - 5, .domain = domain, .count = samples.len, }; if (low != null and high != null) { const band_x0 = plot.xValue(low.?); const band_x1 = plot.xValue(high.?); try appendFmt(&out, allocator, "<rect class=\"chart-band\" x=\"{d:.2}\" y=\"{d:.2}\" width=\"{d:.2}\" height=\"{d:.2}\"/>", .{ @min(band_x0, band_x1), plot.y0, @abs(band_x1 - band_x0), plot.y1 - plot.y0, }); } if (median) |actual| { const x = plot.xValue(actual); try appendFmt(&out, allocator, "<line class=\"chart-median\" x1=\"{d:.2}\" y1=\"{d:.2}\" x2=\"{d:.2}\" y2=\"{d:.2}\"><title>median ", .{ x, plot.y0, x, plot.y1 }); try appendEscapedText(&out, allocator, try options.format(allocator, actual)); try out.appendSlice(allocator, "</title></line>"); } const stride = if (samples.len > max_strip_dots) samples.len / max_strip_dots + 1 else 1; const middle = (plot.y0 + plot.y1) / 2; const amplitude = (plot.y1 - plot.y0) / 2 - 3; const observation_fmt = "<circle class=\"chart-sample\" cx=\"{d:.2}\" cy=\"{d:.2}\" r=\"2\">" ++ "<title>observation {d}: "; var index: usize = 0; while (index < samples.len) : (index += stride) { const value: f64 = @floatFromInt(samples[index]); const offsets = [_]f64{ 0, -0.5, 0.5, -1, 1 }; const jitter = offsets[index % offsets.len] * amplitude; try appendFmt(&out, allocator, observation_fmt, .{ plot.xValue(value), middle + jitter, index, }); try appendEscapedText(&out, allocator, try options.format(allocator, value)); try out.appendSlice(allocator, "</title></circle>"); } try out.appendSlice(allocator, "</svg>"); return try out.toOwnedSlice(allocator);}pub fn sequence( allocator: Allocator, samples: []const u64, median: ?f64, low: ?f64, high: ?f64, options: SequenceOptions,) Allocator.Error![]u8 { var out: std.ArrayList(u8) = .empty; var domain = empty_domain; for (samples) |sample| domain.observe(@floatFromInt(sample)); domain.observe(median); domain.observe(low); domain.observe(high); try openSvg(&out, allocator, options.width, options.height, "chart-sequence-figure"); if (samples.len == 0) { try emptyText(&out, allocator, options.width, options.height); try out.appendSlice(allocator, "</svg>"); return try out.toOwnedSlice(allocator); } const indices = try sequenceIndices(allocator, samples); defer allocator.free(indices); const plot = Plot{ .x0 = 8, .x1 = options.width - 8, .y0 = 5, .y1 = options.height - 5, .domain = domain, .count = samples.len, }; try sequenceSummary(&out, allocator, plot, median, low, high, options.format); try sequencePath(&out, allocator, plot, samples, indices); try sequenceDots(&out, allocator, plot, samples, indices, options); try out.appendSlice(allocator, "</svg>"); return try out.toOwnedSlice(allocator);}fn sequenceIndices(allocator: Allocator, samples: []const u64) Allocator.Error![]usize { var indices: std.ArrayList(usize) = .empty; errdefer indices.deinit(allocator); try indices.ensureTotalCapacity(allocator, @min(samples.len, max_sequence_points)); if (samples.len <= max_sequence_points) { for (samples, 0..) |_, index| indices.appendAssumeCapacity(index); return try indices.toOwnedSlice(allocator); } indices.appendAssumeCapacity(0); const bucket_count = (max_sequence_points - 2) / 2; const interior = samples.len - 2; const base_width = interior / bucket_count; const wide_buckets = interior % bucket_count; var start: usize = 1; for (0..bucket_count) |bucket| { const stop = start + base_width + @intFromBool(bucket < wide_buckets); var min_index = start; var max_index = start; for (start + 1..stop) |index| { if (samples[index] < samples[min_index]) min_index = index; if (samples[index] > samples[max_index]) max_index = index; } indices.appendAssumeCapacity(@min(min_index, max_index)); if (min_index != max_index) indices.appendAssumeCapacity(@max(min_index, max_index)); start = stop; } indices.appendAssumeCapacity(samples.len - 1); std.debug.assert(indices.items.len <= max_sequence_points); return try indices.toOwnedSlice(allocator);}fn sequenceSummary( out: *std.ArrayList(u8), allocator: Allocator, plot: Plot, median: ?f64, low: ?f64, high: ?f64, format: Format,) Allocator.Error!void { if (low != null and high != null) { const y0 = plot.yValue(@max(low.?, high.?)); const y1 = plot.yValue(@min(low.?, high.?)); try appendFmt(out, allocator, "<rect class=\"chart-band\" x=\"{d:.2}\" y=\"{d:.2}\" " ++ "width=\"{d:.2}\" height=\"{d:.2}\"/>", .{ plot.x0, y0, plot.x1 - plot.x0, y1 - y0, }); } if (median) |value| { const y = plot.yValue(value); try appendFmt(out, allocator, "<line class=\"chart-median\" x1=\"{d:.2}\" y1=\"{d:.2}\" " ++ "x2=\"{d:.2}\" y2=\"{d:.2}\"><title>median ", .{ plot.x0, y, plot.x1, y, }); try appendEscapedText(out, allocator, try format(allocator, value)); try out.appendSlice(allocator, "</title></line>"); }}fn sequencePath( out: *std.ArrayList(u8), allocator: Allocator, plot: Plot, samples: []const u64, indices: []const usize,) Allocator.Error!void { if (indices.len < 2) return; try out.appendSlice(allocator, "<path class=\"chart-line\" d=\""); for (indices, 0..) |index, path_index| { try appendFmt(out, allocator, "{s}{d:.2} {d:.2} ", .{ if (path_index == 0) "M" else "L", plot.xIndex(index), plot.yValue(@floatFromInt(samples[index])), }); } try out.appendSlice(allocator, "\"/>");}fn sequenceDots( out: *std.ArrayList(u8), allocator: Allocator, plot: Plot, samples: []const u64, indices: []const usize, options: SequenceOptions,) Allocator.Error!void { const point_fmt = "<circle class=\"chart-dot chart-sequence-sample\" cx=\"{d:.2}\" " ++ "cy=\"{d:.2}\" r=\"2\"><title>sample {d}: "; for (indices) |index| { const value: f64 = @floatFromInt(samples[index]); const display_index = std.math.add( u64, options.index_origin, @intCast(index), ) catch std.math.maxInt(u64); try appendFmt(out, allocator, point_fmt, .{ plot.xIndex(index), plot.yValue(value), display_index, }); try appendEscapedText(out, allocator, try options.format(allocator, value)); try out.appendSlice(allocator, "</title></circle>"); }}pub fn xy(allocator: Allocator, points: []const XyPoint, options: XyOptions) Allocator.Error![]u8 { var out: std.ArrayList(u8) = .empty; try openSvg(&out, allocator, options.width, options.height, "chart-xy-figure"); if (points.len == 0) { try emptyText(&out, allocator, options.width, options.height); try out.appendSlice(allocator, "</svg>"); return try out.toOwnedSlice(allocator); } var x_domain = empty_domain; var y_domain = empty_domain; y_domain.observe(0); for (points) |point| { x_domain.observe(point.x); y_domain.observe(point.y); } const plot = Plot{ .x0 = 8, .x1 = options.width - 8, .y0 = 7, .y1 = options.height - 7, .domain = y_domain, .count = points.len, }; const zero_y = plot.yValue(0); try appendFmt(&out, allocator, "<line class=\"chart-grid\" x1=\"{d:.2}\" y1=\"{d:.2}\" x2=\"{d:.2}\" y2=\"{d:.2}\"/>", .{ plot.x0, zero_y, plot.x1, zero_y }); const order = try allocator.alloc(usize, points.len); for (order, 0..) |*slot, point_index| slot.* = point_index; std.mem.sort(usize, order, points, xyBefore); if (points.len >= 2) { try out.appendSlice(allocator, "<path class=\"chart-line\" d=\""); for (order, 0..) |point_index, path_index| { const point = points[point_index]; try appendFmt(&out, allocator, "{s}{d:.2} {d:.2} ", .{ if (path_index == 0) "M" else "L", xyX(plot, x_domain, point.x), plot.yValue(point.y), }); } try out.appendSlice(allocator, "\"/>"); } for (order) |point_index| { const point = points[point_index]; try appendFmt(&out, allocator, "<circle class=\"chart-dot\" cx=\"{d:.2}\" cy=\"{d:.2}\" r=\"2.5\"><title>", .{ xyX(plot, x_domain, point.x), plot.yValue(point.y), }); try appendEscapedText(&out, allocator, point.label); try out.appendSlice(allocator, "</title></circle>"); } try out.appendSlice(allocator, "</svg>"); return try out.toOwnedSlice(allocator);}fn xyX(plot: Plot, x_domain: Domain, value: f64) f64 { if (x_domain.max == x_domain.min) return (plot.x0 + plot.x1) / 2; const fraction = (value - x_domain.min) / x_domain.span(); return plot.x0 + fraction * (plot.x1 - plot.x0);}fn xyBefore(points: []const XyPoint, left: usize, right: usize) bool { return points[left].x < points[right].x;}const Plot = struct { x0: f64, x1: f64, y0: f64, y1: f64, domain: Domain, count: usize, fn xIndex(self: Plot, index: usize) f64 { if (self.count <= 1) return (self.x0 + self.x1) / 2; const fraction = @as(f64, @floatFromInt(index)) / @as(f64, @floatFromInt(self.count - 1)); return self.x0 + fraction * (self.x1 - self.x0); } fn xValue(self: Plot, value: f64) f64 { if (self.domain.max == self.domain.min) return (self.x0 + self.x1) / 2; const fraction = (value - self.domain.min) / self.domain.span(); return self.x0 + fraction * (self.x1 - self.x0); } fn yValue(self: Plot, value: f64) f64 { if (self.domain.max == self.domain.min) return (self.y0 + self.y1) / 2; const fraction = (value - self.domain.min) / self.domain.span(); return self.y1 - fraction * (self.y1 - self.y0); }};fn openSvg(out: *std.ArrayList(u8), allocator: Allocator, width: f64, height: f64, class: []const u8) Allocator.Error!void { try appendFmt(out, allocator, "<svg class=\"{s}\" viewBox=\"0 0 {d:.0} {d:.0}\" width=\"{d:.0}\" height=\"{d:.0}\" role=\"img\">", .{ class, width, height, width, height, });}fn emptyText(out: *std.ArrayList(u8), allocator: Allocator, width: f64, height: f64) Allocator.Error!void { try appendFmt(out, allocator, "<text class=\"chart-empty\" x=\"{d:.0}\" y=\"{d:.0}\" text-anchor=\"middle\">no data</text>", .{ width / 2, height / 2 + 4, });}fn lineTicks(out: *std.ArrayList(u8), allocator: Allocator, plot: Plot, format: Format) Allocator.Error!void { const rows = [_]f64{ plot.domain.max, (plot.domain.max + plot.domain.min) / 2, plot.domain.min }; for (rows) |value| { const y = plot.yValue(value); try appendFmt(out, allocator, "<line class=\"chart-grid\" x1=\"{d:.2}\" y1=\"{d:.2}\" x2=\"{d:.2}\" y2=\"{d:.2}\"/>", .{ plot.x0, y, plot.x1, y }); try appendFmt(out, allocator, "<text class=\"chart-tick\" x=\"{d:.2}\" y=\"{d:.2}\" text-anchor=\"end\">", .{ plot.x0 - 6, y + 3 }); try appendEscapedText(out, allocator, try format(allocator, value)); try out.appendSlice(allocator, "</text>"); }}fn lineBand(out: *std.ArrayList(u8), allocator: Allocator, points: []const Point, plot: Plot) Allocator.Error!void { var start: usize = 0; while (start < points.len) { while (start < points.len and !banded(points[start])) start += 1; if (start >= points.len) return; var stop = start; while (stop < points.len and banded(points[stop])) stop += 1; if (stop - start >= 2) { try out.appendSlice(allocator, "<path class=\"chart-band\" d=\""); var index = start; while (index < stop) : (index += 1) { try appendFmt(out, allocator, "{s}{d:.2} {d:.2} ", .{ if (index == start) "M" else "L", plot.xIndex(index), plot.yValue(points[index].high.?) }); } index = stop; while (index > start) { index -= 1; try appendFmt(out, allocator, "L{d:.2} {d:.2} ", .{ plot.xIndex(index), plot.yValue(points[index].low.?) }); } try out.appendSlice(allocator, "Z\"/>"); } start = stop; }}fn banded(point: Point) bool { return point.value != null and point.low != null and point.high != null;}fn linePath(out: *std.ArrayList(u8), allocator: Allocator, points: []const Point, plot: Plot) Allocator.Error!void { var start: usize = 0; while (start < points.len) { while (start < points.len and points[start].value == null) start += 1; if (start >= points.len) return; var stop = start; while (stop < points.len and points[stop].value != null) stop += 1; if (stop - start >= 2) { try out.appendSlice(allocator, "<path class=\"chart-line\" d=\""); var index = start; while (index < stop) : (index += 1) { try appendFmt(out, allocator, "{s}{d:.2} {d:.2} ", .{ if (index == start) "M" else "L", plot.xIndex(index), plot.yValue(points[index].value.?) }); } try out.appendSlice(allocator, "\"/>"); } start = stop; }}fn lineDots(out: *std.ArrayList(u8), allocator: Allocator, points: []const Point, plot: Plot, format: Format) Allocator.Error!void { for (points, 0..) |point, index| { const value = point.value orelse continue; try appendFmt(out, allocator, "<circle class=\"chart-dot\" cx=\"{d:.2}\" cy=\"{d:.2}\" r=\"2.5\"><title>", .{ plot.xIndex(index), plot.yValue(value), }); try appendEscapedText(out, allocator, point.label); try out.appendSlice(allocator, ": "); try appendEscapedText(out, allocator, try format(allocator, value)); if (point.low != null and point.high != null) { try out.appendSlice(allocator, " ("); try appendEscapedText(out, allocator, try format(allocator, point.low.?)); try out.appendSlice(allocator, " .. "); try appendEscapedText(out, allocator, try format(allocator, point.high.?)); try out.appendSlice(allocator, ")"); } try out.appendSlice(allocator, "</title></circle>"); }}fn appendFmt(out: *std.ArrayList(u8), allocator: Allocator, comptime fmt: []const u8, args: anytype) Allocator.Error!void { const text = try std.fmt.allocPrint(allocator, fmt, args); defer allocator.free(text); try out.appendSlice(allocator, text);}fn appendEscapedText(out: *std.ArrayList(u8), allocator: Allocator, text: []const u8) Allocator.Error!void { try zen.html.appendEscaped(out, allocator, text);}fn formatRaw(allocator: Allocator, value: f64) Allocator.Error![]u8 { return std.fmt.allocPrint(allocator, "{d:.1}", .{value});}test "chart line renders band path and dots" { var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena_state.deinit(); const allocator = arena_state.allocator(); const points = [_]Point{ .{ .label = "run a", .value = 10, .low = 9, .high = 11 }, .{ .label = "run b", .value = 20, .low = 18, .high = 22 }, .{ .label = "run c", .value = 15, .low = 14, .high = 16 }, }; const svg = try line(allocator, &points, .{ .format = formatRaw }); try std.testing.expectEqual(@as(usize, 3), std.mem.count(u8, svg, "chart-dot")); try std.testing.expectEqual(@as(usize, 1), std.mem.count(u8, svg, "chart-band")); try std.testing.expectEqual(@as(usize, 1), std.mem.count(u8, svg, "chart-line\"")); try std.testing.expect(std.mem.indexOf(u8, svg, "run b: 20.0 (18.0 .. 22.0)") != null);}test "chart line skips gaps and handles empty series" { var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena_state.deinit(); const allocator = arena_state.allocator(); const gapped = [_]Point{ .{ .label = "a", .value = 5 }, .{ .label = "b", .value = null }, .{ .label = "c", .value = 6 }, }; const svg = try line(allocator, &gapped, .{ .format = formatRaw }); try std.testing.expectEqual(@as(usize, 2), std.mem.count(u8, svg, "chart-dot")); try std.testing.expectEqual(@as(usize, 0), std.mem.count(u8, svg, "chart-line\"")); const empty = try line(allocator, &.{}, .{ .format = formatRaw }); try std.testing.expect(std.mem.indexOf(u8, empty, "no data") != null);}test "chart line single point centers a dot" { var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena_state.deinit(); const allocator = arena_state.allocator(); const points = [_]Point{.{ .label = "only", .value = 42 }}; const svg = try line(allocator, &points, .{ .format = formatRaw, .ticks = true }); try std.testing.expectEqual(@as(usize, 1), std.mem.count(u8, svg, "chart-dot")); try std.testing.expectEqual(@as(usize, 3), std.mem.count(u8, svg, "chart-grid"));}test "chart strip renders samples median and band" { var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena_state.deinit(); const allocator = arena_state.allocator(); const samples = [_]u64{ 100, 120, 110, 130, 105 }; const svg = try strip(allocator, &samples, 110, 104, 126, .{ .format = formatRaw }); try std.testing.expectEqual(@as(usize, 5), std.mem.count(u8, svg, "chart-sample")); try std.testing.expectEqual(@as(usize, 1), std.mem.count(u8, svg, "chart-median")); try std.testing.expectEqual(@as(usize, 1), std.mem.count(u8, svg, "chart-band")); try std.testing.expect(std.mem.indexOf(u8, svg, "observation 0: 100.0") != null); const empty = try strip(allocator, &.{}, null, null, null, .{ .format = formatRaw }); try std.testing.expect(std.mem.indexOf(u8, empty, "no data") != null);}test "chart sequence renders acquisition order with summary context" { var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena_state.deinit(); const allocator = arena_state.allocator(); const samples = [_]u64{ 130, 100, 120, 110 }; const svg = try sequence( allocator, &samples, 115, 105, 125, .{ .format = formatRaw, .index_origin = 7 }, ); try std.testing.expectEqual(@as(usize, 4), std.mem.count(u8, svg, "chart-sequence-sample")); try std.testing.expectEqual(@as(usize, 1), std.mem.count(u8, svg, "chart-median")); try std.testing.expectEqual(@as(usize, 1), std.mem.count(u8, svg, "chart-band")); try std.testing.expectEqual(@as(usize, 1), std.mem.count(u8, svg, "chart-line\"")); const first = std.mem.indexOf(u8, svg, "sample 7: 130.0").?; const second = std.mem.indexOf(u8, svg, "sample 8: 100.0").?; try std.testing.expect(first < second);}test "chart sequence bounds points and preserves extrema" { var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena_state.deinit(); const allocator = arena_state.allocator(); var samples = @as([2000]u64, @splat(100)); samples[1000] = 999; samples[1200] = 1; const svg = try sequence(allocator, &samples, null, null, null, .{ .format = formatRaw }); const points = std.mem.count(u8, svg, "chart-sequence-sample"); try std.testing.expect(points <= max_sequence_points); try std.testing.expect(std.mem.indexOf(u8, svg, "sample 0: 100.0") != null); try std.testing.expect(std.mem.indexOf(u8, svg, "sample 1000: 999.0") != null); try std.testing.expect(std.mem.indexOf(u8, svg, "sample 1200: 1.0") != null); try std.testing.expect(std.mem.indexOf(u8, svg, "sample 1999: 100.0") != null);}test "chart xy sorts by x and anchors the zero line" { var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena_state.deinit(); const allocator = arena_state.allocator(); const points = [_]XyPoint{ .{ .x = 20, .y = 18, .label = "+20% virtual: +18% program" }, .{ .x = 0, .y = 0, .label = "baseline" }, .{ .x = 10, .y = -4, .label = "+10% virtual: -4% program" }, }; const svg = try xy(allocator, &points, .{}); try std.testing.expectEqual(@as(usize, 3), std.mem.count(u8, svg, "chart-dot")); try std.testing.expectEqual(@as(usize, 1), std.mem.count(u8, svg, "chart-grid")); try std.testing.expectEqual(@as(usize, 1), std.mem.count(u8, svg, "chart-line\"")); try std.testing.expect(std.mem.indexOf(u8, svg, "d=\"M8.00") != null); try std.testing.expect(std.mem.indexOf(u8, svg, "+20% virtual") != null); const empty = try xy(allocator, &.{}, .{}); try std.testing.expect(std.mem.indexOf(u8, empty, "no data") != null);}test "chart strip strides oversized sample sets" { var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena_state.deinit(); const allocator = arena_state.allocator(); var samples: [2000]u64 = undefined; for (&samples, 0..) |*sample, index| sample.* = index; const svg = try strip(allocator, &samples, null, null, null, .{ .format = formatRaw }); const dots = std.mem.count(u8, svg, "chart-sample"); try std.testing.expect(dots <= max_strip_dots); try std.testing.expect(dots > 400);}Source: src/profiling/report/root.zig:2
zig
pub const chart = @import("chart.zig");Audit
| Definitions | 12 |
|---|---|
| Public names | 12 |
| Members | 20 |
| Version | 26.7.0 |
| Revision | daab053ee433 |