Skip to content

fix(transpile): #6664 serialize XMIR into one string in classes.xsl - #6666

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

fix(transpile): #6664 serialize XMIR into one string in classes.xsl#6666
yegor256 merged 1 commit into
masterfrom
claude/xsl-classes-performance-7mck7r

Conversation

@yegor256

Copy link
Copy Markdown
Member

Closes #6664.

classes.xsl writes the XMIR of each top-level object into <xmir>, as the text to-java.xsl turns into the Javadoc of the generated class. The serialize template spelled that text out piece by piece — one instruction per indent, per angle bracket, per name and five per attribute. jcabi-xml runs every stylesheet DOM to DOM (DOMSourceDOMResult), so each piece became a separate DOM text node: a single <xmir> for string/printf.eo (~1.6k elements, ~8.3k attributes) held roughly 50k of them. Building them, and then reading them back through a DOM that has to merge adjacent text nodes to answer text(), cost far more than the concatenation itself — hence XSL 'classes' took 4s (over 500ms) from xsline's StFast on a single XMIR.

It is now an xsl:function returning one xs:string, written with one xsl:value-of, so <xmir> holds exactly one text node.

Numbers

Saxon-HE 13.0, all 170 sources of eo-runtime/src/main/eo parsed with EoSyntax and put through set-locators.xsl + set-original-names.xsl first, best of two warm runs, single-threaded:

classes.xsl alone whole transpile train (9 XSLs) worst file, train (string/printf.eo)
before 21.5s 56.9s 8.0s
after 1.0s 34.4s 4.0s

21x for the pass, 1.7x for the train. The train gains more than the pass saves, because the eight downstream stylesheets no longer walk those adjacent text nodes either. For reference, replacing the serializer with an empty <xmir/> altogether takes the pass to 0.14s, so what is left is close to the floor.

Why nested concat and not a joined sequence

Two other shapes were measured on the 21 largest sources (baseline 15.2s there):

shape time
function returning xs:string*, one string-join at the end 2.1s
same, but each attribute as its own sequence item 4.8s
function returning xs:string, recursive call inside concat (this PR) 0.53s

Nesting the recursive call copies a subtree's string once per level of depth above it, which at the depth of a real XMIR is far cheaper than Saxon's per-item bookkeeping. The rejected shapes are noted in the comment on the function so the next reader does not re-try them.

Correctness

  • Output is byte-identical for all 170 eo-runtime sources — both of this pass alone, and of the full nine-stylesheet train, i.e. of the generated Java.
  • MjTranspileTest (52 transpile packs) and FingerprintTest: 57 tests, all green.
  • xcop clean on the changed file.
  • The pass is fingerprinted into the transpile cache key via Transpilation.XSLS, so existing caches invalidate on their own.

Also in this diff

The class-matching pattern loses a redundant eo:atom call and a count(...) > 0:

-match="object/o[not(eo:atom(.)) or (eo:atom(.) and count(./o[eo:test-attr(.)])&gt;0)]"
+match="object/o[not(eo:atom(.)) or exists(o[eo:test-attr(.)])]"

not(A) or (A and B) is not(A) or B. Not measurable next to the serializer, but it is the same question asked once instead of twice.


Generated by Claude Code

The "serialize" template in classes.xsl wrote the XMIR of an object into
"<xmir>" piece by piece - one instruction per indent, per angle bracket,
per name and five per attribute. Since jcabi-xml runs every stylesheet
DOM to DOM, each of those pieces became a separate DOM text node, so a
single "<xmir>" of a large eo-runtime object ended up holding tens of
thousands of them. Building those nodes, and then reading them back
through a DOM that has to merge adjacent text to answer "text()", cost
far more than the concatenation itself: the pass alone was two fifths of
the whole transpile train, and xsline warned "XSL 'classes' took 4s".

It is now an "xsl:function" returning one string, written with one
"xsl:value-of", so "<xmir>" holds exactly one text node. The recursive
call sits inside the enclosing "concat", which copies a subtree's string
once per level of depth above it - measurably cheaper than returning a
sequence of pieces and joining them at the end.

Over all 170 sources of eo-runtime/src/main/eo, the pass drops from 21.5s
to 1.0s and the whole nine-stylesheet train from 56.9s to 34.4s (the
train gains more than the pass saves, because the downstream stylesheets
no longer walk those adjacent text nodes). The output is byte-identical,
both of this pass and of the train that ends in the generated Java.

The class-matching pattern loses its redundant second "eo:atom" call at
the same time: "not(A) or (A and B)" is "not(A) or B".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GevVXivd1KDNnCHqKW9rua
@yegor256
yegor256 marked this pull request as ready for review August 12, 2026 13:33
Copilot AI lite review requested due to automatic review settings August 12, 2026 13:33
@sonarqubecloud

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

Improves transpilation performance by changing how classes.xsl serializes per-object XMIR into the generated <xmir> text used downstream for JavaDoc generation, ensuring the result is produced as a single DOM text node instead of tens of thousands of adjacent nodes.

Changes:

  • Replaces the recursive serialize named template with an eo:serialize() xsl:function that returns a single xs:string, emitted via one xsl:value-of.
  • Simplifies the class-selection match predicate by removing redundant eo:atom(.) work and using exists(...) instead of count(...) > 0.

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

Comment on lines +35 to +36
<!-- An object with neither data nor children is written as a single self-closing tag. -->
<xsl:variable name="void" as="xs:boolean" select="$content = '' and empty($kids)"/>

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Keeping it, since the convention it appeals to isn't the one in this codebase: _funcs.xsl — the library this stylesheet imports — carries ten inline comments inside function bodies (<!-- 2-byte sequence: 110xxxxx 10xxxxxx -->, <!-- 55296 = 0xD800 --> and so on), all of them annotating a single line the way this one does.

The line above $void explains what the variable means — that an object with neither data nor children is written as a self-closing tag — while the doc comment above the function is about why the function has this shape at all (DOM text nodes, and the two slower shapes that were measured and rejected). Folding a one-line note about output format into that paragraph would bury it, and it belongs next to the variable it names.


Generated by Claude Code

@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 816.672 836.829 20.156 2.47% ms/op Average Time

⚠️ Performance loss: benchmarks.XslBench.manySheetsOnLargeXmir is slower by 20.156 ms/op (2.47%)

@yegor256
yegor256 merged commit e4a5dea into master Aug 12, 2026
29 of 31 checks passed
@yegor256
yegor256 deleted the claude/xsl-classes-performance-7mck7r branch August 12, 2026 13:54
@0crat

0crat commented Aug 12, 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 +2874; 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.

Performance: classes.xsl builds <xmir> one DOM text node at a time, taking seconds per object

4 participants