Skip to content

fix(transpile): #6674 buffer the generated Java before handing it to the DOM - #6676

Merged
yegor256 merged 1 commit into
masterfrom
claude/xsl-classes-performance-7mck7r
Aug 12, 2026
Merged

fix(transpile): #6674 buffer the generated Java before handing it to the DOM#6676
yegor256 merged 1 commit into
masterfrom
claude/xsl-classes-performance-7mck7r

Conversation

@yegor256

Copy link
Copy Markdown
Member

Closes #6674. Follow-up to #6666 — same mechanism, and after that fix to-java.xsl was 82% of the transpile train.

Where the time went

Per stylesheet, all 170 sources of eo-runtime/src/main/eo, best of two warm runs, Saxon-HE 13.0, single-threaded:

    725 ms  parse/set-locators.xsl              1205 ms  transpile/classes.xsl
    508 ms  transpile/set-original-names.xsl     649 ms  transpile/tests.xsl
    728 ms  transpile/anonymous-to-nested.xsl    675 ms  transpile/package.xsl
    683 ms  transpile/attrs.xsl                  585 ms  transpile/data.xsl
  25408 ms  transpile/to-java.xsl   <-- 82%
  31168 ms  TRAIN TOTAL

Saxon's TimingTraceListener on string/printf.eo put 8,328ms of the 8,881ms total in the net time of xsl:template match="class" — the template that only opens <tests> and <java> and delegates. Net, so not the 26 templates it calls: that is the cost of handing text to the result tree.

Why

The result tree is a DOM (XSLDocument.transform passes Saxon a DOMSource and a DOMResult). The templates produce the Java as thousands of small xsl:text/xsl:value-of pieces, each handed over as it is produced, and Saxon merges adjacent text into one DOM text node. Merging into a Xerces Text node is setNodeValue(this.data + data) — a full copy of everything accumulated.

So the pass was quadratic in the size of the generated Java. <java> for string/printf.eo is one text node of 484,853 characters, and every piece paid for a copy of all the text before it. Retargeting the identical transform isolates it — same stylesheet, same input, warm:

result before after
DOMResult (what the build uses) 3460 ms 159 ms
StreamResult 91 ms 144 ms

The gap closing is the point: the quadratic term is gone, not just smaller.

The change

Nine lines. Each body goes into a variable, then reaches the element through one xsl:value-of:

<java>
  <xsl:variable name="code">
    <xsl:call-template name="commonclass"/>
    <xsl:apply-templates select="." mode="body"/>
  </xsl:variable>
  <xsl:value-of select="$code"/>
</java>

A temporary tree is Saxon's own, where appending text grows a buffer rather than copying one. Both bodies are pure text, so the variable holds exactly what the element held before — none of the 26 templates changed.

to-java.xsl whole train
before 25.4s 31.2s
after 2.1s 7.9s

What I ruled out first

eo:eol() and eo:tabs() are each called 4,257 times on one source and build a temporary tree per call, since neither declares as="xs:string" — the obvious suspect. Rewriting $TAB, eo:eol and eo:tabs as pure string functions moved the total by nothing measurable (24.5s → 26.1s, i.e. noise), and the trace agrees: 61ms and 27ms gross. That variant is not in this PR; it would be churn for no gain.

Correctness

  • Output byte-identical for all 170 eo-runtime sources — the generated Java itself.
  • MjTranspileTest (52 transpile packs) and FingerprintTest: 57 tests green. That suite also drops from 34.5s to 14.0s.
  • xcop clean on the changed file.
  • to-java.xsl is fingerprinted into the transpile cache key via Transpilation.XSLS, so existing caches invalidate on their own.

Generated by Claude Code

The templates of to-java.xsl assemble a class from thousands of small
"xsl:text" and "xsl:value-of" pieces, and each piece was handed straight
to the result tree. That tree is a DOM, and merging a piece into the text
node already sitting in "<java>" means Xerces copies the whole string
accumulated so far - so the pass was quadratic in the size of the code it
generates. "<java>" for string/printf.eo is one text node of 484,853
characters, and every piece along the way paid for a copy of everything
written before it. Saxon's TimingTraceListener put 8,328ms of an 8,881ms
run in the net time of "xsl:template match=class" alone, the template
that only opens "<tests>" and "<java>" and delegates.

Each body now goes into a variable first and reaches the element through
one "xsl:value-of". A temporary tree is Saxon's own, where appending text
grows a buffer instead of copying one, so the assembly is linear and the
DOM is handed the finished string once. Both bodies are pure text, so a
variable holds exactly what the element held before, and none of the 26
templates needed to change.

Over all 170 sources of eo-runtime/src/main/eo, the pass drops from 25.4s
to 2.1s and the whole transpile train from 31.2s to 7.9s - it was four
fifths of that train. The gap between writing to a DOM and writing to a
stream closes as well (159ms against 144ms on the largest source, from
3460ms against 91ms), which is what tells the quadratic term is gone
rather than merely smaller. Output is byte-identical for all 170 sources.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GevVXivd1KDNnCHqKW9rua
@sonarqubecloud

Copy link
Copy Markdown

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

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 addresses a major performance bottleneck in the XSLT transpilation pipeline by buffering the generated Java/tests text in an XSLT variable before emitting it into the DOM-backed result tree, eliminating quadratic-time DOM text-node growth during <java>/<tests> construction (per #6674).

Changes:

  • Buffer <tests> body generation into an xsl:variable and emit it via a single xsl:value-of.
  • Buffer <java> body generation into an xsl:variable and emit it via a single xsl:value-of.
  • Expand the stylesheet comment above the match="class" entry point to document the DOM quadratic behavior and rationale.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@yegor256
yegor256 merged commit bea5a74 into master Aug 12, 2026
29 of 31 checks passed
@yegor256
yegor256 deleted the claude/xsl-classes-performance-7mck7r branch August 12, 2026 15:09
@github-actions

Copy link
Copy Markdown
Contributor

🚀 Performance Analysis

All benchmarks are within the acceptable range. No critical degradation detected (threshold is 100%). Please refer to the detailed report for more information.

Click to see the detailed report
Test Base Score PR Score Change % Change Unit Mode
benchmarks.XslBench.manySheetsOnLargeXmir 868.056 841.062 -26.994 -3.11% ms/op Average Time

✅ Performance gain: benchmarks.XslBench.manySheetsOnLargeXmir is faster by 26.994 ms/op (3.11%)

@0crat

0crat commented Aug 12, 2026

Copy link
Copy Markdown

@yegor256 Thanks for the contribution! You've earned +4 points for this: +16 as a basis; -8 for the lack of code review; -4 for too few (34) hits-of-code. Please, keep them coming. Your running score is +2886; 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.

to-java.xsl performance issue: time complexity is quadratic for generating Java code sizes

4 participants