diff --git a/crates/ui-egui/src/panels/layers/tests.rs b/crates/ui-egui/src/panels/layers/tests.rs index 212778957..4b47cbd3f 100644 --- a/crates/ui-egui/src/panels/layers/tests.rs +++ b/crates/ui-egui/src/panels/layers/tests.rs @@ -272,12 +272,44 @@ fn closing_a_document_releases_only_its_layer_thumbnails() { }); } +/// From now on each reading of this thread's render budget clock is `ms` later than the one before. +/// A render reads it before and after, so each row thumbnail rendered takes `ms` of the budget. +fn clock_steps(ms: f64) { + let mut now = 0.0; + THUMBS.with(|c| { + c.borrow_mut().clock = Some(Box::new(move || { + now += ms; + now + })) + }); +} + +/// Rendering on the UI thread stops for the pass once the thumbnails rendered in it took the +/// budget, and the rest render in the next passes. +#[test] +fn row_thumbnails_past_the_render_budget_wait_for_the_next_pass() { + let (mut app, _, _) = two_rects(); + let ctx = egui::Context::default(); + let renders = || THUMBS.with(|c| c.borrow().renders); + // Each render takes 5 ms: after two renders the pass has used 10 ms, past the 8 ms budget. + clock_steps(5.0); + let mut per_pass = vec![]; + for _ in 0..4 { + let before = renders(); + frame(&mut app, &ctx, vec![], false); + per_pass.push(renders() - before); + } + assert_eq!(per_pass, [2, 1, 0, 0], "the layer's and two objects' thumbnails, two in the first pass"); +} + #[test] fn row_thumbnails_wait_for_the_end_of_a_drag() { let (mut app, _, _) = two_rects(); app.session.execute("select.all", &json!({})).unwrap(); let ctx = egui::Context::default(); let renders = || THUMBS.with(|c| c.borrow().renders); + // Renders take no time, so every row's thumbnail fits in a pass however busy the machine is. + clock_steps(0.0); frame(&mut app, &ctx, vec![], false); let first = renders(); assert!(first >= 1, "the rows' thumbnails render at once"); diff --git a/crates/ui-egui/src/panels/layers/thumbs.rs b/crates/ui-egui/src/panels/layers/thumbs.rs index dacf77425..861e89427 100644 --- a/crates/ui-egui/src/panels/layers/thumbs.rs +++ b/crates/ui-egui/src/panels/layers/thumbs.rs @@ -65,6 +65,10 @@ pub(super) struct Thumbs { spent: (u64, f64, u32), /// Thumbnails rendered on the UI thread (tests count them). pub(super) renders: u64, + /// A test's clock (ms) for the render budget, replacing [`crate::now_ms`] so a test decides how + /// long each render takes (production always leaves this `None`). + #[cfg(test)] + pub(super) clock: Option f64>>, } thread_local! { @@ -258,6 +262,15 @@ impl Thumbs { self.entries.keys().any(|key| key.0 == uid) } + /// The time (ms) a render on the UI thread is measured by: [`crate::now_ms`], or a test's clock. + fn now_ms(&mut self) -> f64 { + #[cfg(test)] + if let Some(clock) = self.clock.as_mut() { + return clock(); + } + crate::now_ms() + } + /// Release thumbnails and queued jobs for documents that have closed, keeping the cache for /// other open documents even when the panels are hidden or another document is active. When a /// document closes, the worker drops its jobs, the one running included, and the images it @@ -416,9 +429,9 @@ impl Thumbs { thread_local! { static RENDERER: std::cell::RefCell = std::cell::RefCell::new(vectorcraft_render::Renderer::new()); } - let t0 = crate::now_ms(); + let t0 = self.now_ms(); let img = RENDERER.with(|r| render_thumb(&mut r.borrow_mut(), src.doc, n, px)); - let ms = crate::now_ms() - t0; + let ms = self.now_ms() - t0; self.spent.1 += ms; self.spent.2 += 1; self.renders += 1;