Warm renderer resources before parity sampling

This commit is contained in:
SGavrl committed 2026-07-31 11:11:26 +00:00
1 parent 4d459c2125
commit 65ed38a71d
4 files changed
+95 -11

No files matched your search

+25 -4
View File
@@ -781,6 +781,25 @@ async fn run_fetch(
if let Some(ref path) = screenshot {
#[cfg(feature = "render")]
{
// Default CSS-pixel viewport, matching the engine's innerWidth/Height.
// OBSCURA_SHOT_W / OBSCURA_SHOT_H override it (e.g. a tall viewport to
// capture below-the-fold content in one shot).
let viewport = screenshot_viewport.unwrap_or((1280.0, 720.0));
// The parity harness performs one throwaway paint in both engines
// before observing image/font readiness. Obscura resolves retained
// render resources during prepare/paint, so sampling first would
// compare pre-paint Obscura state with post-load Chromium state.
// Keep this private opt-in out of ordinary CLI screenshots.
let warmup_capture = std::env::var("OBSCURA_SHOT_RESOURCE_WARMUP")
.is_ok_and(|value| value == "1");
if warmup_capture {
if page.screenshot(viewport).is_none() {
anyhow::bail!("resource warm-up screenshot failed: page has no DOM to render");
}
// Give completion callbacks one bounded task turn before the
// capture-boundary evaluation reads resource state.
page.settle(1).await;
}
// Paired renderer captures need a stable final coordinate after
// the post-eval settle. Authored smooth scrolling and scroll
// anchoring may legitimately move an earlier scrollTo while the
@@ -827,10 +846,6 @@ async fn run_fetch(
}))()",
)
});
// Default CSS-pixel viewport, matching the engine's innerWidth/Height.
// OBSCURA_SHOT_W / OBSCURA_SHOT_H override it (e.g. a tall viewport to
// capture below-the-fold content in one shot).
let viewport = screenshot_viewport.unwrap_or((1280.0, 720.0));
match page.screenshot(viewport) {
Some(bytes) => std::fs::write(path, &bytes)?,
None => anyhow::bail!("screenshot failed: page has no DOM to render"),
@@ -865,6 +880,12 @@ async fn run_fetch(
serde_json::json!({
"evaluation": result,
"controlledScroll": controlled_scroll_report,
"resourceWarmup": {
"performed": warmup_capture,
"discardedShots": if warmup_capture { 1 } else { 0 },
"taskTurnMs": if warmup_capture { 1 } else { 0 },
"phase": "before-final-scroll-reassert-and-state-sample",
},
"captureState": capture_state.unwrap_or(serde_json::Value::Null),
})
);
@@ -155,6 +155,7 @@ fn paired_capture_evaluates_state_after_final_scroll_reassert() {
.env("OBSCURA_SHOT_SCROLL_X", "0")
.env("OBSCURA_SHOT_SCROLL_Y", "80")
.env("OBSCURA_SHOT_EVAL_AT_CAPTURE", "1")
.env("OBSCURA_SHOT_RESOURCE_WARMUP", "1")
.output()
.expect("run obscura fetch");
assert!(
@@ -188,6 +189,15 @@ fn paired_capture_evaluates_state_after_final_scroll_reassert() {
report["controlledScroll"]["phase"].as_str(),
Some("immediately-before-capture-state-and-screenshot")
);
assert_eq!(report["resourceWarmup"]["performed"].as_bool(), Some(true));
assert_eq!(
report["resourceWarmup"]["discardedShots"].as_u64(),
Some(1)
);
assert_eq!(
report["resourceWarmup"]["phase"].as_str(),
Some("before-final-scroll-reassert-and-state-sample")
);
std::fs::remove_dir_all(directory).expect("remove temporary output directory");
}
+39 -7
View File
@@ -64,6 +64,7 @@ BRAND_PERMUTATIONS = [
]
GEOMETRY_PROBE_RECT_LIMIT = 200
CAPTURE_BOUNDARY_PHASE = "capture-boundary-before-screenshot"
RESOURCE_WARMUP_PHASE = "before-final-scroll-reassert-and-state-sample"
def geometry_probe_javascript(selectors_expression):
@@ -360,6 +361,8 @@ def parse_obscura_capture_report(stdout):
state["geometry"] = geometry
if isinstance(report.get("controlledScroll"), dict):
state["controlled_scroll_capture"] = report["controlledScroll"]
if isinstance(report.get("resourceWarmup"), dict):
state["resource_warmup"] = report["resourceWarmup"]
state["evaluation_sampled_phase"] = state.get("sampled_phase")
state["screenshot_sampled_phase"] = CAPTURE_BOUNDARY_PHASE
state["state_and_screenshot_share_capture_boundary"] = (
@@ -434,6 +437,10 @@ def obscura_environment(width, height):
# all settle phases and the final scroll reassertion. No event-loop
# pumping occurs between that evaluation and screenshot paint.
OBSCURA_SHOT_EVAL_AT_CAPTURE="1",
# Resolve the exact retained image/font graph with one throwaway paint
# before resource-readiness state is sampled. Chromium performs the
# same warm-up shot in the paired process.
OBSCURA_SHOT_RESOURCE_WARMUP="1",
)
return env
@@ -952,6 +959,19 @@ def capture_chromium_image(page, screenshot, geometry_selectors=None):
return state, boundary
def warm_chromium_capture(page):
"""Resolve paint-time resources before capture-boundary state sampling."""
page.screenshot(full_page=False, timeout=50000)
# Yield one bounded browser task turn, matching the Obscura CLI warm-up.
page.wait_for_timeout(1)
return {
"performed": True,
"discardedShots": 1,
"taskTurnMs": 1,
"phase": RESOURCE_WARMUP_PHASE,
}
def freeze_chromium_animations(page, sample_ms):
"""Pause currently exposed Web Animations at one explicit local time.
@@ -1278,17 +1298,21 @@ def main():
},
"state_observability": {
"capture_boundary_phase": CAPTURE_BOUNDARY_PHASE,
"resource_warmup_phase": RESOURCE_WARMUP_PHASE,
"chromium": (
"same page, synchronously sampled after all settle phases and "
"same page, one discard paint resolves the retained resource "
"graph, then one bounded task turn and the final scroll "
"reassert run before synchronous state sampling. State is "
"immediately before screenshot. A second synchronous sample "
"after paint brackets the PNG and reports whether capture-"
"critical geometry/readiness remained stable. DOM SHA-256 "
"finalization runs on the host only after paint"
),
"obscura": (
"the paired-capture CLI defers DOM/resource/selector evaluation "
"until after all settle phases and the final instant scroll "
"reassert. Evaluation, captureState, and screenshot then run "
"the paired-capture CLI takes one discard paint over the same "
"live page, pumps one bounded task turn, then performs the "
"final instant scroll reassert before deferred DOM/resource/"
"selector evaluation. Evaluation, captureState, and screenshot run "
"consecutively without another settle; captureState records the "
"exact shared PreparedRender viewport, scroll offset, and "
"content size used by paint"
@@ -1318,9 +1342,12 @@ def main():
"serialize fetched stylesheet text into outerHTML."
),
"resource_readiness": (
"Obscura's DOM image/font readiness is sampled before paint; "
"paint may then fetch retained renderer resources that are not "
"reflected back into HTMLImageElement or FontFaceSet state."
"Both engines take and discard one screenshot before sampling "
"resource readiness, then yield one bounded task turn. This "
"warms the exact retained render graph instead of comparing "
"Obscura's pre-paint state with Chromium's post-load state. "
"A successful fetch is still not proof of equal decode, selected "
"source, or paint output."
),
"animation_sampling": (
"The deterministic option controls only animations still "
@@ -1452,6 +1479,7 @@ def main():
chrome_started = time.time()
chromium_scroll_state = None
chromium_capture_boundary = None
chromium_resource_warmup = None
try:
page.goto(url, wait_until="load", timeout=50000)
page.wait_for_timeout(args.settle_ms)
@@ -1472,6 +1500,7 @@ def main():
animation_sampling = freeze_chromium_animations(
page, args.animation_time_ms
)
chromium_resource_warmup = warm_chromium_capture(page)
if controlled_scroll is not None:
chromium_scroll_state = (
reassert_chromium_controlled_scroll(
@@ -1538,6 +1567,9 @@ def main():
"capture_boundary_validation": (
chromium_capture_boundary if chrome_ok else None
),
"resource_warmup": (
chromium_resource_warmup if chrome_ok else None
),
"scroll_state": (
chromium_scroll_state if chrome_ok else None
),
+21
View File
@@ -118,6 +118,7 @@ class ControlledScrollTests(unittest.TestCase):
self.assertEqual(environment["OBSCURA_SHOT_SCROLL_X"], "12")
self.assertEqual(environment["OBSCURA_SHOT_SCROLL_Y"], "bottom")
self.assertEqual(environment["OBSCURA_SHOT_EVAL_AT_CAPTURE"], "1")
self.assertEqual(environment["OBSCURA_SHOT_RESOURCE_WARMUP"], "1")
def test_paired_state_expression_is_read_only_at_capture_boundary(self):
expression = paired_corpus.obscura_state_eval_expression(
@@ -326,6 +327,26 @@ class GeometryProbeTests(unittest.TestCase):
hashlib.sha256(b"hello").hexdigest(),
)
def test_chromium_resource_warmup_discards_one_shot_then_yields(self):
events = []
class WarmupPage:
def screenshot(self, **kwargs):
events.append(("screenshot", kwargs))
return b"discarded"
def wait_for_timeout(self, timeout):
events.append(("wait", timeout))
report = paired_corpus.warm_chromium_capture(WarmupPage())
self.assertEqual([event[0] for event in events], ["screenshot", "wait"])
self.assertEqual(events[1], ("wait", 1))
self.assertNotIn("path", events[0][1])
self.assertEqual(report["discardedShots"], 1)
self.assertEqual(
report["phase"], paired_corpus.RESOURCE_WARMUP_PHASE
)
def test_repeatable_selectors_are_passed_safely_in_one_state_expression(self):
selectors = ["header nav a", '[data-label="a\\\"b"]', "["]
obscura_expression = paired_corpus.obscura_state_eval_expression(