Speed up retained rendering and honor PDF backgrounds
This commit is contained in:
1 parent
222b50e480
commit
ede14b7740
9 files changed
+1385
-92
No files matched your search
@@ -129,11 +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.
|
||||
/// Connected mutations awaiting dependency-indexed retained style refresh.
|
||||
/// Tree changes carry stable node/parent ids so a later geometry read can
|
||||
/// coalesce framework DOM churn into one conservative local cascade.
|
||||
#[cfg(feature = "render")]
|
||||
pub pending_attribute_style_mutations: Vec<obscura_render::AttributeStyleMutation>,
|
||||
pub pending_style_mutations: Vec<obscura_render::RetainedStyleMutation>,
|
||||
/// Page-lifetime raw image/font bytes. A new document resets this cache;
|
||||
/// relayout of the same document reuses it without refetching.
|
||||
#[cfg(feature = "render")]
|
||||
@@ -202,7 +202,7 @@ impl ObscuraState {
|
||||
#[cfg(feature = "render")]
|
||||
prepared_render: None,
|
||||
#[cfg(feature = "render")]
|
||||
pending_attribute_style_mutations: Vec::new(),
|
||||
pending_style_mutations: Vec::new(),
|
||||
#[cfg(feature = "render")]
|
||||
render_resources: obscura_render::RenderResourceCache::default(),
|
||||
#[cfg(feature = "render")]
|
||||
@@ -473,17 +473,18 @@ fn render_mutation_impact(
|
||||
}
|
||||
|
||||
#[cfg(feature = "render")]
|
||||
fn retained_attribute_style_mutation(
|
||||
fn retained_style_mutation(
|
||||
dom: &DomTree,
|
||||
cmd: &str,
|
||||
arg1: &str,
|
||||
arg2: &str,
|
||||
) -> Option<obscura_render::AttributeStyleMutation> {
|
||||
) -> Option<obscura_render::RetainedStyleMutation> {
|
||||
let node = NodeId::new(arg1.parse::<u32>().ok()?);
|
||||
let can_retain = |name: &str| {
|
||||
let name = name.to_ascii_lowercase();
|
||||
name == "class"
|
||||
|| name == "id"
|
||||
|| name == "style"
|
||||
|| name.starts_with("data-")
|
||||
|| name.starts_with("aria-")
|
||||
};
|
||||
@@ -493,23 +494,78 @@ fn retained_attribute_style_mutation(
|
||||
if !can_retain(name) {
|
||||
return None;
|
||||
}
|
||||
let keeps_selector_value = !name.eq_ignore_ascii_case("style");
|
||||
Some(obscura_render::AttributeStyleMutation {
|
||||
node,
|
||||
name: name.to_string(),
|
||||
old_value: dom
|
||||
.with_node(node, |node| node.get_attribute(name).map(str::to_owned))
|
||||
old_value: keeps_selector_value
|
||||
.then(|| {
|
||||
dom.with_node(node, |node| {
|
||||
node.get_attribute(name).map(str::to_owned)
|
||||
})
|
||||
.flatten()
|
||||
})
|
||||
.flatten(),
|
||||
new_value: Some(value.to_string()),
|
||||
})
|
||||
new_value: keeps_selector_value.then(|| value.to_string()),
|
||||
}
|
||||
.into())
|
||||
}
|
||||
"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,
|
||||
"remove_attribute" => can_retain(arg2).then(|| {
|
||||
let keeps_selector_value = !arg2.eq_ignore_ascii_case("style");
|
||||
obscura_render::AttributeStyleMutation {
|
||||
node,
|
||||
name: arg2.to_string(),
|
||||
old_value: keeps_selector_value
|
||||
.then(|| {
|
||||
dom.with_node(node, |node| {
|
||||
node.get_attribute(arg2).map(str::to_owned)
|
||||
})
|
||||
.flatten()
|
||||
})
|
||||
.flatten(),
|
||||
new_value: None,
|
||||
}
|
||||
.into()
|
||||
}),
|
||||
"append_child" => {
|
||||
let child = NodeId::new(arg2.parse::<u32>().ok()?);
|
||||
dom.get_node(node)?;
|
||||
let old_parent = dom.get_node(child)?.parent;
|
||||
Some(
|
||||
obscura_render::TreeStyleMutation::Insert {
|
||||
node: child,
|
||||
old_parent,
|
||||
new_parent: node,
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
}
|
||||
"remove_child" => {
|
||||
let old_parent = dom.get_node(node)?.parent?;
|
||||
Some(
|
||||
obscura_render::TreeStyleMutation::Remove { node, old_parent }.into(),
|
||||
)
|
||||
}
|
||||
"insert_before" => {
|
||||
let reference = NodeId::new(arg2.parse::<u32>().ok()?);
|
||||
let new_parent = dom.get_node(reference)?.parent?;
|
||||
let old_parent = dom.get_node(node)?.parent;
|
||||
Some(
|
||||
obscura_render::TreeStyleMutation::Insert {
|
||||
node,
|
||||
old_parent,
|
||||
new_parent,
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
}
|
||||
"set_text_content" => Some(
|
||||
obscura_render::TreeStyleMutation::Text {
|
||||
node,
|
||||
parent: dom.get_node(node)?.parent,
|
||||
}
|
||||
.into(),
|
||||
),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -655,10 +711,10 @@ fn op_dom_inner(state: &OpState, cmd: String, arg1: String, arg2: String) -> Str
|
||||
.map(|dom| render_mutation_impact(dom, &cmd, &arg1, &arg2))
|
||||
.unwrap_or_default();
|
||||
#[cfg(feature = "render")]
|
||||
let retained_attribute_mutation = state
|
||||
let retained_style_mutation = state
|
||||
.dom
|
||||
.as_ref()
|
||||
.and_then(|dom| retained_attribute_style_mutation(dom, &cmd, &arg1, &arg2));
|
||||
.and_then(|dom| retained_style_mutation(dom, &cmd, &arg1, &arg2));
|
||||
let invalidate = impact.connected && impact.actual_change;
|
||||
if invalidate {
|
||||
state.activity_generation = state.activity_generation.wrapping_add(1);
|
||||
@@ -674,18 +730,18 @@ 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 {
|
||||
if let Some(mutation) = retained_attribute_mutation {
|
||||
if let Some(mutation) = retained_style_mutation {
|
||||
if state.prepared_render.is_some()
|
||||
&& state.pending_attribute_style_mutations.len() < 256
|
||||
&& state.pending_style_mutations.len() < 256
|
||||
{
|
||||
state.pending_attribute_style_mutations.push(mutation);
|
||||
state.pending_style_mutations.push(mutation);
|
||||
} else {
|
||||
state.prepared_render = None;
|
||||
state.pending_attribute_style_mutations.clear();
|
||||
state.pending_style_mutations.clear();
|
||||
}
|
||||
} else {
|
||||
state.prepared_render = None;
|
||||
state.pending_attribute_style_mutations.clear();
|
||||
state.pending_style_mutations.clear();
|
||||
}
|
||||
state.resolved_scroll = None;
|
||||
}
|
||||
@@ -3024,7 +3080,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.pending_style_mutations.clear();
|
||||
state.resolved_scroll = None;
|
||||
}
|
||||
true
|
||||
@@ -3108,13 +3164,13 @@ 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 || !state.pending_attribute_style_mutations.is_empty() {
|
||||
if stale || !state.pending_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 mutations = std::mem::take(&mut state.pending_style_mutations);
|
||||
let prepared = {
|
||||
let dom = state.dom.as_ref()?;
|
||||
match previous {
|
||||
Some(previous) => obscura_render::prepare_dom_with_retained_attribute_styles(
|
||||
Some(previous) => obscura_render::prepare_dom_with_retained_styles(
|
||||
dom,
|
||||
viewport,
|
||||
base_url.as_deref(),
|
||||
|
||||
@@ -264,7 +264,7 @@ impl ObscuraJsRuntime {
|
||||
#[cfg(feature = "render")]
|
||||
{
|
||||
gs.prepared_render = None;
|
||||
gs.pending_attribute_style_mutations.clear();
|
||||
gs.pending_style_mutations.clear();
|
||||
gs.render_resources = obscura_render::RenderResourceCache::default();
|
||||
gs.stylesheet_cache = obscura_render::StylesheetCache::default();
|
||||
gs.dynamic_fonts.clear();
|
||||
@@ -285,7 +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.pending_style_mutations.clear();
|
||||
state.resolved_scroll = None;
|
||||
}
|
||||
}
|
||||
@@ -396,7 +396,7 @@ impl ObscuraJsRuntime {
|
||||
if state.viewport != viewport {
|
||||
state.viewport = viewport;
|
||||
state.prepared_render = None;
|
||||
state.pending_attribute_style_mutations.clear();
|
||||
state.pending_style_mutations.clear();
|
||||
state.resolved_scroll = None;
|
||||
}
|
||||
}
|
||||
@@ -465,6 +465,17 @@ impl ObscuraJsRuntime {
|
||||
pub fn screenshot_prepared_region(
|
||||
&self,
|
||||
region: obscura_render::CaptureRegion,
|
||||
) -> Result<Vec<u8>, obscura_render::CaptureError> {
|
||||
self.screenshot_prepared_region_with_backgrounds(region, true)
|
||||
}
|
||||
|
||||
/// Capture a document-space rectangle with the PDF print-background
|
||||
/// policy without mutating the page DOM or retained geometry.
|
||||
#[cfg(feature = "render")]
|
||||
pub fn screenshot_prepared_region_with_backgrounds(
|
||||
&self,
|
||||
region: obscura_render::CaptureRegion,
|
||||
paint_backgrounds: bool,
|
||||
) -> Result<Vec<u8>, obscura_render::CaptureError> {
|
||||
let mut state = self.state.borrow_mut();
|
||||
with_sync_render_loading_disabled(&mut state, |state| {
|
||||
@@ -479,7 +490,7 @@ impl ObscuraJsRuntime {
|
||||
let (_, scroll) = resolved_scroll
|
||||
.as_ref()
|
||||
.ok_or(obscura_render::CaptureError::PaintFailed)?;
|
||||
obscura_render::screenshot_prepared_region_with_scroll(
|
||||
obscura_render::screenshot_prepared_region_with_scroll_and_backgrounds(
|
||||
dom.as_ref()
|
||||
.ok_or(obscura_render::CaptureError::PaintFailed)?,
|
||||
prepared_render
|
||||
@@ -488,6 +499,7 @@ impl ObscuraJsRuntime {
|
||||
render_resources,
|
||||
scroll,
|
||||
region,
|
||||
paint_backgrounds,
|
||||
)
|
||||
})
|
||||
}
|
||||
@@ -557,7 +569,7 @@ impl ObscuraJsRuntime {
|
||||
Some(bytes) => {
|
||||
state.render_resources.seed(url, bytes);
|
||||
state.prepared_render = None;
|
||||
state.pending_attribute_style_mutations.clear();
|
||||
state.pending_style_mutations.clear();
|
||||
state.resolved_scroll = None;
|
||||
}
|
||||
None => state.render_resources.seed_missing(url),
|
||||
@@ -1576,7 +1588,7 @@ impl ObscuraJsRuntime {
|
||||
#[cfg(feature = "render")]
|
||||
{
|
||||
state.prepared_render = None;
|
||||
state.pending_attribute_style_mutations.clear();
|
||||
state.pending_style_mutations.clear();
|
||||
state.render_resources = obscura_render::RenderResourceCache::default();
|
||||
state.stylesheet_cache = obscura_render::StylesheetCache::default();
|
||||
state.dynamic_fonts.clear();
|
||||
@@ -6141,10 +6153,19 @@ mod tests {
|
||||
"document.getElementById('box').setAttribute('style', 'height:60px;width:40px')",
|
||||
)
|
||||
.unwrap();
|
||||
assert!(
|
||||
rt.state.borrow().prepared_render.is_none(),
|
||||
"a changed attribute on a connected element must invalidate layout"
|
||||
);
|
||||
{
|
||||
let state = rt.state.borrow();
|
||||
assert!(
|
||||
state.prepared_render.is_some(),
|
||||
"a retained inline-style change must keep the prior style maps until flush"
|
||||
);
|
||||
assert!(matches!(
|
||||
state.pending_style_mutations.as_slice(),
|
||||
[obscura_render::RetainedStyleMutation::Attribute(
|
||||
obscura_render::AttributeStyleMutation { name, .. }
|
||||
)] if name == "style"
|
||||
));
|
||||
}
|
||||
assert_eq!(
|
||||
rt.evaluate("document.getElementById('box').getBoundingClientRect().height")
|
||||
.unwrap()
|
||||
@@ -6189,7 +6210,7 @@ mod tests {
|
||||
{
|
||||
let state = rt.state.borrow();
|
||||
assert!(state.prepared_render.is_some());
|
||||
assert_eq!(state.pending_attribute_style_mutations.len(), 1);
|
||||
assert_eq!(state.pending_style_mutations.len(), 1);
|
||||
}
|
||||
assert_eq!(
|
||||
rt.evaluate("document.getElementById('box').getBoundingClientRect().width")
|
||||
@@ -6235,6 +6256,98 @@ mod tests {
|
||||
assert_eq!(state.stylesheet_cache.hit_count(), 1);
|
||||
}
|
||||
|
||||
#[cfg(feature = "render")]
|
||||
#[test]
|
||||
fn connected_tree_mutations_queue_retained_styles_until_geometry_flush() {
|
||||
let dom = parse_html(
|
||||
r#"<html style="margin:0"><head><style>
|
||||
.item{display:block;width:40px;height:12px}
|
||||
.item:nth-child(2){width:80px}
|
||||
</style></head><body style="margin:0">
|
||||
<main id="list"><div id="first" class="item"></div></main>
|
||||
</body></html>"#,
|
||||
);
|
||||
let mut rt = ObscuraJsRuntime::new();
|
||||
rt.set_dom(dom);
|
||||
rt.set_viewport(200.0, 100.0);
|
||||
rt.run_page_init();
|
||||
|
||||
assert_eq!(
|
||||
rt.evaluate("document.getElementById('first').getBoundingClientRect().width")
|
||||
.unwrap()
|
||||
.as_f64(),
|
||||
Some(40.0)
|
||||
);
|
||||
rt.evaluate(
|
||||
"const added=document.createElement('div');added.id='added';added.className='item';document.getElementById('list').appendChild(added)",
|
||||
)
|
||||
.unwrap();
|
||||
{
|
||||
let state = rt.state.borrow();
|
||||
assert!(state.prepared_render.is_some());
|
||||
assert!(matches!(
|
||||
state.pending_style_mutations.as_slice(),
|
||||
[obscura_render::RetainedStyleMutation::Tree(
|
||||
obscura_render::TreeStyleMutation::Insert { .. }
|
||||
)]
|
||||
));
|
||||
}
|
||||
assert_eq!(
|
||||
rt.evaluate("document.getElementById('added').getBoundingClientRect().width")
|
||||
.unwrap()
|
||||
.as_f64(),
|
||||
Some(80.0)
|
||||
);
|
||||
assert!(rt.state.borrow().pending_style_mutations.is_empty());
|
||||
|
||||
rt.evaluate("document.getElementById('added').style.width='65px'")
|
||||
.unwrap();
|
||||
{
|
||||
let state = rt.state.borrow();
|
||||
assert!(state.prepared_render.is_some());
|
||||
assert!(matches!(
|
||||
state.pending_style_mutations.as_slice(),
|
||||
[obscura_render::RetainedStyleMutation::Attribute(
|
||||
obscura_render::AttributeStyleMutation { name, .. }
|
||||
)] if name == "style"
|
||||
));
|
||||
}
|
||||
assert_eq!(
|
||||
rt.evaluate("document.getElementById('added').getBoundingClientRect().width")
|
||||
.unwrap()
|
||||
.as_f64(),
|
||||
Some(65.0)
|
||||
);
|
||||
assert_eq!(
|
||||
rt.evaluate("(function(){const added=document.getElementById('added');added.style.width='';return added.getBoundingClientRect().width})()")
|
||||
.unwrap()
|
||||
.as_f64(),
|
||||
Some(80.0)
|
||||
);
|
||||
|
||||
rt.evaluate(
|
||||
"document.getElementById('list').removeChild(document.getElementById('first'))",
|
||||
)
|
||||
.unwrap();
|
||||
{
|
||||
let state = rt.state.borrow();
|
||||
assert!(state.prepared_render.is_some());
|
||||
assert!(matches!(
|
||||
state.pending_style_mutations.as_slice(),
|
||||
[obscura_render::RetainedStyleMutation::Tree(
|
||||
obscura_render::TreeStyleMutation::Remove { .. }
|
||||
)]
|
||||
));
|
||||
}
|
||||
assert_eq!(
|
||||
rt.evaluate("document.getElementById('added').getBoundingClientRect().width")
|
||||
.unwrap()
|
||||
.as_f64(),
|
||||
Some(40.0)
|
||||
);
|
||||
assert!(rt.state.borrow().pending_style_mutations.is_empty());
|
||||
}
|
||||
|
||||
#[cfg(feature = "render")]
|
||||
#[test]
|
||||
fn root_overflow_clip_preserves_cssom_scroll_range() {
|
||||
|
||||
Reference in new issue
Block a user