Skip to content

perf(printer): index merge-monikers lookups with xsl:key - #6511

Merged
yegor256 merged 3 commits into
masterfrom
claude/merge-monikers-xsl-perf-flf20v
Aug 9, 2026
Merged

perf(printer): index merge-monikers lookups with xsl:key#6511
yegor256 merged 3 commits into
masterfrom
claude/merge-monikers-xsl-perf-flf20v

Conversation

@yegor256

@yegor256 yegor256 commented Aug 9, 2026

Copy link
Copy Markdown
Member

Fixes part of #6512.

merge-monikers.xsl answers 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 from
template patterns, which Saxon evaluates against every o node, a document
of N nodes walked its own subtree N times. Print time therefore grew
quadratically, and in the outermost formation $owner//o is the whole
document.

What changed

Four lookups are now backed by xsl:key indexes:

key replaces
moniker-ref the $owner//o[...] scan in eo:moniker-refs
applied-ref the $owner//o[...] scan in eo:applied-refs
moniker-binding the $owner/o[...] sibling scan in eo:hosted-binding and eo:applied-handle
moniker-name the $ref/ancestor::o[eo:abstract(.)]/o[...] climb in eo:kept-const-ref and eo:kept-local-ref

The first three are keyed on generate-id() of the owning formation plus the
name, 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 not
fold it, because $eo:xi is a global variable rather than a literal, so
leaving 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> nodes before after speedup
502 782 ms 112 ms
1002 2421 ms 67 ms 36×
2002 10170 ms 90 ms 113×
4002 41427 ms 199 ms 208×
16002 (over 10 min) 558 ms

On real eo-runtime sources, taken from target/eo/1-parse and pushed through
the five print sheets that precede this one, so the input is exactly what
merge-monikers receives:

file <o> nodes before after speedup
string/printf.eo 1151 440 ms 95 ms 4.6×
path.eo 1021 205 ms 81 ms 2.5×
file.eo 810 127 ms 71 ms 1.8×
map.eo 741 108 ms 64 ms 1.7×
tuple.eo 719 97 ms 55 ms 1.8×
directory.eo 684 111 ms 62 ms 1.8×

Runtime 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-sources
takes 250 s before and 252 s after, which is to say no measurable difference —
eo-runtime is not where this hurts, since the goal's time goes to linting,
inference and to-java. The win is on files whose cactus bindings concentrate
in 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

  • Output is byte-identical to master on every input tried: all six real
    eo-runtime files above, the three print-packs fixtures, and eleven
    synthetic 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.
  • All 334 eo-printer tests pass, including the 293 XmirTest pack tests.
  • Given Saxon-HE 13.0 incorrectly assigns values to xsl:function parameters triggering parse termination #6091 (Saxon-HE 13.0 miscomputing xsl:function arguments when one
    compiled 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, and
    this 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.xsl and restore-local-names.xsl — are listed in #6512.

🤖 Generated with Claude Code

https://claude.ai/code/session_01SgyCtxMVnZq4qnDXedksSu

"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

yegor256 commented Aug 9, 2026

Copy link
Copy Markdown
Member Author

CI is red on mvn (windows-2022, 26), and it is failing on the base branch too — not caused by this PR.

Master at 98647bd, the exact commit this branch sits on, fails the same job with the same two errors and the same totals (547 run, 2 errors, 1 skipped):

[ERROR] CommitHashesMapTest.containsValidHash:36 NullPointer Cannot invoke
"org.eolang.maven.CommitHash.value()" because the return value of
"org.eolang.maven.CommitHashesMap$Fake.get(Object)" is null

884d2f7 before it fails identically. Every other platform in the matrix is
unaffected, eo-printer builds green on Windows with all its tests, and this
PR touches one XSL file in that module.

The cause looks deterministic rather than flaky: CommitHashesMap.FAKES joins
its rows with System.lineSeparator(), while fromTable splits the table on
"\\n". On Windows the separator is \r\n, so every row keeps a trailing
\r and each map key becomes "0.26.0\r" rather than "0.26.0" — hence the
null from get("0.26.0"). Either side alone would fix it; the class is also
already up for removal in #6487, which would take the test with it.

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
@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

⚠️ Benchmark Comparison Unavailable

Unfortunately, one of the benchmarks is missing, and we couldn't generate a performance comparison report.

Please ensure that both the base and PR benchmark results are available for analysis.

yegor256 commented Aug 9, 2026

Copy link
Copy Markdown
Member Author

Second red check, also from the base branch rather than this PR: counts (suppressions, *.java, @SuppressWarnings|@checkstyle, 364).

That job demands an exact total. Running its own command on this branch:

$ git ls-files -z '*.java' | xargs -0 grep -Eoh '@SuppressWarnings|@checkstyle' | wc -l
362

Two short of the 364 in .github/workflows/counts.yml. This PR changes no
Java at all — git diff --name-only master... lists only
eo-printer/.../merge-monikers.xsl — so the count here is necessarily
whatever master's is. counts duly fails on master too, at 98647bd (this
PR's base) and 884d2f7, having last passed at 0b2d7df6. Two suppressions
were dropped without lowering the expected number; the fix is count: 362
in that workflow, on master.

So both red checks on this PR — this one and the Windows CommitHashesMap
failure above — are inherited, and neither is something a perf change to one
XSL sheet should be carrying a fix for. The "Benchmark Comparison
Unavailable" notice is a third symptom of the same thing: that action diffs
against a master benchmark that the red base build never produced. It
benchmarks eo-parser in any case, so it would not have measured this
change either way.

The checks that do gate this PR — xcop, xslint and typos — are still
queued behind the backlog; I will follow up when they land.


Generated by Claude Code

@yegor256
yegor256 marked this pull request as ready for review August 9, 2026 14:01
Copilot AI lite review requested due to automatic review settings August 9, 2026 14:01
@sonarqubecloud

sonarqubecloud Bot commented Aug 9, 2026

Copy link
Copy Markdown

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:key indexes (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-dot to 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 use key(...) 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
yegor256 merged commit b9ad1c7 into master Aug 9, 2026
35 of 37 checks passed
@yegor256
yegor256 deleted the claude/merge-monikers-xsl-perf-flf20v branch August 9, 2026 14:10
@0crat

0crat commented Aug 9, 2026

Copy link
Copy Markdown

@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).

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.

3 participants