engine_observables_api/quiescence.rs
1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5//! Quiescence: the report a surface makes of its pending work, and the default
6//! policy for "settled".
7//!
8//! Every automation harness that polls-and-sleeps does so because it cannot ask
9//! the engine what is still in flight. The engine can answer per source, and the
10//! per-source signals already exist — this module adds no mechanism, only the
11//! common vocabulary the host uses to join them:
12//!
13//! - loads: `LoadingState` (this crate) — terminal at `Done` / `Failed`
14//! - script microtasks: `script-engine-api`'s `pump` (`Quiescent` / `Pending`)
15//! - timers: the runtime's `next_timer_delay()`
16//! - animation-frame callbacks: the runtime's `has_animation_frame_callbacks()`
17//! - declared animations: layout's `has_active_animations()`
18//!
19//! Only the host sees all of these at once, so the host assembles a
20//! [`PendingWork`] per surface; consumers (a test harness, a WebDriver adapter,
21//! an agent's observation) read it instead of sleeping.
22//!
23//! ## Settling vs perpetual sources
24//!
25//! The default [`settled`](PendingWork::settled) policy counts only work that
26//! *finishes by itself*: loads, microtasks, and dirty layout. The other sources
27//! are reported but do not block, each for a stated reason:
28//!
29//! - **Timers.** A page with `setTimeout(fn, 30_000)` is not "busy" for those
30//! thirty seconds. WebDriver's own document-readiness never waits on timers,
31//! and a harness that did would turn every long-poll page into a hang.
32//! - **Animation-frame callbacks.** A one-shot rAF (schedule a measurement)
33//! settles next frame, but a rAF *loop* — every game loop, every physics
34//! surface — re-requests forever, and the two are statically
35//! indistinguishable. Blocking on rAF turns "the orrery is breathing" into
36//! "the harness never returns".
37//! - **Declared animations.** CSS transitions/animations run on the engine's
38//! clock and may be infinite (`animation-iteration-count: infinite`).
39//!
40//! For those, the tool is a condition-wait ("element exists", "attribute
41//! equals"), not a broader settle. A caller with cause to block on the
42//! perpetual sources opts in explicitly via [`fully_idle`](PendingWork::fully_idle)
43//! and owns the hang risk it is accepting.
44
45use malloc_size_of_derive::MallocSizeOf;
46use serde::{Deserialize, Serialize};
47
48/// One surface's pending work, by source, at the moment of asking.
49///
50/// A snapshot, not a subscription: settling is level-triggered (ask again), so a
51/// harness loop is "apply step, ask until settled, assert" with no sleep in it.
52#[derive(Clone, Copy, Debug, Default, Deserialize, MallocSizeOf, PartialEq, Serialize)]
53pub struct PendingWork {
54 /// In-flight loads (document, subresources) on this surface.
55 pub loads: usize,
56 /// The script engine's job queue is non-empty (`pump` would report
57 /// `Pending`). `false` where the surface runs no script.
58 pub microtasks: bool,
59 /// Layout or paint work is scheduled but not yet performed.
60 pub layout_dirty: bool,
61 /// Delay to the next scheduled timer, in ms, if any timer is scheduled.
62 /// Reported, never blocking (see module docs).
63 pub next_timer_ms: Option<f64>,
64 /// Animation-frame callbacks are requested for the next frame. Reported,
65 /// never blocking: a rAF loop is indistinguishable from a one-shot.
66 pub animation_frames: bool,
67 /// Declared CSS transitions/animations are running. Reported, never
68 /// blocking: they may be infinite.
69 pub animations: bool,
70}
71
72impl PendingWork {
73 /// The default settle policy: every source that finishes by itself is done.
74 ///
75 /// This is the harness's "the step landed" signal — loads complete, script
76 /// quiescent, layout clean. It deliberately ignores timers, rAF and declared
77 /// animations; see the module docs for why each would turn a live surface
78 /// into a hang.
79 pub fn settled(&self) -> bool {
80 self.loads == 0 && !self.microtasks && !self.layout_dirty
81 }
82
83 /// Everything idle, the perpetual-capable sources included. Meaningful on a
84 /// genuinely static surface; on anything animated this may simply never be
85 /// true, which is the caller's risk to accept.
86 pub fn fully_idle(&self) -> bool {
87 self.settled()
88 && self.next_timer_ms.is_none()
89 && !self.animation_frames
90 && !self.animations
91 }
92}
93
94/// Common-minimum quiescence query, per surface. Implemented host-side (only the
95/// host sees loads, script, and layout at once); read by harnesses, protocol
96/// adapters, and agent observation assembly.
97pub trait QuiescenceQuery {
98 /// The surface's pending work right now.
99 fn pending_work(&mut self) -> PendingWork;
100}
101
102#[cfg(test)]
103mod tests {
104 use super::*;
105
106 #[test]
107 fn settled_ignores_the_perpetual_sources() {
108 // The orrery shape: nothing finishing, everything breathing.
109 let breathing = PendingWork {
110 next_timer_ms: Some(400.0),
111 animation_frames: true,
112 animations: true,
113 ..PendingWork::default()
114 };
115 assert!(
116 breathing.settled(),
117 "a surface that is only animating is settled, or every game loop hangs the harness"
118 );
119 assert!(!breathing.fully_idle());
120 }
121
122 #[test]
123 fn settled_blocks_on_work_that_finishes() {
124 for pending in [
125 PendingWork {
126 loads: 1,
127 ..PendingWork::default()
128 },
129 PendingWork {
130 microtasks: true,
131 ..PendingWork::default()
132 },
133 PendingWork {
134 layout_dirty: true,
135 ..PendingWork::default()
136 },
137 ] {
138 assert!(!pending.settled(), "{pending:?} must block settle");
139 }
140 assert!(PendingWork::default().settled());
141 assert!(PendingWork::default().fully_idle());
142 }
143}