nigig-org/tools/analysis/pdf_drawcall_census.rs

194 lines
7.2 KiB
Rust

//! Count the draw calls the Makepad PDF renderer would issue for every page
//! in the corpus, and what a batched strategy would issue instead.
//!
//! This replays the *exact* state machine in `pdf-makepad/src/renderer.rs`:
//! - `render()` opens with `begin()`.
//! - `finish_path()` calls `DrawVector::end()` (a real draw call) whenever
//! `has_path` is set, and is invoked on Save, Restore, PushClip, PopClip
//! and Clip*.
//! - Stroke / Fill* end the accumulation and `begin()` a fresh one, but do
//! NOT call `end()` themselves.
//! - Text and images go through DrawText / DrawImage, which are separate
//! draw objects and so break the vector batch in submission order.
use nigig_pdf_graphics::content::parse_content_stream;
use nigig_pdf_graphics::recording::{RecordingDevice, RenderCommand};
use std::path::{Path, PathBuf};
#[derive(Default, Debug, Clone)]
struct Census {
commands: usize,
vector_end_calls: usize,
text_draws: usize,
image_draws: usize,
saves: usize,
restores: usize,
clips: usize,
paint_ops: usize,
// Counterfactual: flush only when a clip changes, since colour and CTM
// are baked into vertices on the CPU and need no draw-call boundary.
batched_vector_ends: usize,
// Text runs that are contiguous (no intervening vector paint), which a
// single DrawText batch could absorb.
text_batches: usize,
}
fn census(commands: &[RenderCommand]) -> Census {
let mut c = Census::default();
c.commands = commands.len();
// Mirrors PdfRenderer::render: begin() then has_path = true.
let mut has_path = true;
let mut batched_open = true;
let mut last_was_text = false;
let finish_path = |has_path: &mut bool, c: &mut Census| {
if *has_path {
c.vector_end_calls += 1;
*has_path = false;
}
};
for cmd in commands {
match cmd {
RenderCommand::Save => {
finish_path(&mut has_path, &mut c);
c.saves += 1;
}
RenderCommand::Restore => {
finish_path(&mut has_path, &mut c);
c.restores += 1;
}
RenderCommand::PushClip | RenderCommand::PopClip => {
finish_path(&mut has_path, &mut c);
c.clips += 1;
if batched_open { c.batched_vector_ends += 1; batched_open = false; }
}
RenderCommand::ClipEvenOdd | RenderCommand::ClipWinding => {
finish_path(&mut has_path, &mut c);
c.clips += 1;
if batched_open { c.batched_vector_ends += 1; batched_open = false; }
}
RenderCommand::MoveTo(..)
| RenderCommand::LineTo(..)
| RenderCommand::CurveTo(..)
| RenderCommand::Rectangle(..) => {
has_path = true;
}
RenderCommand::Stroke
| RenderCommand::FillEvenOdd
| RenderCommand::FillWinding
| RenderCommand::FillStrokeEvenOdd
| RenderCommand::FillStrokeWinding => {
// tessellates into the accumulator; begins a fresh one
c.paint_ops += 1;
has_path = true;
batched_open = true;
last_was_text = false;
}
RenderCommand::ShowText(..) | RenderCommand::ShowTextWithMetrics { .. } => {
c.text_draws += 1;
if !last_was_text { c.text_batches += 1; }
last_was_text = true;
}
RenderCommand::DrawImage { .. } | RenderCommand::DrawInlineImage { .. } => {
c.image_draws += 1;
}
_ => {}
}
}
// final finish_path() at the end of render()
finish_path(&mut has_path, &mut c);
if batched_open { c.batched_vector_ends += 1; }
c
}
fn collect_pdfs(dir: &Path, out: &mut Vec<PathBuf>) {
let Ok(rd) = std::fs::read_dir(dir) else {
return;
};
for e in rd.flatten() {
let p = e.path();
if p.is_dir() {
collect_pdfs(&p, out);
} else if p.extension().and_then(|s| s.to_str()) == Some("pdf") {
out.push(p);
}
}
}
fn main() {
let root = std::env::args()
.nth(1)
.unwrap_or_else(|| "tests/corpus".to_string());
let mut pdfs = Vec::new();
collect_pdfs(Path::new(&root), &mut pdfs);
pdfs.sort();
let mut total = Census::default();
let mut pages = 0usize;
let mut worst: Vec<(usize, String)> = Vec::new();
for path in &pdfs {
let Ok(bytes) = std::fs::read(path) else {
continue;
};
let Ok(mut doc) = nigig_pdf_document::PdfDocument::parse(&bytes) else {
continue;
};
for i in 0..doc.page_count() {
let Ok(page) = doc.page(i) else { continue };
let content = page.content_data.clone();
let Ok(ops) = parse_content_stream(&content) else {
continue;
};
let cmds = RecordingDevice::from_ops(&ops);
let c = census(&cmds);
if c.commands == 0 {
continue;
}
pages += 1;
total.commands += c.commands;
total.vector_end_calls += c.vector_end_calls;
total.text_draws += c.text_draws;
total.image_draws += c.image_draws;
total.saves += c.saves;
total.restores += c.restores;
total.clips += c.clips;
total.paint_ops += c.paint_ops;
total.batched_vector_ends += c.batched_vector_ends;
total.text_batches += c.text_batches;
let dc = c.vector_end_calls + c.text_draws + c.image_draws;
worst.push((dc, format!("{}#{}", path.display(), i)));
}
}
worst.sort_by(|a, b| b.0.cmp(&a.0));
let dc = total.vector_end_calls + total.text_draws + total.image_draws;
println!("pages measured : {pages}");
println!("render commands : {}", total.commands);
println!("--- draw calls, as the renderer stands ---");
println!("DrawVector::end() calls : {}", total.vector_end_calls);
println!("DrawText::draw_abs() : {}", total.text_draws);
println!("DrawImage::draw_abs() : {}", total.image_draws);
println!("TOTAL draw calls : {dc}");
println!(" per page (mean) : {:.1}", dc as f64 / pages as f64);
println!("--- what forced the breaks ---");
println!("Save : {}", total.saves);
println!("Restore : {}", total.restores);
println!("clip ops : {}", total.clips);
println!("fill/stroke paint ops : {}", total.paint_ops);
let batched = total.batched_vector_ends + total.text_batches + total.image_draws;
println!("--- counterfactual: flush only on clip change ---");
println!("DrawVector::end() calls : {}", total.batched_vector_ends);
println!("DrawText batches : {}", total.text_batches);
println!("TOTAL draw calls : {batched}");
println!(" per page (mean) : {:.1}", batched as f64 / pages as f64);
println!(" reduction : {:.1}x", dc as f64 / batched as f64);
println!("--- worst pages ---");
for (n, name) in worst.iter().take(10) {
println!("{n:>7} {name}");
}
}