Skip to content

Keep the xpath attribute selector's results per scope and current with the DOM - #56

Open
michaelthyregod wants to merge 2 commits into
AngleSharp:masterfrom
michaelthyregod:fix/xpath-selector-results
Open

michaelthyregod wants to merge 2 commits into
AngleSharp:masterfrom
michaelthyregod:fix/xpath-selector-results

Conversation

@michaelthyregod

@michaelthyregod michaelthyregod commented Nov 8, 2026 •

Copy link
Copy Markdown

AngleSharp caches parsed selectors, so one XPathAttrSelector serves every query and thread of a browsing context. It kept a single scope and result list, so:

  • a query after a DOM change returned the old result (append an li, and *[xpath>'//li'] still finds 3);
  • parallel queries on different documents overwrote each other's result (wrong counts in 1,985 of 2,000 parallel queries);
  • every element was looked up in a List, which is quadratic in the number of matches;
  • Text printed a stray $: *[xpath>'$//li'].

The last result answers the elements of one query; a per-scope table (ConditionalWeakTable) is only created once a second scope shows up. Each result is tagged with the document's MutationVersion (public since AngleSharp 1.8.2) and replaced as a whole when the document has changed. Results of up to 8 nodes are compared by reference, bigger ones through a HashSet that compares references, because Node.Equals(INode) compares structure.

Like the counter itself, the result doesn't follow changes made inside a detached subtree or by the parser while the document is still loading.

Six new tests (one with 20 matches for the HashSet path). Benchmark results are in the comment below.

🤖 Generated with Claude Code

…h the DOM

AngleSharp caches parsed selectors, so one XPathAttrSelector serves every
query and thread of a context. It kept a single scope and result list, so:

- a query after a DOM change returned the old result
- parallel queries on different documents overwrote each other's result
- every element was looked up in a list, quadratic in the number of matches
- Text printed a stray '$': *[xpath>'$//li']

The result is now kept per scope (ConditionalWeakTable), tagged with the
document's MutationVersion and replaced as a whole when the document changed.
Elements are looked up in a HashSet that compares references, because
Node.Equals(INode) compares structure.
@CLAassistant

CLAassistant commented Nov 8, 2026 •

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

…ope only when needed

The per-scope table cost a lookup for every element and a table for every new
selector, which made a few cases slower than before. The last result is now
checked first, the table only exists once a second scope shows up, and results
of up to 8 nodes are compared by reference without hashing.
@michaelthyregod

Copy link
Copy Markdown
Author

Benchmarked with BenchmarkDotNet: master (4f0ff27) vs this PR, both on AngleSharp 1.8.6, net8.0, 3 rounds with the builds alternating, median per case. "Warm" repeats a query (core reuses its cached selector); "Cold" uses a fresh selector, as a first query or the first one after a DOM change. list-N has N matching li; sparse-10000 has 10,000 li with one match.

The first version made two cases slower (warm sparse-10000 2×, cold list-10 2.1×), so 3b1eb11 checks the last result first, creates the per-scope table only once a second scope shows up, and compares results of up to 8 nodes by reference.

Case master this PR master / PR
Warm list-10 450 ns 386 ns 1.16
Cold list-10 1.45 µs 1.48 µs 0.99
Warm list-1000 268 µs 27 µs 9.9
Cold list-1000 770 µs 536 µs 1.44
Warm list-10000 24.8 ms 285 µs 87
Cold list-10000 68.1 ms 44.8 ms 1.52
Warm sparse-10000 120 µs 93 µs 1.29
Cold sparse-10000 44.5 ms 44.7 ms 1.00

Cold queries allocate a set for the result: +424 B at 10 matches, about +200 KB at 10,000.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants