M8: the browser's wait after a preview never exceeds the render it may save

With a fixed 120 ms wait, a page whose full renders take 15–100 ms was held coarse for 120 ms
after a key zoom or a page switch, where before it was refined in the next frame. The wait is
now the last full render's time when that is shorter than `SETTLE_MS`: a wait never costs more
than the render it may save, and on a heavy page (seconds a render) it is 120 ms as before.

Also from review: the test that the wait counts from the preview no longer times a preview
render (a margin of a few milliseconds under load); a sleep between the request and the poll
stands in for the render, with the whole settle as margin. `Inline::in_flight` says that a
preview's wait counts.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
samhyunandClaude Fable 5.1 committed 2026-10-28 22:53:07 +09:00
1 parent 8c951810ab
commit 3773a60801
3 files changed
+62 -17

No files matched your search

+27 -8
View File
@@ -13,7 +13,9 @@
//! the render from scratch has run past [`PREVIEW_AFTER_MS`], beside it (the full render keeps the
//! other cores), and drops it if the full image is there first.
//! The browser renders on the UI thread, so a zoom's full render blocks the page: there a preview
//! is refined only once no newer request has come for [`SETTLE_MS`] (the zoom has paused).
//! is refined only once no newer request has come for [`SETTLE_MS`] (the zoom has paused), or for
//! as long as the last full render took when that is shorter (a wait never costs more than the
//! render it may save).
use std::sync::Arc;
@@ -27,9 +29,9 @@ use web_time::Instant;
pub const PREVIEW_FACTOR: f64 = 4.0;
/// Full renders quicker than this (about a frame) show without a preview.
pub const PREVIEW_AFTER_MS: f64 = 12.0;
/// The browser refines a zoom's preview only once no newer request came for this long: a full
/// render there blocks the page, and one between every two wheel steps of a zoom held it up for
/// seconds.
/// The browser refines a zoom's preview only once no newer request came for this long (or for as
/// long as the last full render took, when shorter): a full render there blocks the page, and one
/// between every two wheel steps of a zoom held it up for seconds.
pub const SETTLE_MS: u64 = 120;
#[derive(Clone)]
@@ -354,7 +356,11 @@ pub struct Inline {
/// rendering the preview itself can take longer than the wait (the page is blocked meanwhile,
/// so no newer request could have come).
since: Instant,
/// How long to wait after a preview, at most: `wait` is this capped by the last full render's
/// time, so a page whose renders are quick is not held coarse for longer than one would take.
settle: std::time::Duration,
/// The wait for the pending job's preview.
wait: std::time::Duration,
renders: Renders,
}
@@ -367,7 +373,17 @@ impl Inline {
}
/// Refining a preview after `settle` without newer requests (tests: zero, the next frame).
pub fn with_settle(ctx: egui::Context, settle: std::time::Duration) -> Self {
Self { ctx, pending: None, submitted: 0, previewed: false, preview_shown: false, since: Instant::now(), settle, renders: Renders::default() }
Self {
ctx,
pending: None,
submitted: 0,
previewed: false,
preview_shown: false,
since: Instant::now(),
settle,
wait: settle,
renders: Renders::default(),
}
}
pub fn submit(&mut self, job: Job) {
self.pending = Some(job);
@@ -375,6 +391,7 @@ impl Inline {
self.previewed = false;
self.preview_shown = false;
}
/// Whether a request is still to be refined (a preview's wait counts).
pub fn in_flight(&self) -> bool {
self.pending.is_some()
}
@@ -384,14 +401,16 @@ impl Inline {
if let Some(d) = self.pending.as_ref().and_then(|job| self.renders.preview(job)) {
self.preview_shown = true;
self.since = Instant::now();
self.ctx.request_repaint_after(self.settle);
let full = std::time::Duration::try_from_secs_f64(self.renders.full_ms / 1000.0);
self.wait = full.map_or(self.settle, |full| full.min(self.settle));
self.ctx.request_repaint_after(self.wait);
return Some(answering(d, self.submitted));
}
}
if self.pending.is_some() && self.preview_shown {
let waited = self.since.elapsed();
if waited < self.settle {
self.ctx.request_repaint_after(self.settle - waited);
if waited < self.wait {
self.ctx.request_repaint_after(self.wait - waited);
return None;
}
}
@@ -443,21 +443,23 @@ fn the_browser_worker_refines_a_zoom_only_once_it_settles() {
/// The wait counts from the preview being shown, not from the request: rendering the preview can
/// take longer than the wait (the page is blocked meanwhile, so no newer request could come), and
/// counted from the request the refinement would follow in the very next frame.
/// counted from the request the refinement would follow in the very next frame. The time the
/// preview's render would take is stood in for by a sleep between the request and the poll.
#[test]
fn the_wait_counts_from_the_preview_not_from_the_request() {
let view = |scale| view_at((300.0, 200.0), scale, 1200, 1200);
let s = slow_scene(&[view(2.0), view(1.0)], 20.0 * PREVIEW_AFTER_MS);
// Renders from scratch of at least the settle, so the wait is the whole settle (see the cap).
let s = slow_scene(&[view(2.0), view(1.0)], SETTLE_MS as f64);
let ds = s.ds().unwrap();
let at = |scale| job(&ds.doc, ds.page, ds.uid, view(scale));
let preview_ms = preview_with(&at(1.0), &mut Retained::default()).unwrap().ms;
let settle = std::time::Duration::from_secs_f64(preview_ms / 2000.0).max(std::time::Duration::from_millis(1));
let settle = std::time::Duration::from_millis(SETTLE_MS);
let mut w = Inline::with_settle(egui::Context::default(), settle);
w.submit(at(2.0));
assert!(w.poll().is_some_and(|d| !d.coarse));
w.submit(at(1.0));
assert!(w.poll().is_some_and(|d| d.coarse), "the preview, which takes {preview_ms} ms");
assert!(w.poll().is_none() && w.in_flight(), "the wait ({settle:?}) starts only now");
std::thread::sleep(settle + std::time::Duration::from_millis(30));
assert!(w.poll().is_some_and(|d| d.coarse), "the preview, longer after the request than the wait");
assert!(w.poll().is_none() && w.in_flight(), "the wait starts only now");
std::thread::sleep(settle);
assert!(w.poll().is_some_and(|d| !d.coarse && d.view.scale == 1.0));
}
@@ -491,3 +493,26 @@ fn same_zoom_requests_before_the_zoom_refines_keep_waiting_and_refine_at_once_af
let d = w.poll().expect("the refined image");
assert!(!d.coarse && !w.in_flight());
}
/// The wait never costs more than the full render it may save: a preview is refined once the
/// last full render's time has passed when that is shorter than `SETTLE_MS`, so a key zoom or a
/// page switch on a page whose renders are quick is not held coarse for longer than before.
#[test]
fn the_wait_is_capped_by_the_time_of_the_last_full_render() {
let view = |scale| view_at((300.0, 200.0), scale, 1200, 1200);
let s = slow_scene(&[view(2.0), view(1.0)], 3.0 * PREVIEW_AFTER_MS);
let ds = s.ds().unwrap();
let at = |scale| job(&ds.doc, ds.page, ds.uid, view(scale));
// A settle far longer than this page's renders: the wait is their time instead.
let settle = std::time::Duration::from_secs(10);
let mut w = Inline::with_settle(egui::Context::default(), settle);
w.submit(at(2.0));
let first = w.poll().expect("the first image");
assert!(!first.coarse && first.ms >= PREVIEW_AFTER_MS, "slow enough to preview: {} ms", first.ms);
w.submit(at(1.0));
assert!(w.poll().is_some_and(|d| d.coarse));
assert!(w.poll().is_none() && w.in_flight(), "not refined in the very next frame");
std::thread::sleep(std::time::Duration::from_secs_f64(first.ms / 1000.0));
let d = w.poll().expect("refined once the last full render's time has passed, not the settle");
assert!(!d.coarse && d.view.scale == 1.0 && !w.in_flight());
}
+4 -3
View File
@@ -248,9 +248,10 @@ No alpha blockers: the alpha gate in [`roadmap.md`](roadmap.md#alpha-gate) passe
page, a zoom after quick renders), a watchdog thread previews any such render still running
after 12 ms (desktop only; the browser renders inline).
- **Landed (2026-10-28):** the browser refines a zoom's preview only once no newer request has
come for 120 ms (`SETTLE_MS`): its full renders run on the UI thread, and one between every two
wheel steps held the page up for seconds. Pans and edits (no preview) are refined at once; the
desktop, with its render thread, is unchanged.
come for 120 ms (`SETTLE_MS`), or for as long as its last full render took when that is shorter:
its full renders run on the UI thread, and one between every two wheel steps held the page up
for seconds. Pans and edits (no preview) are refined at once; the desktop, with its render
thread, is unchanged.
- **Landed (2026-10-15):** a bounded render margin is reused during panning and refilled before
the visible view reaches its edge. The extra rendered area is capped at 1.6× visible pixels;
this reduces blank strips without changing the CPU renderer.