-
Notifications
You must be signed in to change notification settings - Fork 131
perf(cg): render cost prediction module, prefill skip optimization, and Skia benchmarks #619
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
5ddb8f6
perf(cg): skip prefill loop on cache-warm frames via generation tracking
softmarshmallow 491413e
style(cg): cargo fmt – fix formatting in scene.rs
softmarshmallow 855e48c
Merge pull request #618 from gridaco/feature/stupefied-mendeleev
softmarshmallow ec0d857
docs(cg): add render cost prediction reference and validation benchmarks
softmarshmallow 20a72e2
refactor(cg): extract cost prediction as debug-only module
softmarshmallow 7fb4e28
feat(tests): add box-margin.html to demonstrate CSS margin behaviors
softmarshmallow 6392699
Merge remote-tracking branch 'origin/main' into canary
softmarshmallow 32e43fe
style(cg): cargo fmt
softmarshmallow File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
265 changes: 265 additions & 0 deletions
265
crates/grida-canvas/examples/skia_bench/skia_bench_cache_blit.rs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,265 @@ | ||
| //! Cache Hit vs. Miss Cost Ratio Benchmark | ||
| //! | ||
| //! Measures the actual cost ratio between a cache hit (GPU texture blit) and | ||
| //! a cache miss (full rasterization). Validates the ~0.1× estimate from | ||
| //! `docs/wg/feat-2d/render-cost-prediction.md`. | ||
| //! | ||
| //! Run with: | ||
| //! ```bash | ||
| //! cargo run -p cg --example skia_bench_cache_blit --features native-gl-context --release | ||
| //! ``` | ||
|
|
||
| #[cfg(not(feature = "native-gl-context"))] | ||
| fn main() { | ||
| eprintln!("This example requires --features native-gl-context"); | ||
| } | ||
|
|
||
| #[cfg(feature = "native-gl-context")] | ||
| fn main() { | ||
| use cg::window::headless::HeadlessGpu; | ||
| use skia_safe::{ | ||
| canvas::SaveLayerRec, image_filters, Color, Image, ImageInfo, Paint, Rect, Surface, | ||
| }; | ||
| use std::time::Instant; | ||
|
|
||
| const W: i32 = 1000; | ||
| const H: i32 = 1000; | ||
| const WARMUP: u32 = 10; | ||
| const ITERS: u32 = 50; | ||
|
|
||
| let mut gpu = HeadlessGpu::new(W, H).expect("GPU init"); | ||
| gpu.print_gl_info(); | ||
| println!(); | ||
|
|
||
| let surface = &mut gpu.surface; | ||
|
|
||
| // ── Helpers ────────────────────────────────────────────────────── | ||
|
|
||
| fn flush(s: &mut Surface) { | ||
| if let Some(mut ctx) = s.recording_context() { | ||
| if let Some(mut d) = ctx.as_direct_context() { | ||
| d.flush_and_submit(); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /// Measure median time (µs) for a drawing operation. | ||
| fn bench_draw(surface: &mut Surface, draw_fn: &dyn Fn(&skia_safe::Canvas)) -> f64 { | ||
| // Warmup | ||
| for _ in 0..WARMUP { | ||
| let canvas = surface.canvas(); | ||
| canvas.clear(Color::WHITE); | ||
| draw_fn(canvas); | ||
| flush(surface); | ||
| } | ||
| // Measure | ||
| let mut timings = Vec::with_capacity(ITERS as usize); | ||
| for _ in 0..ITERS { | ||
| let t0 = Instant::now(); | ||
| let canvas = surface.canvas(); | ||
| canvas.clear(Color::WHITE); | ||
| draw_fn(canvas); | ||
| flush(surface); | ||
| timings.push(t0.elapsed().as_nanos() as f64 / 1000.0); | ||
| } | ||
| timings.sort_by(|a, b| a.partial_cmp(b).unwrap()); | ||
| timings[timings.len() / 2] | ||
| } | ||
|
|
||
| /// Capture a rect with effects into a GPU-resident Image. | ||
| fn capture_to_image( | ||
| surface: &mut Surface, | ||
| size: i32, | ||
| draw_fn: &dyn Fn(&skia_safe::Canvas, Rect), | ||
| ) -> Image { | ||
| let info = ImageInfo::new_n32_premul((size, size), None); | ||
| let mut offscreen = surface.new_surface(&info).expect("offscreen surface"); | ||
| { | ||
| let canvas = offscreen.canvas(); | ||
| canvas.clear(Color::TRANSPARENT); | ||
| let rect = Rect::from_xywh(0.0, 0.0, size as f32, size as f32); | ||
| draw_fn(canvas, rect); | ||
| } | ||
| flush(surface); | ||
| offscreen.image_snapshot() | ||
| } | ||
|
|
||
| // ── Effect configurations ─────────────────────────────────────── | ||
|
|
||
| struct EffectConfig { | ||
| name: &'static str, | ||
| draw: Box<dyn Fn(&skia_safe::Canvas, Rect)>, | ||
| } | ||
|
|
||
| let shadow_filter = image_filters::drop_shadow( | ||
| (4.0, 4.0), | ||
| (8.0, 8.0), | ||
| Color::from_argb(128, 0, 0, 0), | ||
| None, | ||
| None, | ||
| None, | ||
| ); | ||
| let blur_filter = image_filters::blur((8.0, 8.0), None, None, None); | ||
|
|
||
| let sf = shadow_filter.clone(); | ||
| let blf = blur_filter.clone(); | ||
| let sf2 = shadow_filter.clone(); | ||
|
|
||
| let effects: Vec<EffectConfig> = vec![ | ||
| EffectConfig { | ||
| name: "solid rect", | ||
| draw: Box::new(|canvas, rect| { | ||
| let mut p = Paint::default(); | ||
| p.set_color(Color::from_argb(255, 66, 133, 244)); | ||
| canvas.draw_rect(rect, &p); | ||
| }), | ||
| }, | ||
| EffectConfig { | ||
| name: "rect + blur (s=8)", | ||
| draw: Box::new(move |canvas, rect| { | ||
| let mut lp = Paint::default(); | ||
| lp.set_image_filter(blf.clone()); | ||
| let rec = SaveLayerRec::default().bounds(&rect).paint(&lp); | ||
| canvas.save_layer(&rec); | ||
| let mut p = Paint::default(); | ||
| p.set_color(Color::from_argb(255, 66, 133, 244)); | ||
| canvas.draw_rect(rect, &p); | ||
| canvas.restore(); | ||
| }), | ||
| }, | ||
| EffectConfig { | ||
| name: "rect + shadow (s=8)", | ||
| draw: Box::new(move |canvas, rect| { | ||
| let mut lp = Paint::default(); | ||
| lp.set_image_filter(sf.clone()); | ||
| let rec = SaveLayerRec::default().bounds(&rect).paint(&lp); | ||
| canvas.save_layer(&rec); | ||
| let mut p = Paint::default(); | ||
| p.set_color(Color::from_argb(255, 66, 133, 244)); | ||
| canvas.draw_rect(rect, &p); | ||
| canvas.restore(); | ||
| }), | ||
| }, | ||
| EffectConfig { | ||
| name: "complex (3 fills + stroke + shadow)", | ||
| draw: Box::new(move |canvas, rect| { | ||
| let mut lp = Paint::default(); | ||
| lp.set_image_filter(sf2.clone()); | ||
| let rec = SaveLayerRec::default().bounds(&rect).paint(&lp); | ||
| canvas.save_layer(&rec); | ||
| // 3 fills | ||
| let mut p1 = Paint::default(); | ||
| p1.set_color(Color::from_argb(255, 66, 133, 244)); | ||
| canvas.draw_rect(rect, &p1); | ||
| let mut p2 = Paint::default(); | ||
| p2.set_color(Color::from_argb(128, 255, 0, 0)); | ||
| canvas.draw_rect(rect, &p2); | ||
| let mut p3 = Paint::default(); | ||
| p3.set_color(Color::from_argb(64, 0, 255, 0)); | ||
| canvas.draw_rect(rect, &p3); | ||
| // 1 stroke | ||
| let mut s = Paint::default(); | ||
| s.set_color(Color::BLACK); | ||
| s.set_style(skia_safe::PaintStyle::Stroke); | ||
| s.set_stroke_width(2.0); | ||
| canvas.draw_rect(rect, &s); | ||
| canvas.restore(); | ||
| }), | ||
| }, | ||
| ]; | ||
|
|
||
| let sizes: [i32; 3] = [100, 200, 500]; | ||
|
|
||
| // ── Run benchmarks ────────────────────────────────────────────── | ||
|
|
||
| println!("═══════════════════════════════════════════════════════════════════════════"); | ||
| println!(" SECTION 1: Cache Hit vs. Miss Ratio"); | ||
| println!("═══════════════════════════════════════════════════════════════════════════"); | ||
| println!( | ||
| " {:<36} {:>5} {:>10} {:>10} {:>10}", | ||
| "Effect", "Size", "Miss(µs)", "Hit(µs)", "Ratio" | ||
| ); | ||
| println!( | ||
| " {:-<36} {:->5} {:->10} {:->10} {:->10}", | ||
| "", "", "", "", "" | ||
| ); | ||
|
|
||
| // blit_times[effect_idx][size_idx] for constancy check | ||
| let mut blit_times: Vec<Vec<f64>> = vec![Vec::new(); effects.len()]; | ||
|
|
||
| for (ei, effect) in effects.iter().enumerate() { | ||
| for (si, &size) in sizes.iter().enumerate() { | ||
| let sizef = size as f32; | ||
| let cx = (W as f32 - sizef) / 2.0; | ||
| let cy = (H as f32 - sizef) / 2.0; | ||
| let dst_rect = Rect::from_xywh(cx, cy, sizef, sizef); | ||
|
|
||
| // Cache miss: full rasterize | ||
| let miss_us = bench_draw(surface, &|canvas| { | ||
| (effect.draw)(canvas, dst_rect); | ||
| }); | ||
|
|
||
| // Capture to GPU texture | ||
| let cached_image = capture_to_image(surface, size, &*effect.draw); | ||
|
|
||
| // Cache hit: texture blit | ||
| let hit_us = bench_draw(surface, &|canvas| { | ||
| canvas.draw_image_rect(&cached_image, None, dst_rect, &Paint::default()); | ||
| }); | ||
|
|
||
| let ratio = hit_us / miss_us; | ||
| blit_times[ei].push(hit_us); | ||
|
|
||
| println!( | ||
| " {:<36} {:>4}² {:>10.1} {:>10.1} {:>9.3}×", | ||
| effect.name, size, miss_us, hit_us, ratio | ||
| ); | ||
|
|
||
| eprint!( | ||
| "\r [{}/{}]", | ||
| ei * sizes.len() + si + 1, | ||
| effects.len() * sizes.len() | ||
| ); | ||
| } | ||
| } | ||
| eprintln!("\r Done.{:40}", ""); | ||
|
|
||
| // ── Output Section 2: Blit Constancy ──────────────────────────── | ||
|
|
||
| println!(); | ||
| println!("═══════════════════════════════════════════════════════════════════════════"); | ||
| println!(" SECTION 2: Blit Cost Constancy (same size, different source complexity)"); | ||
| println!("═══════════════════════════════════════════════════════════════════════════"); | ||
| println!(" Blit cost should NOT vary with source effect complexity at the same size."); | ||
| println!(); | ||
|
|
||
| for (si, &size) in sizes.iter().enumerate() { | ||
| let blit_at_size: Vec<f64> = blit_times.iter().map(|bt| bt[si]).collect(); | ||
| let mean = blit_at_size.iter().sum::<f64>() / blit_at_size.len() as f64; | ||
| let variance = blit_at_size.iter().map(|v| (v - mean).powi(2)).sum::<f64>() | ||
| / blit_at_size.len() as f64; | ||
| let stddev = variance.sqrt(); | ||
| let cv = if mean > 0.0 { | ||
| stddev / mean * 100.0 | ||
| } else { | ||
| 0.0 | ||
| }; | ||
|
|
||
| println!(" Size {}²:", size); | ||
| for (ei, effect) in effects.iter().enumerate() { | ||
| println!(" {:<36} {:>8.1} µs", effect.name, blit_times[ei][si]); | ||
| } | ||
| println!( | ||
| " mean={:.1} µs stddev={:.1} µs CV={:.1}% {}", | ||
| mean, | ||
| stddev, | ||
| cv, | ||
| if cv < 10.0 { "OK" } else { "WARN (>10%)" } | ||
| ); | ||
| println!(); | ||
| } | ||
|
|
||
| println!(" Expected: CV < 10% at each size (blit cost independent of source complexity)"); | ||
| println!(" Reference: predicted cache-hit ratio ~0.1× (from cost model doc)"); | ||
| println!(); | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Miss(µs)is timing a direct draw, not a cache miss.miss_usis recorded before the offscreen render + snapshot needed to populate the cache. That means the printed ratio is comparingblitvsdraw, notblitvsmiss + populate, so the "cache hit vs. miss" numbers are systematically optimistic. Either time the populate step inside the miss path or rename the benchmark/columns to reflect what is actually being measured.Also applies to: 210-215
🤖 Prompt for AI Agents