Skip to content

Streamed //x record paths and live-count limits (v2.3.0) - #37

Merged
knroy merged 8 commits into
mainfrom
feat/v2.3.0-streaming
Nov 2, 2026
Merged

knroy merged 8 commits into
mainfrom
feat/v2.3.0-streaming

Conversation

@knroy

@knroy knroy commented Nov 2, 2026

Copy link
Copy Markdown
Owner

Streamed record paths with //, wildcards and outermost(), and limits that count what a streamed loop holds. The full list is in CHANGELOG.md under "Unreleased".

Streaming

  • Paths that now stream: //rec, doc//rec, descendant::rec, /*/*/rec, *:rec, p:*, default-namespace names, and outermost()/innermost() of these. Both streamed shapes take them: the xsl:for-each/xsl:iterate loop, and a single value-of/copy-of/sequence.
  • Nested matches: a record is cut at its outermost match, so a match nested inside it is delivered inside its tree, in document order.
  • Small records share one tree, so ancestors are copied once per element opened.
Case Before After
W3C XSLT 3.0 cases on the burst path 364 450
count(//rec) under a 900-deep spine 23.6 s CPU 2.0 s (tree mode 1.6 s)
Flat 100 MB record shapes — 35–60% less CPU

Every admitted shape uses at most the tree's memory. The folded shapes stay near 3 MB heap at 1 GB.

Limits

  • Refunds in streamed loops: a streamed loop item that writes no output now hands back the temporaries it dropped, at most 4,096 nodes and 64 KiB per item by default. Flat-state streams that were refused after about 1M records now run at 1 GB in 3–12 MB. Growing state is still refused.
  • New TransformOptions fields: MaxNodes and MaxBytes (held bounds); MaxNodesBuilt and MaxBytesBuilt (total-work bounds). Zero keeps the defaults.
  • Behaviour change: fn:copy-of and fn:snapshot copies are now charged to the result-node budget, as xsl:copy-of already was.

Reviews

Check Result
Three-way W3C XSLT 3.0 comparison (base / branch / bursts off) byte-identical except the known nondeterministic cases
Fuzz cases 240,000, no differences outside the documented loop-shape error-order class
Tests 11 new, each sabotaged

Verification

sh tests/check.sh at a6db8ec: every lane that ran passed. UBL and CII are skipped (licensed corpora).

Suite Count
QT3 XPath 2.0 / 3.0 / 3.1 15217 / 19362 / 22054
QT3 XQuery 30516
XSD 1.0 / 1.1 39358 / 41567
RELAX NG 965
XSLT 2.0 / 3.0 6193 / 11495
Vendored schemas 185
DocBook 578
XSpec 224 (the usual worktree reading)

Race, 32-bit vet and the w3cschemas module pass.

knroy added 8 commits November 1, 2026 18:45
…dcards

The burster keeps, per open spine element, the set of pattern steps its
ancestry has matched; an element that completes the pattern is a burst
and holds any match nested in it. Reached through internal/xdmburst, so
xdm gains no exported API; ParseBursts takes at most 63 names.
… outermost()

//rec, doc//rec, descendant::rec, /*/*/rec, *:rec, p:* and outermost()
or innermost() of such a path now cut bursts at the outermost match; a
match nested in it is evaluated inside it, in document order, so each
burst holds all of the path's items that lie in it and no other.
xpath's per-transform step memo keyed //x on each burst's document node
and so kept every burst tree to the end: count(//rec) over 100 MB peaked
at 1.8 GB heap against the tree's 0.8 GB. The two forms select the same
nodes, since a record path's predicates do not depend on position.
The result-node and byte budgets were cumulative, so a correct stream with
flat state was refused after a million records. An item of a burst-fed
for-each or iterate that writes no output and forces no global now refunds
its charges beyond what xsl:next-iteration carries on, at most 4,096 nodes
and 64 KiB per item unless the caller bounds the total. fn:copy-of and
fn:snapshot are charged like xsl:copy-of. New TransformOptions MaxNodes,
MaxBytes, MaxNodesBuilt, MaxBytesBuilt.
Each burst's tree copied every ancestor, so a count(//rec) under a
900-deep spine took 37.5 s against the tree's 1.6 s. Small bursts are now
copied into a batch tree, under ancestor copies made once per spine
element opened and once per batch, and delivered together; xslt evaluates
a select once per tree. The parse defers taking back its stores while a
batch shares them, bounded per spine level.
@knroy
knroy merged commit aeaf6cb into main Nov 2, 2026
5 checks passed
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.

1 participant