PR #89 TemplateProcessingTracer reworked:
- Instead of passing several arguments to the TemplateProcessingTracer method, only pass a single TracedElement object. This allow use to add new methods later without breaking backward compatibility.
- Added TracedElement.getDescription() to give a single-line canonical description of the element (same format that we use in FTL stack taces)
- Expose the Environment object to TemplateProcessingTracer methods
- Do not expose the TracedElement to exitElement, because we can't always ensure that it's available because of some internal tricks
- Added special branch to iterator directives and if-elseif-else, as because of the element stack tricks they apply normally, some elements were hidden from the tracer
- Some minor renaming.
- Added getter to Environment.
- Extended test coverage, and fixed quotation logic in the test (it was broken for example for ?interpret-ed snippets)
- Added more documentation
diff --git a/src/main/java/freemarker/core/Environment.java b/src/main/java/freemarker/core/Environment.java
index 6cbfe28..66f1214 100644
--- a/src/main/java/freemarker/core/Environment.java
+++ b/src/main/java/freemarker/core/Environment.java
@@ -188,7 +188,7 @@
private boolean fastInvalidReferenceExceptions;
- private TemplateProcessingTracer currentTracer;
+ private TemplateProcessingTracer templateProcessingTracer;
/**
* Retrieves the environment object associated with the current thread, or {@code null} if there's no template
@@ -2882,10 +2882,22 @@
}
/**
- * Sets the tracer to use for this environment.
+ * Sets the {@link TemplateProcessingTracer} to use for this {@link Environment};
+ * can be {@code null} to not have one. The default is also {@code null}.
+ *
+ * @since 2.3.33
*/
- public void setTracer(TemplateProcessingTracer tracer) {
- currentTracer = tracer;
+ public void setTemplateProcessingTracer(TemplateProcessingTracer templateProcessingTracer) {
+ this.templateProcessingTracer = templateProcessingTracer;
+ }
+
+ /**
+ * Getter pair of {@link #setTemplateProcessingTracer(TemplateProcessingTracer)}. Can be {@code null}.
+ *
+ * @since 2.3.33
+ */
+ public TemplateProcessingTracer getTemplateProcessingTracer() {
+ return templateProcessingTracer;
}
private void pushElement(TemplateElement element) {
@@ -2900,19 +2912,15 @@
this.instructionStack = instructionStack;
}
instructionStack[newSize - 1] = element;
- if (currentTracer != null) {
- currentTracer.enterElement(element.getTemplate(),
- element.getBeginColumn(), element.getBeginLine(),
- element.getEndColumn(), element.getEndLine(), element.isLeaf());
+ if (templateProcessingTracer != null) {
+ templateProcessingTracer.enterElement(this, element);
}
}
private void popElement() {
- if (currentTracer != null) {
+ if (templateProcessingTracer != null) {
TemplateElement element = instructionStack[instructionStackSize - 1];
- currentTracer.exitElement(element.getTemplate(),
- element.getBeginColumn(), element.getBeginLine(),
- element.getEndColumn(), element.getEndLine());
+ templateProcessingTracer.exitElement(this);
}
instructionStackSize--;
}
diff --git a/src/main/java/freemarker/core/IfBlock.java b/src/main/java/freemarker/core/IfBlock.java
index 223d755..52615d2 100644
--- a/src/main/java/freemarker/core/IfBlock.java
+++ b/src/main/java/freemarker/core/IfBlock.java
@@ -42,12 +42,24 @@
@Override
TemplateElement[] accept(Environment env) throws TemplateException, IOException {
int ln = getChildCount();
- for (int i = 0; i < ln; i++) {
- ConditionalBlock cblock = (ConditionalBlock) getChild(i);
- Expression condition = cblock.condition;
- env.replaceElementStackTop(cblock);
- if (condition == null || condition.evalToBoolean(env)) {
- return cblock.getChildBuffer();
+ if (env.getTemplateProcessingTracer() == null) {
+ for (int i = 0; i < ln; i++) {
+ ConditionalBlock cblock = (ConditionalBlock) getChild(i);
+ Expression condition = cblock.condition;
+ env.replaceElementStackTop(cblock);
+ if (condition == null || condition.evalToBoolean(env)) {
+ return cblock.getChildBuffer();
+ }
+ }
+ } else {
+ for (int i = 0; i < ln; i++) {
+ ConditionalBlock cblock = (ConditionalBlock) getChild(i);
+ Expression condition = cblock.condition;
+ env.replaceElementStackTop(cblock);
+ if (condition == null || condition.evalToBoolean(env)) {
+ env.visit(cblock);
+ return null;
+ }
}
}
return null;
diff --git a/src/main/java/freemarker/core/ListElseContainer.java b/src/main/java/freemarker/core/ListElseContainer.java
index 856e5b0..4e307c3 100644
--- a/src/main/java/freemarker/core/ListElseContainer.java
+++ b/src/main/java/freemarker/core/ListElseContainer.java
@@ -37,9 +37,24 @@
@Override
TemplateElement[] accept(Environment env) throws TemplateException, IOException {
- if (listPart.acceptWithResult(env)) {
+ boolean hadItems;
+
+ TemplateProcessingTracer templateProcessingTracer = env.getTemplateProcessingTracer();
+ if (templateProcessingTracer == null) {
+ hadItems = listPart.acceptWithResult(env);
+ } else {
+ templateProcessingTracer.enterElement(env, listPart);
+ try {
+ hadItems = listPart.acceptWithResult(env);
+ } finally {
+ templateProcessingTracer.exitElement(env);
+ }
+ }
+
+ if (hadItems) {
return null;
}
+
return new TemplateElement[] { elsePart };
}
diff --git a/src/main/java/freemarker/core/TemplateElement.java b/src/main/java/freemarker/core/TemplateElement.java
index 6cb9b54..2e79d93 100644
--- a/src/main/java/freemarker/core/TemplateElement.java
+++ b/src/main/java/freemarker/core/TemplateElement.java
@@ -37,7 +37,7 @@
* it.
*/
@Deprecated
-abstract public class TemplateElement extends TemplateObject {
+abstract public class TemplateElement extends TemplateObject implements TemplateProcessingTracer.TracedElement {
private static final int INITIAL_REGULATED_CHILD_BUFFER_CAPACITY = 6;
@@ -89,9 +89,9 @@
* One-line description of the element, that contains all the information that is used in
* {@link #getCanonicalForm()}, except the nested content (elements) of the element. The expressions inside the
* element (the parameters) has to be shown. Meant to be used for stack traces, also for tree views that don't go
- * down to the expression-level. There are no backward-compatibility guarantees regarding the format used ATM, but
- * it must be regular enough to be machine-parseable, and it must contain all information necessary for restoring an
- * AST equivalent to the original.
+ * down to the expression-level. There are no backward-compatibility guarantees regarding the format used, although
+ * it shouldn't change unless to fix a bug. It must be regular enough to be machine-parseable, and it must contain
+ * all information necessary for restoring an AST equivalent to the original.
*
* This final implementation calls {@link #dump(boolean) dump(false)}.
*
diff --git a/src/main/java/freemarker/core/TemplateObject.java b/src/main/java/freemarker/core/TemplateObject.java
index e535d08..7ef6cfb 100644
--- a/src/main/java/freemarker/core/TemplateObject.java
+++ b/src/main/java/freemarker/core/TemplateObject.java
@@ -86,24 +86,36 @@
this.endColumn = endColumn;
this.endLine = endLine;
}
-
- public final int getBeginColumn() {
- return beginColumn;
- }
+ /**
+ * 1-based index of the line (row) of the first character of the element in the template.
+ */
public final int getBeginLine() {
return beginLine;
}
- public final int getEndColumn() {
- return endColumn;
+ /**
+ * 1-based index of the column of the first character of the element in the template.
+ */
+ public final int getBeginColumn() {
+ return beginColumn;
}
+ /**
+ * 1-based index of the line (row) of the last character of the element in the template.
+ */
public final int getEndLine() {
return endLine;
}
/**
+ * 1-based index of the column of the last character of the element in the template.
+ */
+ public final int getEndColumn() {
+ return endColumn;
+ }
+
+ /**
* Returns a string that indicates
* where in the template source, this object is.
*/
diff --git a/src/main/java/freemarker/core/TemplateProcessingTracer.java b/src/main/java/freemarker/core/TemplateProcessingTracer.java
index 434891e..2130e6c 100644
--- a/src/main/java/freemarker/core/TemplateProcessingTracer.java
+++ b/src/main/java/freemarker/core/TemplateProcessingTracer.java
@@ -19,38 +19,77 @@
package freemarker.core;
-import freemarker.ext.util.IdentityHashMap;
-import freemarker.template.Configuration;
import freemarker.template.Template;
-import freemarker.template.TemplateDirectiveModel;
-import freemarker.template.TemplateTransformModel;
-import freemarker.template.utility.ObjectFactory;
/**
- * Run-time tracer plug-in. This may be * used to implement profiling, coverage analytis, execution tracing,
+ * Hooks to monitor as templates run. This may be used to implement profiling, coverage analysis, execution tracing,
* and other on-the-fly debugging mechanisms.
* <p>
- * Use {@link Environment#setTracer(TemplateProcessingTracer)} to configure a tracer for the current environment.
+ * Use {@link Environment#setTemplateProcessingTracer(TemplateProcessingTracer)} to set a tracer for the current
+ * environment.
*
* @since 2.3.33
*/
public interface TemplateProcessingTracer {
/**
- * Invoked by {@link Environment} whenever it starts processing a new template element. {@code
- * isLeafElement} indicates whether this element is a leaf, or whether the tracer should expect
- * to receive lower-level elements within the context of this one.
+ * Invoked by {@link Environment} whenever it starts processing a new template element. A template element is a
+ * directive call, an interpolation (like <code>${...}</code>), a comment block, or static text. Expressions
+ * are not template elements.
*
* @since 2.3.23
*/
- void enterElement(Template template, int beginColumn, int beginLine, int endColumn, int endLine,
- boolean isLeafElement);
+ void enterElement(Environment env, TracedElement tracedElement);
/**
* Invoked by {@link Environment} whenever it completes processing a new template element.
+ *
+ * @see #enterElement(Environment, TracedElement)
*
* @since 2.3.23
*/
- void exitElement(Template template, int beginColumn, int beginLine, int endColumn, int endLine);
+ void exitElement(Environment env);
+
+ /**
+ * Information about the template element that we enter of exit.
+ */
+ interface TracedElement {
+ /**
+ * The {@link Template} that contains this element.
+ */
+ Template getTemplate();
+
+ /**
+ * 1-based index of the line (row) of the first character of the element in the template.
+ */
+ int getBeginLine();
+
+ /**
+ * 1-based index of the column of the first character of the element in the template.
+ */
+ int getBeginColumn();
+
+ /**
+ * 1-based index of the line (row) of the last character of the element in the template.
+ */
+ int getEndColumn();
+
+ /**
+ * 1-based index of the column of the last character of the element in the template.
+ */
+ int getEndLine();
+
+ /**
+ * If this is an element that has no nested elements.
+ */
+ boolean isLeaf();
+
+ /**
+ * One-line description of the element, that also contains the parameter expressions, but not the nested content
+ * (child elements). There are no hard backward-compatibility guarantees regarding the format used, although
+ * it shouldn't change unless to fix a bug.
+ */
+ String getDescription();
+ }
}
diff --git a/src/main/java/freemarker/template/Template.java b/src/main/java/freemarker/template/Template.java
index 967d222..c6fcc31 100644
--- a/src/main/java/freemarker/template/Template.java
+++ b/src/main/java/freemarker/template/Template.java
@@ -764,7 +764,8 @@
*
* @param beginColumn the first column of the requested source, 1-based
* @param beginLine the first line of the requested source, 1-based
- * @param endColumn the last column of the requested source, 1-based
+ * @param endColumn the last column of the requested source, 1-based. If this is beyond the last character of the
+ * line, it assumes that you want to whole line.
* @param endLine the last line of the requested source, 1-based
*
* @see freemarker.core.TemplateObject#getSource()
@@ -787,7 +788,7 @@
}
}
int lastLineLength = lines.get(endLine).toString().length();
- int trailingCharsToDelete = lastLineLength - endColumn - 1;
+ int trailingCharsToDelete = endColumn < lastLineLength ? lastLineLength - endColumn - 1 : 0;
buf.delete(0, beginColumn);
buf.delete(buf.length() - trailingCharsToDelete, buf.length());
return buf.toString();
diff --git a/src/manual/en_US/book.xml b/src/manual/en_US/book.xml
index 26bc653..d5969fe 100644
--- a/src/manual/en_US/book.xml
+++ b/src/manual/en_US/book.xml
@@ -30140,6 +30140,21 @@
</listitem>
</itemizedlist>
</listitem>
+
+ <listitem>
+ <para><link
+ xlink:href="https://github.com/apache/freemarker/pull/89">GitHub
+ PR 89</link>: Added <literal>TemplateProcessingTracer</literal>
+ mechanism, that can be used to monitor coverage, and performance
+ <emphasis>inside</emphasis> templates as they are being
+ processed. For example, you could construct a heat map for how
+ often the different parts run, or finding the performance hot
+ spots. (There can be other creative uses, like watching for a
+ variable to have a certain value.) Use
+ <literal>Environment.setTemplateProcessingTracer(TemplateProcessingTracer)</literal>
+ to enable this kind of monitoring. (See the API docs for
+ more.)</para>
+ </listitem>
</itemizedlist>
</section>
diff --git a/src/test/java/freemarker/core/TemplateProcessingTracerTest.java b/src/test/java/freemarker/core/TemplateProcessingTracerTest.java
index f069dc1..c73554d 100644
--- a/src/test/java/freemarker/core/TemplateProcessingTracerTest.java
+++ b/src/test/java/freemarker/core/TemplateProcessingTracerTest.java
@@ -20,15 +20,15 @@
import static org.junit.Assert.*;
-import java.io.StringWriter;
import java.util.ArrayList;
-import java.util.Arrays;
import java.util.List;
import org.junit.Test;
import freemarker.template.Configuration;
import freemarker.template.Template;
+import freemarker.template.utility.NullWriter;
+import freemarker.template.utility.StringUtil;
public class TemplateProcessingTracerTest {
@@ -43,41 +43,203 @@
"<#list [] as item>\n" +
"${item}<#else>" +
"Yup.\n" +
- "</#list>\n";
+ "</#list>\n" +
+ "<#list 1..2 as i>${i}</#list>" +
+ "<#list 1..3 as j>${j}<#sep>, </#list>" +
+ "<#foreach k in 1..2>k=${k}</#foreach>" +
+ "<#attempt>succeed<#recover>not visited</#attempt>" +
+ "<#attempt>will fail${fail}<#recover>recover</#attempt>" +
+ "<@('x'?interpret) />" +
+ "<#if true>t<#else>f</#if>" +
+ "<#if false>t<#else>f</#if>" +
+ "<#if false>t1<#elseif false>f1<#else>f2</#if>" +
+ "<#if false>t1<#elseif true>t2<#else>f2</#if>" +
+ "<#switch 2>" +
+ "<#case 1>C1<#break>" +
+ "<#case 2>C2<#break>" +
+ "<#case 3>C3<#break>" +
+ "<#default>D" +
+ "</#switch>" +
+ "<#switch 3>" +
+ "<#case 1>C1<#break>" +
+ "<#case 2>C3<#break>" +
+ "<#default>D" +
+ "</#switch>" +
+ "<#macro m>Hello from m!</#macro>" +
+ "Calling macro: <@m />" +
+ "<#assign t>captured</#assign>" +
+ "\n";
@Test
public void test() throws Exception {
Configuration cfg = new Configuration(Configuration.VERSION_2_3_32);
Template t = new Template("test.ftl", TEMPLATE_TEXT, cfg);
- StringWriter sw = new StringWriter();
- Tracer tracer = new Tracer(TEMPLATE_TEXT);
- Environment env = t.createProcessingEnvironment(null, sw);
- env.setTracer(tracer);
+ TestTemplateProcessingTracer tracer = new TestTemplateProcessingTracer();
+ Environment env = t.createProcessingEnvironment(null, NullWriter.INSTANCE);
+ env.setTemplateProcessingTracer(tracer);
env.process();
- List<String> expected = Arrays.asList("Yup.", "Always.", "${item}", "${item}", "${item}", "Yup.");
- assertEquals(expected, tracer.elementsVisited);
+ System.out.println();
+ for (String it : tracer.leafElementSourceSnippets) {
+ System.out.println(StringUtil.jQuote(it) + ",");
+ }
+ System.out.println();
+ for (String it : tracer.indentedElementDescriptions) {
+ System.out.println("|" + it);
+ }
+ System.out.println();
+
+ assertEquals(
+ List.of(
+ "Yup.\n",
+ "Always.\n",
+ "${item}",
+ "${item}",
+ "${item}",
+ "Yup.\n",
+ "${i}",
+ "${i}",
+ "${j}",
+ ", ",
+ "${j}",
+ ", ",
+ "${j}",
+ "k=",
+ "${k}",
+ "k=",
+ "${k}",
+ "succeed",
+ "will fail",
+ "${fail}",
+ "recover",
+ "<@('x'?interpret) />",
+ "x",
+ "t",
+ "f",
+ "f2",
+ "t2",
+ "C2",
+ "<#break>",
+ "D",
+ "Calling macro: ",
+ "<@m />",
+ "Hello from m!",
+ "captured",
+ "\n"
+ ),
+ tracer.leafElementSourceSnippets);
+
+ assertEquals(
+ List.of(
+ "root",
+ " #if 0 == 1",
+ " #if 1 == 1",
+ " text \"Yup.\\n\"",
+ " text \"Always.\\n\"",
+ " #list-#else-container",
+ " #list [1, 2, 3] as item",
+ " ${item}",
+ " ${item}",
+ " ${item}",
+ " #list-#else-container",
+ " #list [] as item",
+ " #else",
+ " text \"Yup.\\n\"",
+ " #list 1..2 as i",
+ " ${i}",
+ " ${i}",
+ " #list 1..3 as j",
+ " ${j}",
+ " #sep",
+ " text \", \"",
+ " ${j}",
+ " #sep",
+ " text \", \"",
+ " ${j}",
+ " #sep",
+ " #foreach k in 1..2",
+ " text \"k=\"",
+ " ${k}",
+ " text \"k=\"",
+ " ${k}",
+ " #attempt",
+ " text \"succeed\"",
+ " #attempt",
+ " #mixed_content",
+ " text \"will fail\"",
+ " ${fail}",
+ " #recover",
+ " text \"recover\"",
+ " @(\"x\"?interpret)",
+ " text \"x\"",
+ " #if-#elseif-#else-container",
+ " #if true",
+ " text \"t\"",
+ " #if-#elseif-#else-container",
+ " #else",
+ " text \"f\"",
+ " #if-#elseif-#else-container",
+ " #else",
+ " text \"f2\"",
+ " #if-#elseif-#else-container",
+ " #elseif true",
+ " text \"t2\"",
+ " #switch 2",
+ " #case 2",
+ " text \"C2\"",
+ " #break",
+ " #switch 3",
+ " #default",
+ " text \"D\"",
+ " #macro m",
+ " text \"Calling macro: \"",
+ " @m",
+ " #macro m",
+ " text \"Hello from m!\"",
+ " #assign t = .nested_output",
+ " text \"captured\"",
+ " text \"\\n\""
+ ),
+ tracer.indentedElementDescriptions);
}
- private static class Tracer implements TemplateProcessingTracer {
- final ArrayList<String> elementsVisited;
- final String[] templateLines;
+ private static class TestTemplateProcessingTracer implements TemplateProcessingTracer {
+ private final List<String> leafElementSourceSnippets = new ArrayList<>();
+ private final List<String> indentedElementDescriptions = new ArrayList<>();
+ private String indentation = null;
- Tracer(String template) {
- elementsVisited = new ArrayList<>();
- templateLines = template.split("\\n");
- }
+ public void enterElement(Environment env, TracedElement tracedElement) {
+ if (indentation == null) {
+ indentation = "";
+ } else {
+ indentation += " ";
+ }
- public void enterElement(Template template, int beginColumn, int beginLine, int endColumn, int endLine,
- boolean isLeafElement) {
- if (isLeafElement) {
- String line = templateLines[beginLine - 1];
- String elementText = line.substring(beginColumn - 1,
- endLine == beginLine ? Math.min(endColumn, line.length()) : line.length());
- elementsVisited.add(elementText);
+ indentedElementDescriptions.add(indentation + tracedElement.getDescription());
+
+ if (tracedElement.isLeaf()) {
+ int beginColumn = tracedElement.getBeginColumn();
+ int beginLine = tracedElement.getBeginLine();
+ int endLine = tracedElement.getEndLine();
+ int endColumn = tracedElement.getEndColumn();
+
+ String suffix;
+ if (beginLine != endLine) {
+ endLine = beginLine;
+ endColumn = Integer.MAX_VALUE;
+ suffix = "[...]";
+ } else {
+ suffix = "";
+ }
+
+ String sourceQuotation = tracedElement.getTemplate()
+ .getSource(beginColumn, beginLine, endColumn, endLine);
+ leafElementSourceSnippets.add(sourceQuotation + suffix);
}
}
- public void exitElement(Template template, int beginColumn, int beginLine, int endColumn, int endLine) {}
+ public void exitElement(Environment env) {
+ indentation = indentation.isEmpty() ? null : indentation.substring(0, indentation.length() - 1);
+ }
}
}