diff options
Diffstat (limited to 'test')
| -rw-r--r-- | test/pdf.zig | 168 | ||||
| -rw-r--r-- | test/pdf_scroll_bench.zig | 106 |
2 files changed, 241 insertions, 33 deletions
diff --git a/test/pdf.zig b/test/pdf.zig index fe084cc1..6864f247 100644 --- a/test/pdf.zig +++ b/test/pdf.zig @@ -730,6 +730,7 @@ test "MuPDF pane renders, navigates, searches, and round-trips its page" { const request = panes.Pdf.renderRequest( panes.Pdf.paneViewport(p, pane) orelse return error.MissingPdfViewport, pdf_raster_policy, + pane.pdf.?.fit, ); try std.testing.expectEqual(pdf_raster_policy.dpi, request.dpi); try std.testing.expectEqual(pdf_raster_policy.max_dimension, request.max_dimension); @@ -1119,6 +1120,171 @@ test "a fling's banded pages show the reader exactly what whole pages would" { } } +/// MuPDF clips the edges of a path to the pixmap, and an edge that crosses a +/// band's clip is stepped from a different start: along its whole length in +/// the band, its antialiased pixels can be a level or two off the whole +/// page's. Never more. +const band_tolerance: u8 = 4; + +fn expectNearRows(expected: []const u8, got: []const u8) !void { + try std.testing.expectEqual(expected.len, got.len); + var worst: u8 = 0; + for (expected, got) |a, b| worst = @max(worst, if (a > b) a - b else b - a); + if (worst > band_tolerance) { + std.debug.print("band rows are {d} levels off the whole page's\n", .{worst}); + return error.TestExpectedEqual; + } +} + +test "a tall page is drawn at the display's resolution, in bands that are the whole page's rows" { + if (!pdf_enabled or platform == .web) return; + + // The page is fifteen A4 heights tall. Squeezed whole into the raster + // budget it was a third as wide as the pane and blown up, blurred; it is + // drawn instead at the pane's own resolution, a band around the screen at + // a time, and every band row must be the row a whole-page render makes + // (to `band_tolerance`), and the same row however it was scrolled to. + const gpa = std.testing.allocator; + var tmp = std.testing.tmpDir(.{}); + defer tmp.cleanup(); + const bytes = try pdf_impl.makeTallTestPdf(gpa); + defer gpa.free(bytes); + try tmp.dir.writeFile(std.testing.io, .{ .sub_path = "tall.pdf", .data = bytes }); + var dir_buf: [4096]u8 = undefined; + const dir_len = try tmp.dir.realPath(std.testing.io, &dir_buf); + const path = try std.fmt.allocPrint(gpa, "{s}/tall.pdf", .{dir_buf[0..dir_len]}); + defer gpa.free(path); + const p = try Pardes.init(gpa, .{ .file = path, .cols = 120, .rows = 40 }); + defer p.deinit(); + p.native_images = true; + const pane = p.panes[0].?; + const pv = &pane.pdf.?; + var frame = std.heap.ArenaAllocator.init(gpa); + defer frame.deinit(); + _ = try p.render(frame.allocator()); + + const viewport = panes.Pdf.paneViewport(p, pane) orelse return error.MissingPdfViewport; + const request = panes.Pdf.renderRequest(viewport, pdf_raster_policy, pv.fit); + const shape = try pv.document.measureRenderAt(0, request); + if (pdf_raster_policy.match_viewport) { + // The rendered scale IS the screen's: one raster pixel per pixel of + // the pane's width, so nothing is scaled up. + try std.testing.expectEqual(@as(usize, viewport.pixel_w), shape.width); + } else { + // Kitty's policy is a dpi, kept low for the wire; the tall page gets + // it as every other page does, not a quarter of it. + try std.testing.expectEqual( + @as(usize, @intFromFloat(pdf_impl.tall_test_page.width)) * pdf_raster_policy.dpi / 72, + shape.width, + ); + } + try std.testing.expect(panes.Pdf.isBanded(shape)); + + // The reference: the whole page in one render, before highlights and + // tint, and tinted as the pane tints. + const plain = try gpa.alloc(u8, shape.len); + defer gpa.free(plain); + try pv.document.renderIntoAt(0, request, shape, shape.wholePage(), &.{}, plain); + const whole = try gpa.dupe(u8, plain); + defer gpa.free(whole); + try pdf_impl.tintRgba(whole, pv.tint, panes.Pdf.tintColors(p)); + + const max_scroll = @as(f64, @floatFromInt(pv.document_height - viewport.pixel_h)); + var middle: []u8 = &.{}; + defer gpa.free(middle); + var middle_y: usize = 0; + for ([_]f64{ 0, @round(max_scroll / 2), max_scroll }, 0..) |at, step| { + _ = panes.Pdf.scrollPane(p, pane, at - pv.document_scroll_y); + pv.scroll_travel = 0; // read, not flung + _ = frame.reset(.retain_capacity); + const surface = try p.render(frame.allocator()); + try std.testing.expectEqual(@as(usize, 1), surface.nimages); + const place = surface.images[0].?; + const geometry = place.native.geometry orelse return error.MissingPdfGeometry; + const raster = panes.Pdf.rasterForPage(pv, 0) orelse return error.MissingPdfRaster; + // Bounded: a band, never the page, and within one buffer's budget. + try std.testing.expectEqual(shape.width, place.iw); + try std.testing.expect(raster.band_h < raster.ih); + try std.testing.expect(place.rgba.len <= pdf_impl.max_owned_raster_bytes); + // Prefetched: a screenful beyond what is shown, both ways, where the + // page goes on. + const seen_top = raster.band_y + geometry.src.y; + const seen_end = seen_top + geometry.src.h; + try std.testing.expect(raster.band_y <= seen_top -| geometry.src.h); + try std.testing.expect(raster.band_y + raster.band_h >= @min(raster.ih, seen_end + geometry.src.h)); + // ...and every row of the band is the whole page's row. + const stride = shape.stride; + try expectNearRows(whole[raster.band_y * stride ..][0 .. raster.band_h * stride], place.rgba); + if (step == 1) { + middle = try gpa.dupe(u8, place.rgba); + middle_y = raster.band_y; + } + } + + // Straight back to the middle from the foot: the rows rendered afresh + // are the very rows rendered on the way down, so no seam moves with the + // path a reader took. + _ = panes.Pdf.scrollPane(p, pane, @round(max_scroll / 2) - pv.document_scroll_y); + pv.scroll_travel = 0; + _ = frame.reset(.retain_capacity); + const again = (try p.render(frame.allocator())).images[0].?; + const again_y = panes.Pdf.rasterForPage(pv, 0).?.band_y; + const stride = shape.stride; + const common_top = @max(middle_y, again_y); + const common_end = @min(middle_y + middle.len / stride, again_y + again.rgba.len / stride); + try std.testing.expect(common_end > common_top); + try std.testing.expectEqualSlices( + u8, + middle[(common_top - middle_y) * stride .. (common_end - middle_y) * stride], + again.rgba[(common_top - again_y) * stride .. (common_end - again_y) * stride], + ); + _ = panes.Pdf.scrollPane(p, pane, max_scroll - pv.document_scroll_y); + pv.scroll_travel = 0; + _ = frame.reset(.retain_capacity); + const foot = try gpa.dupe(u8, (try p.render(frame.allocator())).images[0].?.rgba); + defer gpa.free(foot); + + // At the foot of the page, a search hit: its highlight lands on exactly + // the rows and pixels it would on the whole page, and a pointer over the + // word finds the word. + try pv.setSearchQuery(gpa, "bottommost"); + _ = frame.reset(.retain_capacity); + const lit = try p.render(frame.allocator()); + const place = lit.images[0].?; + const raster = panes.Pdf.rasterForPage(pv, 0) orelse return error.MissingPdfRaster; + const highlights = try panes.Pdf.buildHighlights(pv, frame.allocator(), panes.Pdf.highlightInput(p, 0, pane)); + const marks = highlights.forPage(0); + try std.testing.expect(marks.len > 0); + const rows = try pv.document.highlightRows(0, request, marks); + try std.testing.expect(rows.start >= raster.band_y and rows.end <= raster.band_y + raster.band_h); + // Exactly the highlight's rows changed, and nothing else... + try std.testing.expectEqual(foot.len, place.rgba.len); + const from = (rows.start - raster.band_y) * stride; + const to = (rows.end - raster.band_y) * stride; + try std.testing.expectEqualSlices(u8, foot[0..from], place.rgba[0..from]); + try std.testing.expectEqualSlices(u8, foot[to..], place.rgba[to..]); + try std.testing.expect(!std.mem.eql(u8, foot[from..to], place.rgba[from..to])); + // ...to what the whole page painted with them is. + const expected = try gpa.dupe(u8, plain[rows.start * stride .. rows.end * stride]); + defer gpa.free(expected); + try pv.document.paintHighlightsAt(0, request, shape, shape.band(rows.start, rows.end - rows.start), marks, expected); + try pdf_impl.tintRgba(expected, pv.tint, panes.Pdf.tintColors(p)); + try expectNearRows(expected, place.rgba[from..to]); + + const quad = pv.search_results.?.quads[0].quad; + const left = @min(quad.ul.x, quad.ll.x); + const right = @max(quad.ur.x, quad.lr.x); + const top = @min(quad.ul.y, quad.ur.y); + const bottom = @max(quad.ll.y, quad.lr.y); + const rect = p.rects[0]; + var inside: usize = 0; + for (rect.y..rect.y + rect.h) |row| for (rect.x..rect.x + rect.w) |col| { + const point = panes.Pdf.panePointAtPage(p, pane, 0, @intCast(col), @intCast(row), false) orelse continue; + if (point.x >= left and point.x <= right and point.y >= top and point.y <= bottom) inside += 1; + }; + try std.testing.expect(inside > 0); +} + /// The pixels a backend samples out of one placement: the source rectangle, /// row by row, at the texture's own stride. Test-only, and the one operation /// that makes "same picture" mean something when the textures differ in shape. @@ -1306,7 +1472,7 @@ test "PDF continuous strip renders every intersecting short page" { const viewport = panes.Pdf.ensurePaneLayout(p, pane).?; const visible = panes.Pdf.visiblePages(pv, viewport); try std.testing.expectEqual(pv.page_count, visible.len); - const request = panes.Pdf.renderRequest(viewport, pdf_raster_policy); + const request = panes.Pdf.renderRequest(viewport, pdf_raster_policy, pv.fit); try std.testing.expect(pv.page_count <= pv.rasters.len); for (0..pv.page_count) |page| { const rgba = try gpa.alloc(u8, @as(usize, viewport.pixel_w) * 4); diff --git a/test/pdf_scroll_bench.zig b/test/pdf_scroll_bench.zig index a4511dbe..166fa809 100644 --- a/test/pdf_scroll_bench.zig +++ b/test/pdf_scroll_bench.zig @@ -4,6 +4,7 @@ //! zig build pdf-scroll-bench -Doptimize=ReleaseFast -- --json //! zig build pdf-scroll-bench -Doptimize=ReleaseFast -- --warmup 1 --reps 15 //! zig build pdf-scroll-bench -Doptimize=ReleaseFast -- --path other.pdf +//! zig build pdf-scroll-bench -Doptimize=ReleaseFast -- --cell 16x32 a 2x display //! zig build pdf-scroll-bench -Doptimize=Debug -- --reps 1 the diagnosis //! zig build pdf-scroll-bench -Dtracy=~/05-genizah/tracy the zones //! @@ -62,7 +63,7 @@ const gpa = std.heap.smp_allocator; // is what the tty shell actually ships. const bench_cols: u16 = 120; const bench_rows: u16 = 40; -const bench_cell_pixels: pardes.CellPixels = .{ .w = 8, .h = 16 }; +const default_cell_pixels: pardes.CellPixels = .{ .w = 8, .h = 16 }; /// config.wheel_rows is one cell per notch, so a notch is `cell_pixels.h` /// display pixels. 192 notches is ~3072px — between two and three fit-width @@ -74,7 +75,7 @@ const slow_ticks_per_frame: usize = 1; /// The control row is a fixed wall of frames rather than a whole-document /// traversal: one notch per frame over a real document is tens of thousands of /// frames, and 240 already crosses several page boundaries at reading speed. -const slow_frames: usize = 240; +const default_slow_frames: usize = 240; /// Guard against a pathological document turning one row into a multi-minute /// run. Deterministic either way: the frame count is derived from layout. const max_frames: usize = 2048; @@ -91,6 +92,10 @@ const Config = struct { /// Override `max_frames` for one run: a 5000-page manual is 2000 frames of /// travel per round, which is a fine scoreboard and a poor edit-run loop. frames: ?usize = null, + /// One cell in display pixels: 16x32 is the same window on a 2x display. + cell: pardes.CellPixels = default_cell_pixels, + /// Frames of the one-notch control row. + slow_frames: usize = default_slow_frames, }; /// The bit-exact description of one scrolling run. `visual` is the invariant; @@ -120,6 +125,9 @@ const Result = struct { total_ns: u64, rasterizations: u64, rasters_retained: u64, + /// Most bytes the pane's rasters held after any frame: every slot's + /// pixels, its clean copy, and the spare buffers kept for reuse. + peak_raster_bytes: u64, identity: Identity, }; @@ -174,13 +182,13 @@ pub fn main(init: std.process.Init) !void { // One untimed probe boot supplies the strip height every plan is sized // from, so the frame counts below are a property of the document and the // viewport rather than of the machine. - const travel = probeTravel(config.path); + const travel = probeTravel(config); const cap = config.frames orelse max_frames; const results = [_]Result{ - try measure(config, planFor(.fling_down, travel, cap)), - try measure(config, planFor(.fling_reverse, travel, cap)), - try measure(config, planFor(.slow_scroll, travel, cap)), + try measure(config, planFor(.fling_down, travel, cap, config.slow_frames)), + try measure(config, planFor(.fling_reverse, travel, cap, config.slow_frames)), + try measure(config, planFor(.slow_scroll, travel, cap, config.slow_frames)), }; if (config.json) @@ -207,9 +215,19 @@ fn parseArgs(args: []const []const u8) Config { } else if (std.mem.eql(u8, args[i], "--frames") and i + 1 < args.len) { i += 1; config.frames = parseCount("--frames", args[i]); + } else if (std.mem.eql(u8, args[i], "--slow-frames") and i + 1 < args.len) { + i += 1; + config.slow_frames = parseCount("--slow-frames", args[i]); + } else if (std.mem.eql(u8, args[i], "--cell") and i + 1 < args.len) { + i += 1; + var it = std.mem.splitScalar(u8, args[i], 'x'); + const w = parseCount("--cell", it.next() orelse ""); + const h = parseCount("--cell", it.next() orelse ""); + if (w > std.math.maxInt(u16) or h > std.math.maxInt(u16)) fatal("--cell {s} is too large", .{args[i]}); + config.cell = .{ .w = @intCast(w), .h = @intCast(h) }; } else fatal( "usage: pardes-pdf-scroll-bench [--json] [--warmup N] [--reps N]" ++ - " [--path FILE] [--frames N]", + " [--path FILE] [--frames N] [--slow-frames N] [--cell WxH]", .{}, ); } @@ -226,28 +244,35 @@ const Travel = struct { /// same benchmark and the report must not pretend otherwise. pages: usize, strip_px: u64, + /// The first frame of a fresh pane: opening the document and rasterizing + /// what it shows first. One sample, from the probe boot. + first_frame_ns: u64, }; -fn probeTravel(path: []const u8) Travel { - const core = bootCore(path) catch |err| +fn probeTravel(config: Config) Travel { + const path = config.path; + const core = bootCore(config) catch |err| fatal("cannot boot a PDF pane on {s}: {t}", .{ path, err }); defer core.deinit(); var frame_arena = std.heap.ArenaAllocator.init(gpa); defer frame_arena.deinit(); + const started = nowNs(); _ = core.render(frame_arena.allocator()) catch |err| fatal("first render failed: {t}", .{err}); drainEffects(core); + const first_frame_ns = nowNs() -| started; const pane = core.panes[0] orelse fatal("PDF pane disappeared", .{}); if (pane.pdf == null) fatal("{s} did not open as a PDF", .{path}); const pv = &pane.pdf.?; - if (pv.page_count < 4) - fatal("{s} has {d} pages; a fling benchmark needs at least 4", .{ path, pv.page_count }); if (!pv.layout_valid or pv.document_height == 0) fatal("the pixel strip never laid out; is native_images/cell_pixels wired?", .{}); // Exactly the clamp scrollPdfDocument enforces, so the last fling frame // lands on the document end instead of adding a no-op frame to the row. - const viewport_h: u64 = @as(u64, bench_rows -| pardes.BOX_H) * bench_cell_pixels.h; - const notch: f64 = @floatFromInt(bench_cell_pixels.h); + const viewport_h: u64 = @as(u64, bench_rows -| pardes.BOX_H) * config.cell.h; + // Four screenfuls of strip, in pages or in one tall page. + if (pv.document_height < 4 * viewport_h) + fatal("{s} is {d}px of strip; a fling benchmark needs at least four screenfuls", .{ path, pv.document_height }); + const notch: f64 = @floatFromInt(config.cell.h); if (!std.math.isFinite(notch) or notch <= 0) fatal("a notch of {d} display pixels cannot size a fling", .{notch}); const strip = @as(f64, @floatFromInt(pv.document_height -| viewport_h)); @@ -258,6 +283,7 @@ fn probeTravel(path: []const u8) Travel { .per_frame = notch * @as(f64, @floatFromInt(fling_ticks_per_frame)), .pages = pv.page_count, .strip_px = pv.document_height, + .first_frame_ns = first_frame_ns, }; } @@ -267,7 +293,7 @@ fn probeTravel(path: []const u8) Travel { /// The cap is applied before any arithmetic on the count, so a document the /// float division cannot describe still produces a runnable plan instead of an /// overflow. -fn planFor(scenario: Scenario, travel: Travel, cap: usize) Plan { +fn planFor(scenario: Scenario, travel: Travel, cap: usize, slow: usize) Plan { const wanted = travel.max_scroll / travel.per_frame; const whole: usize = if (std.math.isFinite(wanted) and wanted >= 1) @intFromFloat(@min(@as(f64, @floatFromInt(cap)), @ceil(wanted))) @@ -282,15 +308,15 @@ fn planFor(scenario: Scenario, travel: Travel, cap: usize) Plan { .frames = @max(2, down), .down_frames = @max(1, down / 2), }, - .slow_scroll => .{ .scenario = scenario, .frames = slow_frames, .down_frames = slow_frames }, + .slow_scroll => .{ .scenario = scenario, .frames = slow, .down_frames = slow }, }; } /// A pane on a real multi-page document with the native pixel-strip path live /// and a genuine pixel viewport delivered the way a shell delivers one. -fn bootCore(path: []const u8) !*pardes.Pardes { +fn bootCore(config: Config) !*pardes.Pardes { const core = try pardes.Pardes.init(gpa, .{ - .file = path, + .file = config.path, .cols = bench_cols, .rows = bench_rows, }); @@ -299,7 +325,7 @@ fn bootCore(path: []const u8) !*pardes.Pardes { core.update(.{ .resize = .{ .cols = bench_cols, .rows = bench_rows, - .cell_pixels = bench_cell_pixels, + .cell_pixels = config.cell, } }); drainEffects(core); return core; @@ -318,9 +344,10 @@ fn measure(config: Config, plan: Plan) !Result { var expected: ?Identity = null; var rasterizations: u64 = 0; var rasters_retained: u64 = 0; + var peak_raster_bytes: u64 = 0; for (0..config.warmup + config.reps) |round| { - const core = try bootCore(config.path); + const core = try bootCore(config); defer core.deinit(); var frame_arena = std.heap.ArenaAllocator.init(gpa); defer frame_arena.deinit(); @@ -355,6 +382,7 @@ fn measure(config: Config, plan: Plan) !Result { const elapsed = nowNs() -| started; total += elapsed; recorder.observe(frame, surface); + peak_raster_bytes = @max(peak_raster_bytes, rasterBytes(pv)); // The same frame boundary src/tty/tty.zig marks, so a Tracy capture // of this benchmark reads like a capture of the real shell. pardes.frameMark(); @@ -388,10 +416,19 @@ fn measure(config: Config, plan: Plan) !Result { .total_ns = totals[totals.len / 2], .rasterizations = rasterizations, .rasters_retained = rasters_retained, + .peak_raster_bytes = peak_raster_bytes, .identity = expected.?, }; } +/// What the pane's rasters hold right now (untimed: outside the frame). +fn rasterBytes(pv: anytype) u64 { + var total: u64 = 0; + for (pv.rasters[0..pv.rasters_len]) |raster| total += raster.rgba.len + raster.clean.len; + for (pv.spare[0..pv.spare_len]) |spare| total += spare.len; + return total; +} + /// Accumulates the two checksums across one run. Raster generations are /// rebased on the counter's value at run start so the trace describes the /// ORDER textures were produced in rather than absolute counter values, which @@ -545,23 +582,24 @@ fn reportText(config: Config, travel: Travel, results: []const Result) void { std.debug.print( "{s}: {d} pages, {d}px strip at {d}x{d} cells of {d}x{d}px\n", .{ - config.path, travel.pages, travel.strip_px, bench_cols, - bench_rows, bench_cell_pixels.w, bench_cell_pixels.h, + config.path, travel.pages, travel.strip_px, bench_cols, + bench_rows, config.cell.w, config.cell.h, }, ); + std.debug.print("first frame of a fresh pane: {d}us\n", .{travel.first_frame_ns / 1000}); std.debug.print( "warmup: {d}, sampled reps: {d}; every number is ONE FRAME (notch batch + render + drain)\n\n", .{ config.warmup, config.reps }, ); - std.debug.print("{s:<16} {s:>6} {s:>7} {s:>10} {s:>10} {s:>10} {s:>10} {s:>10} {s:>11} {s:>7} {s:>5} {s:<16} {s}\n", .{ - "scenario", "frames", "notches", "min", - "median", "p90", "p99", "max", - "total_us", "rasters", "kept", "visual", - "transport", + std.debug.print("{s:<16} {s:>6} {s:>7} {s:>10} {s:>10} {s:>10} {s:>10} {s:>10} {s:>11} {s:>7} {s:>5} {s:>9} {s:<16} {s}\n", .{ + "scenario", "frames", "notches", "min", + "median", "p90", "p99", "max", + "total_us", "rasters", "kept", "peak_kib", + "visual", "transport", }); std.debug.print("{s}\n", .{"-" ** 160}); for (results) |result| std.debug.print( - "{s:<16} {d:>6} {d:>7} {d:>10} {d:>10} {d:>10} {d:>10} {d:>10} {d:>11} {d:>7} {d:>5} {x:0>16} {x:0>16}\n", + "{s:<16} {d:>6} {d:>7} {d:>10} {d:>10} {d:>10} {d:>10} {d:>10} {d:>11} {d:>7} {d:>5} {d:>9} {x:0>16} {x:0>16}\n", .{ result.name, result.frames, @@ -574,12 +612,14 @@ fn reportText(config: Config, travel: Travel, results: []const Result) void { result.total_ns / 1000, result.rasterizations, result.rasters_retained, + result.peak_raster_bytes / 1024, result.identity.visual, result.identity.transport, }, ); std.debug.print( - "\n`rasters` counts MuPDF page rasterizations over one run; `kept` is the retained raster count at the end.\n" ++ + "\n`rasters` counts MuPDF page rasterizations over one run; `kept` is the retained raster count at the end;\n" ++ + "`peak_kib` is the most the pane's rasters, clean copies and spares held after any frame.\n" ++ "`visual` pins presented pixels and placement and must never change; `transport` traces texture uploads.\n", .{}, ); @@ -592,16 +632,17 @@ fn reportJson(io: std.Io, config: Config, travel: Travel, results: []const Resul var out: std.Io.Writer = .fixed(&storage); out.print( "{{\"benchmark\":\"pardes-pdf-scroll\",\"build\":\"{s}\",\"path\":\"{s}\"," ++ - "\"pages\":{d},\"strip_px\":{d},\"warmup\":{d},\"reps\":{d},\"results\":[", + "\"pages\":{d},\"strip_px\":{d},\"first_frame_ns\":{d},\"warmup\":{d},\"reps\":{d},\"results\":[", .{ - bench_config.core_optimize, config.path, travel.pages, - travel.strip_px, config.warmup, config.reps, + bench_config.core_optimize, config.path, travel.pages, + travel.strip_px, travel.first_frame_ns, config.warmup, + config.reps, }, ) catch return; for (results, 0..) |result, i| out.print( "{s}{{\"scenario\":\"{s}\",\"frames\":{d},\"notches_per_frame\":{d}," ++ "\"min_ns\":{d},\"median_ns\":{d},\"p90_ns\":{d},\"p99_ns\":{d},\"max_ns\":{d},\"total_ns\":{d}," ++ - "\"rasterizations\":{d},\"rasters_retained\":{d},\"images\":{d}," ++ + "\"rasterizations\":{d},\"rasters_retained\":{d},\"peak_raster_bytes\":{d},\"images\":{d}," ++ "\"visual\":\"{x:0>16}\",\"transport\":\"{x:0>16}\"}}", .{ if (i == 0) "" else ",", @@ -616,6 +657,7 @@ fn reportJson(io: std.Io, config: Config, travel: Travel, results: []const Resul result.total_ns, result.rasterizations, result.rasters_retained, + result.peak_raster_bytes, result.identity.images, result.identity.visual, result.identity.transport, |
