Reduce render latency and improve headless input
This commit is contained in:
1 parent
b8bdac2163
commit
222b50e480
15 files changed
+2268
-147
No files matched your search
Vendored
+39
-1
@@ -7123,7 +7123,12 @@ globalThis.UIEvent = class extends Event {
|
||||
this.detail=detail||0;
|
||||
}
|
||||
};
|
||||
globalThis.WheelEvent = class extends Event { constructor(t,o={}) { super(t,o);this.deltaX=o.deltaX||0;this.deltaY=o.deltaY||0;this.deltaZ=o.deltaZ||0;this.deltaMode=o.deltaMode||0; } };
|
||||
// WheelEvent inherits all MouseEvent coordinates and modifier state. CDP
|
||||
// Input.dispatchMouseEvent supplies those fields and automation libraries use
|
||||
// them to distinguish wheel gestures over nested panes.
|
||||
globalThis.WheelEvent = class extends MouseEvent {
|
||||
constructor(t,o={}) { super(t,o);this.deltaX=o.deltaX||0;this.deltaY=o.deltaY||0;this.deltaZ=o.deltaZ||0;this.deltaMode=o.deltaMode||0; }
|
||||
};
|
||||
|
||||
globalThis.CompositionEvent = class extends Event {
|
||||
constructor(t,o={}) { super(t,o);this.view=o.view||null;this.detail=o.detail||0;this.data=o.data||""; }
|
||||
@@ -11632,6 +11637,39 @@ if (typeof Document !== 'undefined' && !Document.prototype.elementFromPoint) {
|
||||
var r = el.getBoundingClientRect();
|
||||
if (r.width === 0 || r.height === 0) continue;
|
||||
if (x >= r.left && x <= r.right && y >= r.top && y <= r.bottom) {
|
||||
// A descendant's layout rect can extend beyond an overflow clip. It
|
||||
// must not win hit testing where its scrolling ancestor hides it —
|
||||
// otherwise a wheel well outside a small pane scrolls that pane
|
||||
// instead of the page behind it.
|
||||
var visible = true;
|
||||
var ancestor = el.parentElement;
|
||||
while (ancestor && ancestor !== this.documentElement && ancestor !== this.body) {
|
||||
var style = null;
|
||||
try { style = getComputedStyle(ancestor); } catch (_e) {}
|
||||
var ox = style ? (style.overflowX || style.overflow || '') : '';
|
||||
var oy = style ? (style.overflowY || style.overflow || '') : '';
|
||||
var clipsX = ox === 'auto' || ox === 'scroll' || ox === 'hidden' || ox === 'clip';
|
||||
var clipsY = oy === 'auto' || oy === 'scroll' || oy === 'hidden' || oy === 'clip';
|
||||
if (clipsX || clipsY) {
|
||||
var ar = ancestor.getBoundingClientRect();
|
||||
// Overflow clips at the padding box, inside the border. Renderer
|
||||
// client metrics expose that box's size; computed border widths
|
||||
// locate it within the border-box rect.
|
||||
var borderLeft = parseFloat(style && style.borderLeftWidth) || 0;
|
||||
var borderTop = parseFloat(style && style.borderTopWidth) || 0;
|
||||
var clipLeft = ar.left + borderLeft;
|
||||
var clipTop = ar.top + borderTop;
|
||||
var clipRight = clipLeft + ancestor.clientWidth;
|
||||
var clipBottom = clipTop + ancestor.clientHeight;
|
||||
if ((clipsX && (x < clipLeft || x > clipRight)) ||
|
||||
(clipsY && (y < clipTop || y > clipBottom))) {
|
||||
visible = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
ancestor = ancestor.parentElement;
|
||||
}
|
||||
if (!visible) continue;
|
||||
var nid = el._nid | 0;
|
||||
if (nid > bestNid) { best = el; bestNid = nid; }
|
||||
}
|
||||
|
||||
+101
-10
@@ -129,6 +129,11 @@ pub struct ObscuraState {
|
||||
/// DOM/style/viewport changes clear this value but retain resource bytes.
|
||||
#[cfg(feature = "render")]
|
||||
pub prepared_render: Option<obscura_render::PreparedRender>,
|
||||
/// Connected attribute mutations awaiting dependency-indexed retained
|
||||
/// style refresh. Tree/text and non-attribute invalidations still discard
|
||||
/// the prepared render immediately.
|
||||
#[cfg(feature = "render")]
|
||||
pub pending_attribute_style_mutations: Vec<obscura_render::AttributeStyleMutation>,
|
||||
/// Page-lifetime raw image/font bytes. A new document resets this cache;
|
||||
/// relayout of the same document reuses it without refetching.
|
||||
#[cfg(feature = "render")]
|
||||
@@ -197,6 +202,8 @@ impl ObscuraState {
|
||||
#[cfg(feature = "render")]
|
||||
prepared_render: None,
|
||||
#[cfg(feature = "render")]
|
||||
pending_attribute_style_mutations: Vec::new(),
|
||||
#[cfg(feature = "render")]
|
||||
render_resources: obscura_render::RenderResourceCache::default(),
|
||||
#[cfg(feature = "render")]
|
||||
stylesheet_cache: obscura_render::StylesheetCache::default(),
|
||||
@@ -465,6 +472,48 @@ fn render_mutation_impact(
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "render")]
|
||||
fn retained_attribute_style_mutation(
|
||||
dom: &DomTree,
|
||||
cmd: &str,
|
||||
arg1: &str,
|
||||
arg2: &str,
|
||||
) -> Option<obscura_render::AttributeStyleMutation> {
|
||||
let node = NodeId::new(arg1.parse::<u32>().ok()?);
|
||||
let can_retain = |name: &str| {
|
||||
let name = name.to_ascii_lowercase();
|
||||
name == "class"
|
||||
|| name == "id"
|
||||
|| name.starts_with("data-")
|
||||
|| name.starts_with("aria-")
|
||||
};
|
||||
match cmd {
|
||||
"set_attribute" => {
|
||||
let (name, value) = arg2.split_once('\0')?;
|
||||
if !can_retain(name) {
|
||||
return None;
|
||||
}
|
||||
Some(obscura_render::AttributeStyleMutation {
|
||||
node,
|
||||
name: name.to_string(),
|
||||
old_value: dom
|
||||
.with_node(node, |node| node.get_attribute(name).map(str::to_owned))
|
||||
.flatten(),
|
||||
new_value: Some(value.to_string()),
|
||||
})
|
||||
}
|
||||
"remove_attribute" => can_retain(arg2).then(|| obscura_render::AttributeStyleMutation {
|
||||
node,
|
||||
name: arg2.to_string(),
|
||||
old_value: dom
|
||||
.with_node(node, |node| node.get_attribute(arg2).map(str::to_owned))
|
||||
.flatten(),
|
||||
new_value: None,
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "render")]
|
||||
fn render_timing_enabled() -> bool {
|
||||
static ENABLED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
|
||||
@@ -605,6 +654,11 @@ fn op_dom_inner(state: &OpState, cmd: String, arg1: String, arg2: String) -> Str
|
||||
.as_ref()
|
||||
.map(|dom| render_mutation_impact(dom, &cmd, &arg1, &arg2))
|
||||
.unwrap_or_default();
|
||||
#[cfg(feature = "render")]
|
||||
let retained_attribute_mutation = state
|
||||
.dom
|
||||
.as_ref()
|
||||
.and_then(|dom| retained_attribute_style_mutation(dom, &cmd, &arg1, &arg2));
|
||||
let invalidate = impact.connected && impact.actual_change;
|
||||
if invalidate {
|
||||
state.activity_generation = state.activity_generation.wrapping_add(1);
|
||||
@@ -620,7 +674,19 @@ fn op_dom_inner(state: &OpState, cmd: String, arg1: String, arg2: String) -> Str
|
||||
let had_prepared_render = state.prepared_render.is_some();
|
||||
#[cfg(feature = "render")]
|
||||
if invalidate {
|
||||
state.prepared_render = None;
|
||||
if let Some(mutation) = retained_attribute_mutation {
|
||||
if state.prepared_render.is_some()
|
||||
&& state.pending_attribute_style_mutations.len() < 256
|
||||
{
|
||||
state.pending_attribute_style_mutations.push(mutation);
|
||||
} else {
|
||||
state.prepared_render = None;
|
||||
state.pending_attribute_style_mutations.clear();
|
||||
}
|
||||
} else {
|
||||
state.prepared_render = None;
|
||||
state.pending_attribute_style_mutations.clear();
|
||||
}
|
||||
state.resolved_scroll = None;
|
||||
}
|
||||
#[cfg(feature = "render")]
|
||||
@@ -2958,6 +3024,7 @@ fn op_set_dynamic_fonts(state: &OpState, #[string] registrations: &str) -> bool
|
||||
if state.dynamic_fonts != fonts {
|
||||
state.dynamic_fonts = fonts;
|
||||
state.prepared_render = None;
|
||||
state.pending_attribute_style_mutations.clear();
|
||||
state.resolved_scroll = None;
|
||||
}
|
||||
true
|
||||
@@ -3041,17 +3108,41 @@ pub(crate) fn ensure_prepared_render(
|
||||
let stale = state.prepared_render.as_ref().map_or(true, |prepared| {
|
||||
prepared.viewport() != viewport || prepared.base_url() != base_url.as_deref()
|
||||
});
|
||||
if stale {
|
||||
if stale || !state.pending_attribute_style_mutations.is_empty() {
|
||||
let previous = (!stale).then(|| state.prepared_render.take()).flatten();
|
||||
let mutations = std::mem::take(&mut state.pending_attribute_style_mutations);
|
||||
let prepared = {
|
||||
let dom = state.dom.as_ref()?;
|
||||
obscura_render::prepare_dom_with_dynamic_fonts_and_stylesheet_cache(
|
||||
dom,
|
||||
viewport,
|
||||
base_url.as_deref(),
|
||||
&mut state.render_resources,
|
||||
&state.dynamic_fonts,
|
||||
&mut state.stylesheet_cache,
|
||||
)?
|
||||
match previous {
|
||||
Some(previous) => obscura_render::prepare_dom_with_retained_attribute_styles(
|
||||
dom,
|
||||
viewport,
|
||||
base_url.as_deref(),
|
||||
&mut state.render_resources,
|
||||
&state.dynamic_fonts,
|
||||
&mut state.stylesheet_cache,
|
||||
previous,
|
||||
&mutations,
|
||||
)
|
||||
.or_else(|| {
|
||||
obscura_render::prepare_dom_with_dynamic_fonts_and_stylesheet_cache(
|
||||
dom,
|
||||
viewport,
|
||||
base_url.as_deref(),
|
||||
&mut state.render_resources,
|
||||
&state.dynamic_fonts,
|
||||
&mut state.stylesheet_cache,
|
||||
)
|
||||
})?,
|
||||
None => obscura_render::prepare_dom_with_dynamic_fonts_and_stylesheet_cache(
|
||||
dom,
|
||||
viewport,
|
||||
base_url.as_deref(),
|
||||
&mut state.render_resources,
|
||||
&state.dynamic_fonts,
|
||||
&mut state.stylesheet_cache,
|
||||
)?,
|
||||
}
|
||||
};
|
||||
state.prepared_render = Some(prepared);
|
||||
state.resolved_scroll = None;
|
||||
|
||||
@@ -31,6 +31,24 @@ static SNAPSHOT: &[u8] = include_bytes!(env!("OBSCURA_SNAPSHOT_PATH"));
|
||||
/// parallel, each isolate on its own thread with no shared lock.
|
||||
static ISOLATE_CREATE_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
|
||||
|
||||
#[cfg(feature = "render")]
|
||||
fn with_sync_render_loading_disabled<R>(
|
||||
state: &mut ObscuraState,
|
||||
capture: impl FnOnce(&mut ObscuraState) -> R,
|
||||
) -> R {
|
||||
let previous = state
|
||||
.render_resources
|
||||
.set_sync_loading_enabled(false);
|
||||
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| capture(state)));
|
||||
state
|
||||
.render_resources
|
||||
.set_sync_loading_enabled(previous);
|
||||
match result {
|
||||
Ok(value) => value,
|
||||
Err(payload) => std::panic::resume_unwind(payload),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RemoteObjectInfo {
|
||||
pub js_type: String,
|
||||
@@ -246,6 +264,7 @@ impl ObscuraJsRuntime {
|
||||
#[cfg(feature = "render")]
|
||||
{
|
||||
gs.prepared_render = None;
|
||||
gs.pending_attribute_style_mutations.clear();
|
||||
gs.render_resources = obscura_render::RenderResourceCache::default();
|
||||
gs.stylesheet_cache = obscura_render::StylesheetCache::default();
|
||||
gs.dynamic_fonts.clear();
|
||||
@@ -266,6 +285,7 @@ impl ObscuraJsRuntime {
|
||||
// present. Keep already-fetched absolute bytes, but rebuild
|
||||
// candidate selection/layout against the new base.
|
||||
state.prepared_render = None;
|
||||
state.pending_attribute_style_mutations.clear();
|
||||
state.resolved_scroll = None;
|
||||
}
|
||||
}
|
||||
@@ -376,6 +396,7 @@ impl ObscuraJsRuntime {
|
||||
if state.viewport != viewport {
|
||||
state.viewport = viewport;
|
||||
state.prepared_render = None;
|
||||
state.pending_attribute_style_mutations.clear();
|
||||
state.resolved_scroll = None;
|
||||
}
|
||||
}
|
||||
@@ -418,21 +439,23 @@ impl ObscuraJsRuntime {
|
||||
if viewport != state.viewport || base_url != effective_base.as_deref() {
|
||||
return None;
|
||||
}
|
||||
ensure_resolved_scroll(&mut state)?;
|
||||
let ObscuraState {
|
||||
dom,
|
||||
prepared_render,
|
||||
render_resources,
|
||||
resolved_scroll,
|
||||
..
|
||||
} = &mut *state;
|
||||
let (_, scroll) = resolved_scroll.as_ref()?;
|
||||
obscura_render::screenshot_prepared_with_scroll(
|
||||
dom.as_ref()?,
|
||||
prepared_render.as_mut()?,
|
||||
render_resources,
|
||||
scroll,
|
||||
)
|
||||
with_sync_render_loading_disabled(&mut state, |state| {
|
||||
ensure_resolved_scroll(state)?;
|
||||
let ObscuraState {
|
||||
dom,
|
||||
prepared_render,
|
||||
render_resources,
|
||||
resolved_scroll,
|
||||
..
|
||||
} = state;
|
||||
let (_, scroll) = resolved_scroll.as_ref()?;
|
||||
obscura_render::screenshot_prepared_with_scroll(
|
||||
dom.as_ref()?,
|
||||
prepared_render.as_mut()?,
|
||||
render_resources,
|
||||
scroll,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// Capture a document-space rectangle without changing the live viewport,
|
||||
@@ -444,27 +467,29 @@ impl ObscuraJsRuntime {
|
||||
region: obscura_render::CaptureRegion,
|
||||
) -> Result<Vec<u8>, obscura_render::CaptureError> {
|
||||
let mut state = self.state.borrow_mut();
|
||||
ensure_resolved_scroll(&mut state).ok_or(obscura_render::CaptureError::PaintFailed)?;
|
||||
let ObscuraState {
|
||||
dom,
|
||||
prepared_render,
|
||||
render_resources,
|
||||
resolved_scroll,
|
||||
..
|
||||
} = &mut *state;
|
||||
let (_, scroll) = resolved_scroll
|
||||
.as_ref()
|
||||
.ok_or(obscura_render::CaptureError::PaintFailed)?;
|
||||
obscura_render::screenshot_prepared_region_with_scroll(
|
||||
dom.as_ref()
|
||||
.ok_or(obscura_render::CaptureError::PaintFailed)?,
|
||||
prepared_render
|
||||
.as_mut()
|
||||
.ok_or(obscura_render::CaptureError::PaintFailed)?,
|
||||
render_resources,
|
||||
scroll,
|
||||
region,
|
||||
)
|
||||
with_sync_render_loading_disabled(&mut state, |state| {
|
||||
ensure_resolved_scroll(state).ok_or(obscura_render::CaptureError::PaintFailed)?;
|
||||
let ObscuraState {
|
||||
dom,
|
||||
prepared_render,
|
||||
render_resources,
|
||||
resolved_scroll,
|
||||
..
|
||||
} = state;
|
||||
let (_, scroll) = resolved_scroll
|
||||
.as_ref()
|
||||
.ok_or(obscura_render::CaptureError::PaintFailed)?;
|
||||
obscura_render::screenshot_prepared_region_with_scroll(
|
||||
dom.as_ref()
|
||||
.ok_or(obscura_render::CaptureError::PaintFailed)?,
|
||||
prepared_render
|
||||
.as_mut()
|
||||
.ok_or(obscura_render::CaptureError::PaintFailed)?,
|
||||
render_resources,
|
||||
scroll,
|
||||
region,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// Return the retained layout's scrollable document size without changing
|
||||
@@ -473,11 +498,13 @@ impl ObscuraJsRuntime {
|
||||
#[cfg(feature = "render")]
|
||||
pub fn prepared_content_size(&self) -> Option<(f32, f32)> {
|
||||
let mut state = self.state.borrow_mut();
|
||||
ensure_resolved_scroll(&mut state)?;
|
||||
state
|
||||
.prepared_render
|
||||
.as_ref()
|
||||
.map(|render| render.content_size())
|
||||
with_sync_render_loading_disabled(&mut state, |state| {
|
||||
ensure_resolved_scroll(state)?;
|
||||
state
|
||||
.prepared_render
|
||||
.as_ref()
|
||||
.map(|render| render.content_size())
|
||||
})
|
||||
}
|
||||
|
||||
/// Return the exact responsive candidates selected for live `<img>`
|
||||
@@ -530,6 +557,7 @@ impl ObscuraJsRuntime {
|
||||
Some(bytes) => {
|
||||
state.render_resources.seed(url, bytes);
|
||||
state.prepared_render = None;
|
||||
state.pending_attribute_style_mutations.clear();
|
||||
state.resolved_scroll = None;
|
||||
}
|
||||
None => state.render_resources.seed_missing(url),
|
||||
@@ -1548,6 +1576,7 @@ impl ObscuraJsRuntime {
|
||||
#[cfg(feature = "render")]
|
||||
{
|
||||
state.prepared_render = None;
|
||||
state.pending_attribute_style_mutations.clear();
|
||||
state.render_resources = obscura_render::RenderResourceCache::default();
|
||||
state.stylesheet_cache = obscura_render::StylesheetCache::default();
|
||||
state.dynamic_fonts.clear();
|
||||
@@ -6157,7 +6186,11 @@ mod tests {
|
||||
// must observe the new class on the live connected element.
|
||||
rt.evaluate("document.getElementById('box').className = 'b'")
|
||||
.unwrap();
|
||||
assert!(rt.state.borrow().prepared_render.is_none());
|
||||
{
|
||||
let state = rt.state.borrow();
|
||||
assert!(state.prepared_render.is_some());
|
||||
assert_eq!(state.pending_attribute_style_mutations.len(), 1);
|
||||
}
|
||||
assert_eq!(
|
||||
rt.evaluate("document.getElementById('box').getBoundingClientRect().width")
|
||||
.unwrap()
|
||||
|
||||
Reference in new issue
Block a user