Skip to main content

render-perf

Guides performance optimization work on the Grida Canvas Rust rendering engine (grida crate). Use when asked to benchmark, profile, optimize, or analyze rendering performance — panning, zooming, compositing, caching, culling, or frame budgeting.

Source facts

Repository
gridaco/nothing
Last source activity
July 17, 2026 at 14:06
Detected SKILL.md language
English
Stars
54
Forks
3

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
render-perf
description
Guides performance optimization work on the Grida Canvas Rust rendering engine (grida crate). Use when asked to benchmark, profile, optimize, or analyze rendering performance — panning, zooming, compositing, caching, culling, or frame budgeting.
# Grida Canvas Engine — Performance Development Workflow and reasoning framework for render performance work on the Grida Canvas Rust engine. ## When to Use This Skill - Benchmarking canvas rendering - Optimizing frame time for any camera operation - Designing or modifying caching strategies - Analyzing benchmark results - Writing or auditing benchmarks - Investigating performance regressions --- ## How to Orient Yourself Before touching any code, build context by reading these sources in order: 1. **Read `crates/grida/AGENTS.md`** — crate conventions, test/check commands, benchmarking instructions. 2. **Read `docs/wg/feat-2d/optimization.md`** — the master optimization document. It describes every optimization strategy, measured costs, and the rendering pipeline. This is the single most important file. 3. **Browse `docs/wg/research/chromium/`** — research notes on how Chromium solves equivalent problems. Start with `index.md` for the map, then read whichever documents match the problem at hand. 4. **Read existing benchmarks** — look in `crates/grida/benches/` to understand what is already measured and how. 5. **Read `crates/grida_dev/src/main.rs`** — the `bench` and `bench-report` subcommands show how GPU benchmarks work with real `.grida` scene files. `bench-report` is the bulk mode that outputs JSON across all fixtures. Use `grep` and `glob` to discover the current state of code rather than relying on hardcoded paths. File locations shift as the engine evolves. ### Key discovery queries | What you need | How to find it | | ----------------------------------------- | --------------------------------------------------------------------------------------------------- | | The renderer entry point | `grep "struct Renderer" --include="*.rs"` in `crates/grida/src/` | | Camera change classification | `grep "enum CameraChangeKind" --include="*.rs"` | | How compositor cache works | `grep "struct LayerImage" --include="*.rs"` and read the containing module | | Promotion heuristics | `grep "fn should_promote" --include="*.rs"` | | Where zoom invalidation happens | `grep "zoom_changed\|mark_all_stale\|invalidate_all" --include="*.rs"` in `src/runtime/` | | Frame pipeline flow | Search for `fn queue\|fn flush\|fn draw\|fn frame` in the renderer file | | Benchmark fixture scenes | `--list-scenes` flag on `grida_dev bench`, or run `bench-report` on a directory | | Config toggles (compositing, atlas, etc.) | `grep "set_layer_compositing\|set_compositor_atlas\|set_interaction_render_scale" --include="*.rs"` | | Existing `.plan.md` proposals | `glob "docs/wg/feat-2d/*.plan.md"` | | Scene loading pipeline | `grep "fn load_scene" --include="*.rs"` in `src/runtime/scene.rs` | | Layout engine entry point | `grep "fn compute\b" --include="*.rs"` in `src/layout/engine.rs` | | Text measurement stats | `grep "ParagraphMeasureStats" --include="*.rs"` | | Skip-layout config | `grep "skip_layout" --include="*.rs"` in `src/runtime/` | | Load-bench CLI tool | Read `crates/grida_dev/src/bench/load_bench.rs` | --- ## The Benchmark Systems There are four complementary benchmarks. The bulk report is the recommended starting point for camera-motion work; the node-mutation bench is the go-to for invalidation-pipeline work; the single-scene bench and Criterion provide deeper investigation when needed. | Class | What moves between frames | Tool | Measures | | --------------- | ---------------------------------- | ---------------------------------- | ---------------------------------------------- | | Camera motion | `Camera2D` (pan/zoom) | `grida_dev bench` scenarios | draw + compositor + caches on stable scene | | Node mutation | Scene graph (translate / replace) | `grida_dev bench --translate` | `apply_changes` (invalidation pipeline) + draw | | Bulk regression | All scenes, camera motion | `grida_dev bench-report` (JSON) | per-scene PassStats across the fixture set | | Algorithmic | Synthetic grid, various operations | `cargo bench -p grida` (Criterion) | CPU-only cost with statistical CI | ### 1. Bulk benchmark report (`grida_dev bench-report`) Runs all scenes in all `.grida` files and outputs a compact **JSON report**. Use this to establish baselines, detect regressions across the full fixture set, and identify which scenes/stages are slowest. ```sh # All fixtures — recommended first step cargo run -p grida_dev --release -- bench-report ./fixtures/ --frames 100 --output baseline.json # Single file cargo run -p grida_dev --release -- bench-report ./fixtures/test-grida/bench.grida --frames 200 # Local fixtures for broader coverage cargo run -p grida_dev --release -- bench-report ./fixtures/local/ --frames 100 --output baseline-local.json ``` The JSON report contains per-scene results with: - `nodes`, `effects_nodes` — scene complexity - `fit_zoom` — the zoom level from `fit_camera_to_scene` - `pan` / `zoom` — legacy passes with full `PassStats` - `scenarios[]` — expanded scenario matrix (see below) - `errors[]` — files that failed to load Each `PassStats` contains: - `avg_us`, `fps`, `min_us`, `p50_us`, `p95_us`, `p99_us`, `max_us` — latency distribution - `queue_us`, `draw_us`, `mid_flush_us`, `compositor_us`, `flush_us` — per-stage breakdown - `settle_us` — cost of the stable (settle) frame after the pass ends Progress goes to stderr, JSON to stdout (or `--output path`). This keeps the JSON clean for programmatic consumption. ### 2. Single-scene GPU benchmark (`grida_dev bench`) Runs real scene data on the actual GPU backend (Metal/GL). This is the ground truth for "does the user experience improve?" ```sh # List scenes to pick the right one cargo run -p grida_dev --release -- bench ./fixtures/test-grida/bench.grida --list-scenes # Run a specific scene cargo run -p grida_dev --release -- bench ./fixtures/test-grida/bench.grida --scene <N> --frames 200 ``` **Always use `--release`.** Debug builds are 20-30x slower and produce meaningless data. The output reports legacy pan/zoom, then an expanded scenario matrix covering 20+ scenarios across multiple gesture types. Each scenario reports `min/p50/p95/p99/MAX` plus per-stage breakdown and settle cost. **Scenario types in the expanded matrix:** | Kind | Scenarios | What it tests | | ----------------------- | --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | | `pan` | slow/fast × fit/zoomed | Linear back-and-forth panning | | `circle_pan` | small/large radius × fit/zoomed | Circular trackpad gesture (unpredictable edges) | | `zigzag` | fast (continuous) / slow (with pauses) × fit/zoomed | Diagonal reading pattern with direction changes | | `zoom` | slow/fast × around-fit/high | Zoom oscillation at different levels | | `pan_with_settle` | slow/fast × fit/zoomed | Pan with settle frames interleaved every 12 frames | | `zoom_with_settle` | slow/fast × fit/high | Zoom with settle frames interleaved every 12 frames — captures cache-cold spike after settle nukes zoom cache | | `baseline_nocache_zoom` | slow/fast × fit/high | Forces `stable=true` on every zoom frame — no-cache baseline measuring raw full-draw cost for A/B comparison | | `realtime` | fast/slow × fit/zoomed | **Real-time event loop simulation** with sleep, 240Hz tick thread, and settle countdown matching the native viewer | | `frameloop` | 16/50/80/120/200/300/500ms interval | **Real FrameLoop path** — the only bench that captures stable-frame jank during panning (see below) | | `frameloop_zoom` | 16/50/80/120/200/500ms interval | **Real FrameLoop path for zoom** — captures stable-frame intrusion during zoom gestures | | `resize` | alternating viewport sizes | `--resize` flag. Measures `resize()` + `redraw()` cost per cycle (layout rebuild + cache invalidation + repaint) | **SurfaceUI overlay measurement (`--overlay`):** By default, benchmarks measure content rendering only — the SurfaceUI overlay (frame titles, node badges, hit regions) is **not** included. Pass `--overlay` to include overlay drawing after each content flush, matching the real `Application::frame()` pipeline where `draw_and_flush_devtools_overlay()` runs after `Renderer::flush()`. ```sh # A/B test: content only vs content + overlay (MUST be sequential, never parallel) cargo run -p grida_dev --release -- bench ./fixtures/test-grida/L0.grida --scene 22 --frames 200 && \ cargo run -p grida_dev --release -- bench ./fixtures/test-grida/L0.grida --scene 22 --frames 200 --overlay # Bulk report with overlay cargo run -p grida_dev --release -- bench-report ./fixtures/ --frames 100 --overlay --output overlay.json ``` The overlay cost is opt-in because it is a devtools feature, not user content. Overlay cost scales with visible labeled nodes — viewport culling skips off-screen labels, so zoomed-in views are nearly free. At fit-zoom on large scenes (e.g. 500 labels visible), overlay adds ~1.8ms per frame (paragraph layout dominates). At typical editing zoom, the cost drops to ~190µs or less. | Question | Flag | | ---------------------------------------- | -------------- | | Is content rendering fast enough? | (no flag) | | Is the overlay adding visible latency? | `--overlay` | | Compare content-only vs content+overlay? | Run both, diff | The `realtime` scenarios use actual `thread::sleep()` between frames and simulate the native viewer's 240Hz tick thread + settle countdown. These produce frame timings that match what users actually see, including settle-induced frame drops at their natural frequency. The `frameloop` and `frameloop_zoom` scenarios go through the actual `FrameLoop.poll()` / `complete()` path — the same code path as `Application::frame()`. All other pan/zoom scenarios bypass `FrameLoop` and call `queue_unstable()` directly, which means they never produce stable frames mid-interaction. The `frameloop` scenarios sweep event intervals from 16ms (fast flick) to 500ms (discrete clicks) and reveal how `FrameLoop`'s stable-frame decisions affect the frame time distribution at each speed. Use these when investigating panning or zooming jank, adaptive timing, or pan/zoom image cache behavior. **Choosing scenes:** Use `--list-scenes` to see what's available. Pick scenes that stress the subsystem you're optimizing. For effects/caching work, look for scenes with high promoted-node counts. For culling work, look for scenes with many visible nodes but few effects. ### 3. Node mutation benchmark (`grida_dev bench --translate`) Measures the **invalidation pipeline cost** — not camera motion. Every frame mutates the scene graph (translates a target node), then runs `apply_changes` + `queue_unstable` + `flush`. This is the bench that captures real drag-a-node behavior, identical to what `Application::frame()` + `EditorDocument::apply_and_mark` exercise in the demo. ```sh # List scenes, pick a target cargo run -p grida_dev --release -- bench <path.grida> --list-scenes # Translate the first suitable root node for 200 frames cargo run -p grida_dev --release -- bench <path.grida> --translate first --frames 200 # Translate a specific node by id cargo run -p grida_dev --release -- bench <path.grida> --translate 42 --frames 200 ``` Output includes a separate line for **`apply_changes` timing** — this is the cost of running the classifier + dirty-set dispatch + partial rebuild, isolated from the draw phase. Treat this as the authoritative number for "is invalidation slow?" ``` apply_changes: avg=3720us p50=3288us p95=30714us MAX=30714us total frame: avg=65344us p50=62159us fps=15.3 queue: 1.3ms draw: 10ms mid_flush: 49ms compositor: 1µs flush: 7µs settle: 83ms ```
View on GitHub
This SKILL.md is very large, so SkillsMP previews the first section here. View on GitHub