//! 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) { 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}"); } }