diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/Parser.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/Parser.java index d1f36a32be154e..39e1d80505afb5 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/Parser.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/Parser.java @@ -1339,6 +1339,10 @@ public SectionHelper initialize(SectionInitContext context) { private static final BlockNode BLOCK_NODE = new BlockNode(); static final CommentNode COMMENT_NODE = new CommentNode(); + static boolean isDummyNode(TemplateNode node) { + return node == COMMENT_NODE || node == BLOCK_NODE; + } + // A dummy node for section blocks, it's only used when removing standalone lines private static class BlockNode implements TemplateNode { diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/TemplateImpl.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/TemplateImpl.java index 3719217194df3a..f2d271d83355b4 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/TemplateImpl.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/TemplateImpl.java @@ -37,6 +37,9 @@ class TemplateImpl implements Template { private final List parameterDeclarations; private final LazyValue> fragments; + // The initial capacity of the StringBuilder used to render the template + final Capacity capacity; + TemplateImpl(EngineImpl engine, SectionNode root, String templateId, String generatedId, Optional variant) { this.engine = engine; this.root = root; @@ -47,6 +50,7 @@ class TemplateImpl implements Template { this.parameterDeclarations = ImmutableList.copyOf(root.getParameterDeclarations()); // Use a lazily initialized map to avoid unnecessary performance costs during parsing this.fragments = initFragments(root); + this.capacity = new Capacity(); } @Override @@ -231,8 +235,30 @@ protected Engine engine() { } private CompletionStage renderAsyncNoTimeout() { - StringBuilder builder = new StringBuilder(1028); - return renderData(data(), builder::append).thenApply(v -> builder.toString()); + StringBuilder builder = new StringBuilder(getCapacity()); + return renderData(data(), builder::append).thenApply(v -> { + String str = builder.toString(); + capacity.update(str.length()); + return str; + }); + } + + private int getCapacity() { + if (!attributes.isEmpty()) { + Object c = getAttribute(TemplateInstance.CAPACITY); + if (c != null) { + if (c instanceof Number) { + return ((Number) c).intValue(); + } else { + try { + return Integer.parseInt(c.toString()); + } catch (NumberFormatException e) { + LOG.warnf("Invalid capacity value set for " + toString() + ": " + c); + } + } + } + } + return capacity.get(); } private CompletionStage renderData(Object data, Consumer consumer) { @@ -280,6 +306,64 @@ public String toString() { } + class Capacity { + + static final int LIMIT = 64 * 1024; + + final int computed; + // intentionally not volatile; it's not a big deal if working with an outdated value + int max; + + Capacity() { + this.computed = Math.min(computeCapacity(root.blocks.get(0)), LIMIT); + } + + void update(int length) { + if (length > max) { + max = length < LIMIT ? length : LIMIT; + } + } + + int get() { + return Math.max(max, computed); + } + + private int computeCapacity(SectionBlock block) { + // This is a bit tricky because a template can contain a lot of dynamic parts + // Our approach is rather conservative, i.e. try not to overestimate/waste memory + int ret = 0; + for (TemplateNode node : block.nodes) { + if (Parser.isDummyNode(node)) { + continue; + } + if (node.isText()) { + ret += node.asText().getValue().length(); + } else if (node.isExpression()) { + // Reserve 10 characters per expression + ret += 10; + } else if (node.isSection()) { + SectionHelper helper = node.asSection().getHelper(); + if (LoopSectionHelper.class.isInstance(helper)) { + // Loop secion - multiply the capacity of the main block by 10 + ret += 10 * computeCapacity(node.asSection().blocks.get(0)); + } else if (IncludeSectionHelper.class.isInstance(helper)) { + // At this point we don't really know - the included template can be tiny or huge + // So we just reserve 500 characters + ret += 500; + } else if (UserTagSectionHelper.class.isInstance(helper)) { + // For user tags we don't expect large templates + ret += 200; + } else { + for (SectionBlock b : node.asSection().blocks) { + ret += computeCapacity(b); + } + } + } + } + return ret; + } + } + class FragmentImpl extends TemplateImpl implements Fragment { FragmentImpl(EngineImpl engine, SectionNode root, String fragmentId, String generatedId, diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/TemplateInstance.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/TemplateInstance.java index 5b5a2f1f9a42a7..4f87f8a4ce1b9a 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/TemplateInstance.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/TemplateInstance.java @@ -37,6 +37,11 @@ public interface TemplateInstance { */ String LOCALE = "locale"; + /** + * Attribute key - the initial capacity of the StringBuilder used to render the template. + */ + String CAPACITY = "capacity"; + /** * Set the the root data object. Invocation of this method removes any data set previously by * {@link #data(String, Object)} and {@link #computedData(String, Function)}. @@ -204,13 +209,23 @@ default TemplateInstance setLocale(Locale locale) { /** * Sets the variant attribute that can be used to select a specific variant of the template. * - * @param variant the variant + * @param variant * @return self */ default TemplateInstance setVariant(Variant variant) { return setAttribute(SELECTED_VARIANT, variant); } + /** + * Sets the initial capacity of the StringBuilder used to render the template. + * + * @param capacity + * @return self + */ + default TemplateInstance setCapacity(int capacity) { + return setAttribute(CAPACITY, capacity); + } + /** * This component can be used to initialize a template instance, i.e. the data and attributes. * diff --git a/independent-projects/qute/core/src/test/java/io/quarkus/qute/TemplateInstanceTest.java b/independent-projects/qute/core/src/test/java/io/quarkus/qute/TemplateInstanceTest.java index 1ee22f343b255f..04cb2dce43cb2b 100644 --- a/independent-projects/qute/core/src/test/java/io/quarkus/qute/TemplateInstanceTest.java +++ b/independent-projects/qute/core/src/test/java/io/quarkus/qute/TemplateInstanceTest.java @@ -4,11 +4,15 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; +import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.concurrent.atomic.AtomicBoolean; import org.junit.jupiter.api.Test; +import io.quarkus.qute.TemplateImpl.Capacity; + public class TemplateInstanceTest { @Test @@ -94,4 +98,26 @@ public void testVariant() { String render = hello.instance().setVariant(Variant.forContentType(Variant.TEXT_HTML)).render(); assertEquals("Hello text/html!", render); } + + @Test + public void testCapacity() { + Engine engine = Engine.builder().addDefaults().build(); + assertCapacity(engine, "foo", 3, 3, Map.of()); + assertCapacity(engine, "{! comment is ignored !}foo", 3, 3, Map.of()); + assertCapacity(engine, "{foo} and bar", 10 + 8, 28, Map.of("foo", "bazzz".repeat(4))); + assertCapacity(engine, "{#each foo}bar{/}", 10 * 3, 3, Map.of("foo", List.of(1))); + assertCapacity(engine, "{#include bar /} and bar", 500 + 8, -1, Map.of()); + // limit reached + assertCapacity(engine, "{#each}{foo}{/}".repeat(1000), Capacity.LIMIT, -1, Map.of()); + assertCapacity(engine, "{foo}", 10, Capacity.LIMIT, Map.of("foo", "b".repeat(70_000))); + } + + private void assertCapacity(Engine engine, String val, int expectedComputed, int expectedMax, Map data) { + TemplateImpl template = (TemplateImpl) engine.parse(val); + assertEquals(expectedComputed, template.capacity.computed); + if (expectedMax != -1) { + template.render(data); + assertEquals(expectedMax, template.capacity.max); + } + } }