fix(transpile): #6674 buffer the generated Java before handing it to the DOM - #6676
Conversation
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
|
There was a problem hiding this comment.
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 anxsl:variableand emit it via a singlexsl:value-of. - Buffer
<java>body generation into anxsl:variableand emit it via a singlexsl: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.
🚀 Performance AnalysisAll 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
✅ Performance gain: |
|
@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). |



Closes #6674. Follow-up to #6666 — same mechanism, and after that fix
to-java.xslwas 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:Saxon's
TimingTraceListeneronstring/printf.eoput 8,328ms of the 8,881ms total in the net time ofxsl: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.transformpasses Saxon aDOMSourceand aDOMResult). The templates produce the Java as thousands of smallxsl:text/xsl:value-ofpieces, each handed over as it is produced, and Saxon merges adjacent text into one DOM text node. Merging into a XercesTextnode issetNodeValue(this.data + data)— a full copy of everything accumulated.So the pass was quadratic in the size of the generated Java.
<java>forstring/printf.eois 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:DOMResult(what the build uses)StreamResultThe 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: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.xslWhat I ruled out first
eo:eol()andeo:tabs()are each called 4,257 times on one source and build a temporary tree per call, since neither declaresas="xs:string"— the obvious suspect. Rewriting$TAB,eo:eolandeo:tabsas 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
eo-runtimesources — the generated Java itself.MjTranspileTest(52 transpile packs) andFingerprintTest: 57 tests green. That suite also drops from 34.5s to 14.0s.xcopclean on the changed file.to-java.xslis fingerprinted into the transpile cache key viaTranspilation.XSLS, so existing caches invalidate on their own.Generated by Claude Code