perf(printer): index merge-monikers lookups with xsl:key - #6511
Conversation
"merge-monikers.xsl" asked the same question from every node it visited — "which references in this formation name this binding?" — and answered it with a "$owner//o[...]" scan, so a document of N nodes walked its own subtree N times. On a 2000-node XMIR that is already over 10 seconds, and the cost grows quadratically from there. The four lookups are now backed by "xsl:key" indexes: references by owning formation and resolved name, applied references by owning formation and receiver name, and eligible bindings by owner-and-name and by name alone. Keys hand their nodes back in document order, so "the first hosting reference" still means what it meant before. The "ξ." prefix is hoisted into a global variable too, since Saxon rebuilds "concat($eo:xi, '.')" at every call rather than folding it. Output is unchanged: the sheet emits byte-identical XML on every fixture and synthetic input tested, and all 334 "eo-printer" tests pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgyCtxMVnZq4qnDXedksSu
|
CI is red on Master at
The cause looks deterministic rather than flaky: I will re-run this job once the base branch is green. Generated by Claude Code |
"typos" reads the XPath range variable "$anc" as a misspelling of "and" and fails the build on all four of its occurrences. "eo:shadowed" already walks the ancestor formations under the name "$scope", so the two kept- reference lookups now spell it the same way. Purely a rename: output stays byte-identical on every real "eo-runtime" file and synthetic input checked, and all 334 "eo-printer" tests pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgyCtxMVnZq4qnDXedksSu
|
|
Second red check, also from the base branch rather than this PR: That job demands an exact total. Running its own command on this branch: Two short of the 364 in So both red checks on this PR — this one and the Windows The checks that do gate this PR — Generated by Claude Code |
|
There was a problem hiding this comment.
Pull request overview
This PR optimizes the merge-monikers.xsl printer pass by replacing several repeated subtree/sibling/ancestor scans with xsl:key-backed lookups, addressing the quadratic behavior described in #6512 while keeping the stylesheet’s observable behavior unchanged.
Changes:
- Added four
xsl:keyindexes (moniker-ref,applied-ref,moniker-binding,moniker-name) to replace hot-path XPath scans used by match patterns and helper functions. - Hoisted
concat($eo:xi, '.')into a single global$eo:xi-dotto avoid repeated concatenation at multiple call sites. - Updated affected functions (
eo:moniker-refs,eo:applied-refs,eo:hosted-binding,eo:applied-handle,eo:kept-const-ref,eo:kept-local-ref) to usekey(...)lookups scoped by owning formation.
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
|
@yegor256 Thanks for the contribution! You've earned +8 points for this: +16 as a basis; -8 for the lack of code review. Please, keep them coming. Your running score is +2839; don't forget to check your Zerocracy account too). |



Fixes part of #6512.
merge-monikers.xslanswers the same question from every node it visits —"which references in this formation name this binding?" — and answered it with
a
$owner//o[...]descendant scan. Since those lookups are reached fromtemplate patterns, which Saxon evaluates against every
onode, a documentof N nodes walked its own subtree N times. Print time therefore grew
quadratically, and in the outermost formation
$owner//ois the wholedocument.
What changed
Four lookups are now backed by
xsl:keyindexes:moniker-ref$owner//o[...]scan ineo:moniker-refsapplied-ref$owner//o[...]scan ineo:applied-refsmoniker-binding$owner/o[...]sibling scan ineo:hosted-bindingandeo:applied-handlemoniker-name$ref/ancestor::o[eo:abstract(.)]/o[...]climb ineo:kept-const-refandeo:kept-local-refThe first three are keyed on
generate-id()of the owning formation plus thename, so a lookup keeps the "same formation, no intervening formation" scoping
the scans enforced by hand. Keys hand their nodes back in document order, so
"the first hosting reference" still means what it meant before — which is what
keeps #5739's first-reference rule and #5890's ordering intact.
concat($eo:xi, '.')is hoisted into a global$eo:xi-dot. Saxon does notfold it, because
$eo:xiis a global variable rather than a literal, soleaving it inline rebuilt the same two-character string at each of the seven
call sites; string concatenation was the largest single group of samples in a
JFR profile of the old sheet.
No behaviour changes. The sheet's semantics, its comments and its function
signatures are untouched — only how the four lookups find their nodes.
Results
Saxon-HE 13.0 (the pinned version), best of several runs, on synthetic XMIR of
the shape the sheet sees:
<o>nodesOn real
eo-runtimesources, taken fromtarget/eo/1-parseand pushed throughthe five print sheets that precede this one, so the input is exactly what
merge-monikersreceives:<o>nodesstring/printf.eopath.eofile.eomap.eotuple.eodirectory.eoRuntime sources gain less because their formations are small; the quadratic
term is driven by the size of the largest formation, not the file. To be
explicit about the limit of that: a full
mvn -pl eo-runtime process-sourcestakes 250 s before and 252 s after, which is to say no measurable difference —
eo-runtimeis not where this hurts, since the goal's time goes to linting,inference and
to-java. The win is on files whose cactus bindings concentratein one scope, where the sheet currently spends tens of seconds. Holding a
synthetic document at ~3000 nodes and only redistributing the same 600
bindings shows the effect on its own: 1046 ms across 100 formations against
22224 ms in a single one, and a flat ~160 ms either way after this change.
Verification
masteron every input tried: all six realeo-runtimefiles above, the threeprint-packsfixtures, and elevensynthetic documents covering bare references, single- and multi-segment
dispatch chains, const handles, named handles, applied formation handles,
recursive handles, shared handles and references from nested formations.
eo-printertests pass, including the 293XmirTestpack tests.xsl:functionparameters triggering parse termination #6091 (Saxon-HE 13.0 miscomputingxsl:functionarguments when onecompiled stylesheet is shared across threads), the keys were checked under
the same conditions the printer uses: one shared
Templates, 24 threads,4320 transforms — zero mismatches. Keys are built per source document by the
per-transformation
KeyManager, so they add no shared mutable state, andthis change introduces no new multi-argument
xsl:function.Remaining optimization opportunities — memoizing the functions that are still
evaluated once in a pattern and again in a body, and the same scan shape in
inline-cactoos.xslandrestore-local-names.xsl— are listed in #6512.🤖 Generated with Claude Code
https://claude.ai/code/session_01SgyCtxMVnZq4qnDXedksSu