Warm renderer resources before parity sampling
This commit is contained in:
1 parent
4d459c2125
commit
65ed38a71d
4 files changed
+95
-11
No files matched your search
@@ -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");
|
||||
}
|
||||
@@ -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
|
||||
),
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in new issue
Block a user