Tree builder and RcDom take time growing with the square of the input on four simple inputs
Maintainers usually reply within 1 day
Nobody has claimed this yet.
Assessment
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Newbie friendliness
- 45/100
- Issue type
- Bug
- Clarity
- Mostly clear
- Activity status
- Active
- Tech stack
- rust
- Domain
- performance
Research direction
Start by running the provided cargo run --release reproduction, then inspect tree_builder/rules.rs and RcDom's add_attrs_if_missing implementation. Compare timings for each input as its size doubles, and investigate the reported formatting, nesting, and table-text cases. Done means the identified parsing workloads no longer show roughly quadratic time growth, with the reproduction confirming the improvement.
Written by the indexing model from the issue text.
Description
Hi,
These four inputs make parse_document with markup5ever_rcdom take time that roughly quadruples each time the input doubles. None of them crashes. A program parsing untrusted HTML can limit the input size, so I'm filing this as a performance report.
Measured with html5ever 0.39.0 and markup5ever_rcdom 0.39.0+unofficial (the newest pair that build together; html5ever 0.40.1 needs a newer rcdom), release build, Apple M-series:
| Input | Size | Time |
|---|---|---|
1,000 <body> start tags, each with 64 new attribute names |
489 KB | 1.35 s |
| 2,000 of them | 1.0 MB | 5.05 s |
<p><b x{i}>t</p> repeated 4,000 times (i counts up) |
65 KB | 1.02 s |
| repeated 8,000 times | 131 KB | 4.12 s |
40,000 nested <div> |
195 KB | 1.69 s |
80,000 nested <div> |
390 KB | 9.00 s |
<table>x</table> repeated 40,000 times |
625 KB | 0.44 s |
| repeated 80,000 times | 1.25 MB | 1.73 s |
Where the time seems to go, from reading the code (I have not profiled every case):
- Repeated
<body>(and<html>) tags. The tree builder callsTreeSink::add_attrs_if_missingfor each repeated start tag (tree_builder/rules.rs). RcDom's implementation builds aHashSetof every attribute the element already has, on every call, so each new tag pays again for all the attributes the earlier ones added. - Misnested formatting tags. Each
<p>closes over an open<b>, so the<b>is reconstructed in the next paragraph. With distinct attributes, none of the entries in the list of active formatting elements match, and the list and the reconstruction work grow with the number already seen. - Deep nesting. Each new element appears to cost time in the depth of the stack of open elements.
- Text directly inside tables. The text is foster-parented, and RcDom appears to scan the parent's children to place it, so each new piece costs time in the number already placed.
The attribute-name check in the tokenizer is already covered by #796 and #804, so it isn't repeated here.
Reproduction (cargo run --release):
[dependencies]
html5ever = "0.39"
markup5ever_rcdom = "=0.39.0+unofficial"
use std::time::Instant;
use html5ever::tendril::TendrilSink;
use html5ever::{parse_document, ParseOpts};
use markup5ever_rcdom::RcDom;
fn time(label: &str, html: String) {
let start = Instant::now();
let _dom = parse_document(RcDom::default(), ParseOpts::default())
.one(html.as_str());
println!("{label}: {:.2?} ({} KB)", start.elapsed(), html.len() / 1024);
}
fn main() {
for n in [500, 1_000, 2_000] {
let html = (0..n)
.map(|i| {
let attrs: String =
(0..64).map(|j| format!(" b{i}_{j}")).collect();
format!("<body{attrs}>")
})
.collect();
time(&format!("{n} <body> tags x 64 new attributes"), html);
}
for n in [2_000, 4_000, 8_000] {
let html = (0..n).map(|i| format!("<p><b x{i}>t</p>")).collect();
time(&format!("{n} x <p><b x{{i}}>t</p>"), html);
}
for n in [20_000, 40_000, 80_000] {
time(&format!("{n} nested <div>"), "<div>".repeat(n));
}
for n in [20_000, 40_000, 80_000] {
time(&format!("{n} x <table>x</table>"), "<table>x</table>".repeat(n));
}
}
Happy to test patches.
- Dominant language
- Rust
- Stars
- 2.7k
- Forks
- 303
- Avg merge
- 17h 29m
- Merged PRs (30d)
- 4
Getting set up
This project ships no dev container, Dockerfile or contributing guide, so setting up is up to you: start from its README, and see our first-contribution guide for the general steps.
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from servo/html5ever
-
Difficulty 1/5 Under an hour Newbie friendliness 78/100
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 70/100
Maintainers usually reply within 1 day
-
rcdom: selectedcontent lookup reads self.data, option clone never runsPossibly taken @jdm claimed this 79 days ago. Open
Difficulty 1/5 Under an hour Newbie friendliness 90/100
servo/html5ever#776 · 1 comment ·
Maintainers usually reply within 1 day
-
Difficulty 3/5 1-2 days Newbie friendliness 68/100
Maintainers usually reply within 1 day
-
Difficulty 3/5 1-2 days Newbie friendliness 62/100
Maintainers usually reply within 1 day
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 65/100
cloudflare/workers-rs#1130 ·
Maintainers usually reply within 11 days
-
Difficulty 2/5 1-3 hours Newbie friendliness 69/100
vfarcic/dot-agent-deck#2160 · 2 comments ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
RustCrypto/utils#1597 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
max-sixty/worktrunk#4727 · 7 comments · 1 reaction ·
Maintainers usually reply within 1 day