Skip to content

v1.0.4

Latest

Choose a tag to compare

@github-actions github-actions released this 02 Jun 02:36
· 4 commits to main since this release
68f6ae3

A performance release. No API changes; results are identical to 1.0.3 — verified by the full conformance suite and a 500k-input differential check on normalize-space.

Performance

The headline is an algorithmic rewrite of the query shape Capybara leans on, plus a round of allocation and per-node cuts. Found with a CPU profile (node --cpu-prof bench/bench.js).

  • //-fusion (the big one)//E desugars to /descendant-or-self::node()/child::E (XPath 1.0 REC §2.5), whose node-set is exactly descendant::E. A new parse-time normalization fuses that step pair into a single descendant::E step, so the common .//X shape now streams one tree walk instead of materializing the entire descendant-or-self::node() set (text and comment nodes included) and then re-walking every node's children. The fusion is gated by a conservative position-stability guard: it never fires when a predicate can observe proximity position or size (a numeric/positional predicate, or position()/last()), so //e[1], //e[last()], etc. keep their exact per-parent semantics.
  • Fewer allocations in the predicate loopapplyPredicates reuses one child context per frame and skips the output-array copy when a predicate keeps every candidate; unionNodeSets short-circuits an empty side; NodeSet.first fast-paths the common single-node set.
  • Less per-node work — single-pass normalize-space over the exact XPath whitespace set (#x20|#x9|#xD|#xA), with a fast path for already-normalized text; one Map lookup on the string-value memo hit path; * name tests skip the name/namespace adapter reads; structural classifications (single relative step, @name <op> literal shape, folded HTML attribute name) are memoized in WeakMaps keyed by the AST node, leaving the parse tree immutable.

Total profiled CPU on the benchmark dropped ~40% (1370ms → 827ms). Per query, on the capybara-simulated–shaped benchmark (node bench/bench.js, 200-block page), ~28–58% faster, with scaling still linear:

query 1.0.3 1.0.4
count all elements 0.081ms 0.034ms
all fields (union-in-predicate) 0.214ms 0.134ms
field by label (//label hoist) 0.403ms 0.241ms
deep descendant attr 0.147ms 0.078ms
link by text 0.161ms 0.076ms
table cell by text 0.366ms 0.265ms

Full Changelog: v1.0.3...v1.0.4