Skip to content

Commit 1f09b3d

Browse files
perf: cache toString invocations (#493)
1 parent b8cc7e5 commit 1f09b3d

2 files changed

Lines changed: 60 additions & 19 deletions

File tree

src/main/java/com/jcabi/xml/XSLDocument.java

Lines changed: 49 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@
5050
* @checkstyle ClassFanOutComplexityCheck (500 lines)
5151
*/
5252
@EqualsAndHashCode(of = "xsl")
53-
@SuppressWarnings({"PMD.GodClass", "PMD.TooManyMethods"})
53+
@SuppressWarnings("PMD.TooManyMethods")
5454
public final class XSLDocument implements XSL {
5555

5656
/**
@@ -82,6 +82,25 @@ public final class XSLDocument implements XSL {
8282
XSL.class.getResourceAsStream("strip.xsl")
8383
);
8484

85+
/**
86+
* Per-thread builder — {@link DocumentBuilder} is not thread-safe, so each
87+
* thread keeps its own instance. The factory and builder are created lazily
88+
* on the first {@link #transform} call in each thread, avoiding any risk of
89+
* circular class-initialization when the factory's ServiceLoader scan runs.
90+
*/
91+
private static final ThreadLocal<DocumentBuilder> DBUILDER =
92+
ThreadLocal.withInitial(
93+
() -> {
94+
try {
95+
return DocumentBuilderFactory.newInstance().newDocumentBuilder();
96+
} catch (final ParserConfigurationException ex) {
97+
throw new IllegalStateException(
98+
"Failed to create DocumentBuilder", ex
99+
);
100+
}
101+
}
102+
);
103+
85104
/**
86105
* XSL document.
87106
*/
@@ -108,6 +127,12 @@ public final class XSLDocument implements XSL {
108127
*/
109128
private final transient Unchecked<Templates> templates;
110129

130+
/**
131+
* Formatted (pretty-printed) string form, cached on first use.
132+
* Since {@code xsl} is immutable the result never changes.
133+
*/
134+
private final transient Unchecked<String> formatted;
135+
111136
/**
112137
* Public ctor, from XML as a source.
113138
* @param src XSL document body
@@ -291,7 +316,11 @@ public XSLDocument(final String src, final Sources srcs,
291316
*/
292317
public XSLDocument(final String src, final Sources srcs,
293318
final Map<String, Object> map, final String base) {
294-
this(src, srcs, new HashMap<>(map), base, XSLDocument.load(srcs, src, base));
319+
this(
320+
src, srcs, new HashMap<>(map), base,
321+
XSLDocument.load(srcs, src, base),
322+
XSLDocument.format(src)
323+
);
295324
}
296325

297326
/**
@@ -301,16 +330,18 @@ public XSLDocument(final String src, final Sources srcs,
301330
* @param map Map of XSL params
302331
* @param base SystemId/Base
303332
* @param tmpl Already-compiled stylesheet to reuse
333+
* @param fmt Already-allocated formatted-string scalar to reuse
304334
* @checkstyle ParameterNumberCheck (5 lines)
305335
*/
306336
private XSLDocument(final String src, final Sources srcs,
307337
final Map<String, Object> map, final String base,
308-
final Unchecked<Templates> tmpl) {
338+
final Unchecked<Templates> tmpl, final Unchecked<String> fmt) {
309339
this.xsl = src;
310340
this.sources = srcs;
311341
this.params = new HashMap<>(map);
312342
this.sid = base;
313343
this.templates = tmpl;
344+
this.formatted = fmt;
314345
}
315346

316347
@Override
@@ -325,7 +356,8 @@ public XSL with(final String name, final Object value) {
325356
this.sources,
326357
new MapOf<String, Object>(this.params, new MapEntry<>(name, value)),
327358
this.sid,
328-
this.templates
359+
this.templates,
360+
this.formatted
329361
);
330362
}
331363

@@ -374,25 +406,12 @@ public static XSL make(final URL url) {
374406

375407
@Override
376408
public String toString() {
377-
return new XMLDocument(this.xsl).toString();
409+
return this.formatted.value();
378410
}
379411

380412
@Override
381413
public XML transform(final XML xml) {
382-
final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
383-
final DocumentBuilder builder;
384-
try {
385-
builder = factory.newDocumentBuilder();
386-
} catch (final ParserConfigurationException ex) {
387-
throw new IllegalArgumentException(
388-
String.format(
389-
"Failed to create new XML document by %s",
390-
factory.getClass().getName()
391-
),
392-
ex
393-
);
394-
}
395-
final Document target = builder.newDocument();
414+
final Document target = XSLDocument.DBUILDER.get().newDocument();
396415
this.transformInto(xml, new DOMResult(target));
397416
return new XMLDocument(target);
398417
}
@@ -477,6 +496,17 @@ private Transformer transformer() {
477496
return trans;
478497
}
479498

499+
/**
500+
* Lazy pretty-printed string form of an XSL document.
501+
* @param xsl XSL document body
502+
* @return Cached formatted string
503+
*/
504+
private static Unchecked<String> format(final String xsl) {
505+
return new Unchecked<>(
506+
new Sticky<>(() -> new XMLDocument(xsl).toString())
507+
);
508+
}
509+
480510
/**
481511
* Lazy-load and cache the compiled {@link Templates} object.
482512
* @param sources URI resolver for xsl:import/xsl:include

src/test/java/com/jcabi/xml/XSLDocumentBenchmark.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,4 +97,15 @@ public final XML freshInstanceEachCall() {
9797
.transform(XSLDocumentBenchmark.INPUT);
9898
}
9999

100+
/**
101+
* {@link XSLDocument#toString()} on a reused instance.
102+
* The result is computed once and returned from a
103+
* {@link org.cactoos.scalar.Sticky} cache on every subsequent call.
104+
* @return Formatted XSL string
105+
*/
106+
@Benchmark
107+
public final String toStringCached() {
108+
return XSLDocumentBenchmark.XSL.toString();
109+
}
110+
100111
}

0 commit comments

Comments
 (0)