v1.0.3
A performance release. No API changes; results are identical to 1.0.2.
Performance
Profile-driven tuning of the core step evaluator (evaluateStep). Earlier
guesses aimed at cutting object/GC allocation, but on this engine GC is only
~3.5% of runtime — so a CPU profile (node --prof bench/bench.js) was used to
find the real hot spots:
- Megamorphic axis lookup — the axis function is now resolved once per
step (resolveAxis) instead of once per input node, eliminating a
megamorphicAXES[axis]keyed-load IC (~6.5% → ~1.6% of ticks). - Redundant predicate classification — a predicate's "is it a pure node-set
(existence filter) vs a position test" is a static property of the AST, now
computed once per step instead of once per input node (isPureNodeSet
~3.3% → ~0.1%); predicate-less steps skip the predicate pass entirely. - Defensive array copies — the
childandattributeaxes no longer
.slice()the adapter's arrays.evaluateSteptreats axis results as
read-only (it copies matches into a fresh array), so an axis array is never
mutated or used as a node-set's backing store. This removes ~5% ofslice
(CloneFastJSArray) overhead; a regression test guards the invariant. - Fused descendant walk — a selective
//nameapplies its node test during
the descendant traversal instead of materializing the whole descendant set
and then filtering it. - Empty input node-sets short-circuit before any per-step setup.
On the capybara-simulated–shaped benchmark this is ~10–29% faster across the
query mix, with scaling still linear:
| query | 1.0.2 | 1.0.3 |
|---|---|---|
| count all elements | 0.160ms | 0.114ms |
| all fields (union-in-predicate) | 0.396ms | 0.306ms |
field by label (//label hoist) |
0.743ms | 0.597ms |
| deep descendant attr | 0.261ms | 0.215ms |
| link by text | 0.281ms | 0.231ms |
| table cell by text | 0.570ms | 0.512ms |
Full Changelog: v1.0.2...v1.0.3