M13.14: performance section with before/after numbers; test tidy
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
9904525591
commit
97329edb39
|
|
@ -35,12 +35,14 @@ fn project_panel_scrolls_and_reveals_the_selection() {
|
|||
h.run_steps(3);
|
||||
// Far more rows than fit: a scroll bar, and the bottom rows aren't drawn yet.
|
||||
assert!(h.state().auto.find("project.vscroll").is_some());
|
||||
let last = h.state().session.project.items.values().filter(|i| i.parent.is_some()).map(|i| i.id.0).max().unwrap();
|
||||
let drawn = |h: &Harness<'_, EffectcraftApp>| h.state().auto.previous.iter().filter(|e| e.id.starts_with("project.item.") && e.id.matches('.').count() == 2).count();
|
||||
let drawn = |h: &Harness<'_, EffectcraftApp>| {
|
||||
h.state().auto.previous.iter().filter(|e| e.id.starts_with("project.item.") && e.id.matches('.').count() == 2).count()
|
||||
};
|
||||
let rows = drawn(&h);
|
||||
assert!(rows > 5 && rows < 80, "only the rows in view are drawn: {rows}");
|
||||
// Selecting an item (a command, an agent) scrolls it into view.
|
||||
let footage = h.state().session.project.items.values().filter(|i| matches!(i.kind, effectcraft_engine::project::ItemKind::Footage(_))).map(|i| i.id.0).max().unwrap();
|
||||
let footage =
|
||||
h.state().session.project.items.values().filter(|i| matches!(i.kind, effectcraft_engine::project::ItemKind::Footage(_))).map(|i| i.id.0).max().unwrap();
|
||||
h.state_mut().session.execute("project.select", json!({"items": [footage]})).unwrap();
|
||||
h.run_steps(3);
|
||||
assert!(h.state().auto.find(&format!("project.item.{footage}")).is_some(), "selected item scrolled into view");
|
||||
|
|
@ -50,7 +52,6 @@ fn project_panel_scrolls_and_reveals_the_selection() {
|
|||
h.run_steps(2);
|
||||
let shown: Vec<String> = h.state().auto.previous.iter().filter(|e| e.id.starts_with("project.item.")).map(|e| e.id.clone()).collect();
|
||||
assert!(!shown.is_empty() && drawn(&h) < 80, "{shown:?}");
|
||||
let _ = last;
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -63,7 +64,9 @@ fn comp_thumbnail_renders_off_the_ui_thread() {
|
|||
let mut landed = false;
|
||||
for _ in 0..400 {
|
||||
h.step();
|
||||
if h.state().auto.find("project.thumbnail").is_some() && h.ctx.data(|d| d.get_temp::<(u64, egui::TextureHandle)>(egui::Id::new(("proj-thumb", main.0)))).is_some() {
|
||||
if h.state().auto.find("project.thumbnail").is_some()
|
||||
&& h.ctx.data(|d| d.get_temp::<(u64, egui::TextureHandle)>(egui::Id::new(("proj-thumb", main.0)))).is_some()
|
||||
{
|
||||
landed = true;
|
||||
break;
|
||||
}
|
||||
|
|
@ -109,7 +112,8 @@ fn lazy_open_lists_the_footage_check_in_the_progress_panel() {
|
|||
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||
}
|
||||
assert!(h.state().session.tasks.is_empty());
|
||||
let missing = h.state().session.project.items.values().filter(|i| matches!(&i.kind, effectcraft_engine::project::ItemKind::Footage(f) if f.missing)).count();
|
||||
let missing =
|
||||
h.state().session.project.items.values().filter(|i| matches!(&i.kind, effectcraft_engine::project::ItemKind::Footage(f) if f.missing)).count();
|
||||
assert_eq!(missing, spec().footage);
|
||||
assert!(!h.state().session.is_dirty(), "the check doesn't modify the project");
|
||||
let _ = std::fs::remove_file(path);
|
||||
|
|
|
|||
|
|
@ -407,3 +407,58 @@ Web Worker, and the worker's replies (progress, item status, the analysed proper
|
|||
applied on the UI thread.
|
||||
The shortcut dispatcher and the menus read the active preset. See
|
||||
[preferences.md](preferences.md).
|
||||
|
||||
## 7. Performance of everyday operations
|
||||
|
||||
`effectcraft-cli bench --ops [--small] [--layers N] [--comps N] [--footage N]` measures the
|
||||
operations people do all day on a large generated project (`engine::perf::large_project`: 200
|
||||
comps, 5,000 layers in the main comp plus 10 in each other comp, 300 footage items — 100 image
|
||||
sequences of 240 frames, 100 movies, 100 stills — a 20-deep precomp chain, expressions on every
|
||||
4th layer, Gaussian Blur on every 6th, parenting). The engine side is `engine::perf::ops_bench`;
|
||||
the UI side (`ui-egui::bench`) drives the real `EffectcraftApp` headless: layout, painting and
|
||||
tessellation, no window or GPU upload.
|
||||
|
||||
What keeps it fast:
|
||||
|
||||
- **Lazy open.** A project file carries its footage metadata, so File ▸ Open is read + parse +
|
||||
swap. Nothing is decoded until a frame needs it, expressions compile on first evaluation
|
||||
(cached by text), and system fonts are scanned only when a text layer asks for a family that
|
||||
isn't bundled. The footage files are checked afterwards in the background (`footage.check`, a
|
||||
"Checking footage" job in the Progress panel): missing items are flagged, items saved without
|
||||
metadata are probed, and the project is not marked modified. The desktop app does this after
|
||||
every open (`Session::check_footage_on_open`); headless sessions run `footage.check {wait:true}`.
|
||||
- **No deep copies per frame.** Panels hold `Arc<Comp>` snapshots (`Session::active_comp_arc`,
|
||||
`Project::comp_arc`) instead of cloning a comp's layers every frame.
|
||||
- **Virtualised lists.** The Timeline and the Project panel draw (and register automation ids
|
||||
for) only the rows in view; the Project panel scrolls vertically and reveals an item selected
|
||||
elsewhere. Menus listing every layer (track matte, parent) are built only while open.
|
||||
- **Off the UI thread.** Project panel thumbnails render on a background thread (the previous
|
||||
thumbnail stays up meanwhile); auto-save snapshots the project (an `Arc` clone) and serialises
|
||||
and writes compact JSON on a background thread (`AutoSaveState::background`, on in the desktop
|
||||
app; inline on wasm32).
|
||||
- **Undo is a pointer swap.** History stores `Arc<Project>` snapshots and comps are `Arc`s, so
|
||||
undo and redo of any edit are constant time. An edit copies the comps it changes (a one-layer
|
||||
edit in a 5,000-layer comp copies that comp: ~8 ms).
|
||||
|
||||
Numbers (Apple Silicon, 14 cores, release build, best of 3–20 runs; the machine was shared with
|
||||
other builds, load average 150–220, so treat them as orders of magnitude). "Before" is the
|
||||
commit before M13.14 with the same benchmark code:
|
||||
|
||||
| Operation | Before | After |
|
||||
|---|---|---|
|
||||
| Startup to first full UI frame (open + app + 2 frames) | 2,119 ms | 432 ms (1,482 ms at load 220) |
|
||||
| File ▸ Open (125 MB pretty JSON; parse is ~95% of it) | 1,049 ms | 269–733 ms |
|
||||
| Save (pretty JSON, 125 MB) | 845 ms | 275–359 ms |
|
||||
| Auto-save, UI-thread cost | 530 ms (pretty, synchronous) | 0.1–0.2 ms (compact, background; 69–172 ms off-thread) |
|
||||
| Steady frame, Standard workspace | 862 ms | 0.5–6.9 ms |
|
||||
| Timeline frame scrolling 5,000 layers | 1,725 ms | 1.5–8.1 ms |
|
||||
| Timeline frame, 500 layers twirled open | 1,508 ms | 1.8–5.5 ms |
|
||||
| Project panel frame, 503 items, all folders open | 1.2 ms (first screenful only, no scrolling) | 0.4–2.8 ms (scrolling) |
|
||||
| Small edit (one property) / undo + redo | 14.7 ms / 0.01 ms | 8.4–19.7 ms / 0.01 ms |
|
||||
| Large edit (switch on 5,000 layers) / undo / redo | 62 / 0.03 / 0.01 ms | 18–57 / 0.02 / 0.01 ms |
|
||||
| First rendered frame of the 5,000-layer comp at ¼ (viewer thread) | 5.7 s | 2.7–3.6 s (unchanged code; load) |
|
||||
|
||||
The before frame times were dominated by an O(layers²) menu build per visible timeline row and
|
||||
by every panel deep-cloning the active comp each frame. Left for later: per-layer structural
|
||||
sharing (so a one-layer edit doesn't copy its comp), a binary or compact project format (pretty
|
||||
JSON parsing is now the bulk of open), and rendering 5,000-layer comps faster.
|
||||
|
|
|
|||
Loading…
Reference in New Issue