mirror of
https://github.com/lexmount/moli.git
synced 2026-10-16 08:01:27 +00:00
perf(dom): better inline script performance
This commit is contained in:
@@ -564,6 +564,46 @@ impl DomHost {
|
||||
self.set_text_content_effects(handle, value).did_change()
|
||||
}
|
||||
|
||||
/// Appends parser character tokens while retaining the text node's capacity.
|
||||
/// Old text is copied only when mutation records need its previous value.
|
||||
pub fn append_text_to_text_node_effects(
|
||||
&mut self,
|
||||
handle: DomHandle,
|
||||
value: &str,
|
||||
) -> DomMutationEffects {
|
||||
if value.is_empty() {
|
||||
return DomMutationEffects::default();
|
||||
}
|
||||
let records_enabled = self.mutation_records_enabled();
|
||||
let Some(text) = self
|
||||
.node_mut(handle)
|
||||
.and_then(|node| node.data_mut().as_text_mut())
|
||||
else {
|
||||
return DomMutationEffects::default();
|
||||
};
|
||||
let old_value = records_enabled.then(|| text.data().to_owned());
|
||||
text.append_data(value);
|
||||
self.text_data_mutation_effects(handle, records_enabled, old_value)
|
||||
}
|
||||
|
||||
fn text_data_mutation_effects(
|
||||
&mut self,
|
||||
handle: DomHandle,
|
||||
records_enabled: bool,
|
||||
old_value: Option<String>,
|
||||
) -> DomMutationEffects {
|
||||
self.record_mutation(MutationScope::QueryState);
|
||||
let mut effects = self.node_update_effects(handle);
|
||||
effects.mark_style_character_data_mutation(handle);
|
||||
if let Some(parent) = self.parent_node(handle) {
|
||||
self.mark_stylesheet_owner_contents_change_for_parent(&mut effects, parent);
|
||||
}
|
||||
if records_enabled {
|
||||
effects.mark_character_data_mutation(handle, old_value);
|
||||
}
|
||||
effects
|
||||
}
|
||||
|
||||
pub fn set_text_content_effects(
|
||||
&mut self,
|
||||
handle: DomHandle,
|
||||
@@ -617,16 +657,7 @@ impl DomHost {
|
||||
if !changed {
|
||||
return DomMutationEffects::default();
|
||||
}
|
||||
self.record_mutation(MutationScope::QueryState);
|
||||
let mut effects = self.node_update_effects(handle);
|
||||
effects.mark_style_character_data_mutation(handle);
|
||||
if let Some(parent) = self.parent_node(handle) {
|
||||
self.mark_stylesheet_owner_contents_change_for_parent(&mut effects, parent);
|
||||
}
|
||||
if records_enabled {
|
||||
effects.mark_character_data_mutation(handle, old_value);
|
||||
}
|
||||
effects
|
||||
self.text_data_mutation_effects(handle, records_enabled, old_value)
|
||||
}
|
||||
NodeType::Element | NodeType::DocumentFragment => {
|
||||
let children = self.child_handles(handle).collect::<Vec<_>>();
|
||||
|
||||
@@ -915,6 +915,74 @@ mod tests {
|
||||
assert!(effects.observer_records().records().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn appending_text_preserves_replacement_mutation_effects() {
|
||||
for parent_name in ["p", "style", "script"] {
|
||||
for recording in [false, true] {
|
||||
let mut host = DomHost::from_dom(NativeDom::new_html(test_url()));
|
||||
host.reset_html_document_shell();
|
||||
let body = host.document_body_handle().expect("document body");
|
||||
let parent = host.create_element(parent_name);
|
||||
let text = host.create_text_node("before🙂");
|
||||
assert!(host.append_child(body, parent));
|
||||
assert!(host.append_child(parent, text));
|
||||
host.set_mutation_observer_records_enabled(recording);
|
||||
let mut replacement = host.clone();
|
||||
|
||||
let effects = host.append_text_to_text_node_effects(text, "追加\n");
|
||||
let expected = replacement.set_text_content_effects(text, "before🙂追加\n");
|
||||
|
||||
assert_eq!(
|
||||
effects, expected,
|
||||
"parent={parent_name}, recording={recording}"
|
||||
);
|
||||
assert_eq!(host.text_content(text).as_deref(), Some("before🙂追加\n"));
|
||||
assert_eq!(host.query_version(), replacement.query_version());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn appending_text_keeps_existing_dom_snapshots_unchanged() {
|
||||
let mut host = DomHost::from_dom(NativeDom::new_html(test_url()));
|
||||
let text = host.create_text_node("before🙂");
|
||||
let snapshot = host.snapshot_document();
|
||||
|
||||
assert!(
|
||||
host.append_text_to_text_node_effects(text, "追加")
|
||||
.did_change()
|
||||
);
|
||||
|
||||
assert_eq!(host.text_content(text).as_deref(), Some("before🙂追加"));
|
||||
assert_eq!(snapshot.text_content(text).as_deref(), Some("before🙂"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn appending_empty_text_or_non_text_nodes_has_no_mutation_effects() {
|
||||
let mut host = DomHost::from_dom(NativeDom::new_html(test_url()));
|
||||
let text = host.create_text_node("original");
|
||||
let element = host.create_element("div");
|
||||
let document = host.document_handle();
|
||||
let cdata = host.create_cdata_section_for_document(document, "cdata");
|
||||
host.set_mutation_observer_records_enabled(true);
|
||||
let version = host.query_version();
|
||||
|
||||
assert_eq!(
|
||||
host.append_text_to_text_node_effects(text, ""),
|
||||
DomMutationEffects::default()
|
||||
);
|
||||
for handle in [element, cdata, NativeNodeId::new(99_999)] {
|
||||
assert_eq!(
|
||||
host.append_text_to_text_node_effects(handle, "ignored"),
|
||||
DomMutationEffects::default()
|
||||
);
|
||||
}
|
||||
assert_eq!(host.query_version(), version);
|
||||
assert_eq!(host.text_content(text).as_deref(), Some("original"));
|
||||
assert_eq!(host.text_content(element).as_deref(), Some(""));
|
||||
assert_eq!(host.text_content(cdata).as_deref(), Some("cdata"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert_before_self_reference_reports_remove_and_reinsert() {
|
||||
let mut host = DomHost::from_dom(NativeDom::new_html(test_url()));
|
||||
@@ -957,7 +1025,8 @@ mod tests {
|
||||
"DocumentType grew to {} bytes",
|
||||
size_of::<DocumentType>()
|
||||
);
|
||||
assert_eq!(size_of::<Text>(), 16);
|
||||
// Growing text fits within the existing NodeData/Node size limits below.
|
||||
assert_eq!(size_of::<Text>(), size_of::<String>());
|
||||
assert_eq!(size_of::<CDataSection>(), 16);
|
||||
assert_eq!(size_of::<Comment>(), 16);
|
||||
assert_eq!(size_of::<ProcessingInstruction>(), 32);
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Text {
|
||||
data: Box<str>,
|
||||
// Text shares the larger NodeData enum storage with elements, so retaining
|
||||
// String capacity does not enlarge a Node. Parser character tokens can then
|
||||
// append without reallocating and copying the full text for every token.
|
||||
data: String,
|
||||
}
|
||||
|
||||
impl Text {
|
||||
pub fn new(data: String) -> Self {
|
||||
Self {
|
||||
data: data.into_boxed_str(),
|
||||
}
|
||||
Self { data }
|
||||
}
|
||||
|
||||
pub fn data(&self) -> &str {
|
||||
@@ -15,7 +16,11 @@ impl Text {
|
||||
}
|
||||
|
||||
pub fn set_data(&mut self, data: impl Into<String>) {
|
||||
self.data = data.into().into_boxed_str();
|
||||
self.data = data.into();
|
||||
}
|
||||
|
||||
pub(crate) fn append_data(&mut self, data: &str) {
|
||||
self.data.push_str(data);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1637,12 +1637,7 @@ fn append_text_to_text_node_in_host(
|
||||
node_id: NativeNodeId,
|
||||
text: String,
|
||||
) -> DomMutationEffects {
|
||||
let Some(previous) = host.node(node_id).and_then(Node::as_text) else {
|
||||
return DomMutationEffects::default();
|
||||
};
|
||||
let mut merged = previous.data().to_owned();
|
||||
merged.push_str(&text);
|
||||
host.set_text_content_effects(node_id, &merged)
|
||||
host.append_text_to_text_node_effects(node_id, &text)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -1158,13 +1158,7 @@ mod tests {
|
||||
text: String,
|
||||
) -> DomMutationEffects {
|
||||
// SAFETY: the test keeps the DomHost alive for this parser pump step.
|
||||
let host = unsafe { &mut *self.host };
|
||||
let Some(previous) = host.node(node_id).and_then(Node::as_text) else {
|
||||
return DomMutationEffects::default();
|
||||
};
|
||||
let mut merged = previous.data().to_owned();
|
||||
merged.push_str(&text);
|
||||
host.set_text_content_effects(node_id, &merged)
|
||||
unsafe { &mut *self.host }.append_text_to_text_node_effects(node_id, &text)
|
||||
}
|
||||
|
||||
fn push_parse_error(&mut self, error: String) {
|
||||
@@ -1328,6 +1322,44 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parser_stream_preserves_large_script_text_across_input_chunks() {
|
||||
let stream = DocumentStream::new_scripting_enabled_parser_stream_for_testing(
|
||||
Url::parse("https://example.test/page.html").expect("test url"),
|
||||
);
|
||||
let block = "{\"text\":\"中文🙂 <node> & tail\"}\n".repeat(256);
|
||||
let source = block.repeat(128);
|
||||
assert!(source.len() > 1_000_000);
|
||||
|
||||
stream.feed("<script type='application/json'>");
|
||||
for _ in 0..128 {
|
||||
stream.feed(&block);
|
||||
}
|
||||
stream.feed("</script><p>after script</p>");
|
||||
|
||||
let document = stream.finish();
|
||||
let script = document.elements_by_tag_name(document.document_node_id(), "script", false)[0];
|
||||
assert_eq!(document.child_ids(script).count(), 1);
|
||||
assert_eq!(
|
||||
document.text_content(script).as_deref(),
|
||||
Some(source.as_str())
|
||||
);
|
||||
assert_eq!(
|
||||
document
|
||||
.node(script)
|
||||
.and_then(Node::as_element)
|
||||
.expect("script element")
|
||||
.script_text_internal_slot(),
|
||||
source
|
||||
);
|
||||
assert_eq!(
|
||||
document
|
||||
.elements_by_tag_name(document.document_node_id(), "p", false)
|
||||
.len(),
|
||||
1
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parser_stream_feed_continues_past_definitive_encoding_indicator() {
|
||||
let stream = DocumentStream::new_scripting_enabled_parser_stream_for_testing(
|
||||
|
||||
@@ -382,13 +382,8 @@ impl DocumentRuntime {
|
||||
node_id: DomHandle,
|
||||
text: String,
|
||||
) -> DomMutationEffects {
|
||||
let host = self.dom_host_mut_for_active_parser_step();
|
||||
let Some(previous) = host.node(node_id).and_then(Node::as_text) else {
|
||||
return DomMutationEffects::default();
|
||||
};
|
||||
let mut merged = previous.data().to_owned();
|
||||
merged.push_str(&text);
|
||||
host.set_text_content_effects(node_id, &merged)
|
||||
self.dom_host_mut_for_active_parser_step()
|
||||
.append_text_to_text_node_effects(node_id, &text)
|
||||
}
|
||||
|
||||
pub(crate) fn push_parse_error_in_live_dom_host(&mut self, error: String) {
|
||||
|
||||
Reference in New Issue
Block a user