package org.stagemonitor.tracing.freemarker;
import freemarker.core.Environment;
import freemarker.core.Expression;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.description.method.MethodDescription;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.matcher.ElementMatcher;
import net.bytebuddy.matcher.ElementMatchers;
import org.stagemonitor.core.instrument.StagemonitorByteBuddyTransformer;
import org.stagemonitor.tracing.profiler.CallStackElement;
import org.stagemonitor.tracing.profiler.Profiler;
import static net.bytebuddy.matcher.ElementMatchers.named;
import static net.bytebuddy.matcher.ElementMatchers.takesArguments;
/**
* This class "profiles FTL files"
* <p/>
* This class actually adds expressions and methods which are evaluated by freemarker to the call tree. So that when a
* model class of our application is called, we know which FTL file originated the call.
* <p/>
* You may argue that this is not of particular interest because model objects are mostly POJOs and calling a getter
* is not interesting performance wise. This is true for POJOs but some applications may choose to not fully initialize
* the model objects but instead lazy-load the values on demand i.e. if they are actually needed for the template.
* <p/>
* Example:
*
* <pre>
* </code>
* test.ftl:1#templateModel.allTheThings
* `-- String org.example.TemplateModel.getAllTheThings()
* `-- String org.example.ExampleDao.getAllTheThingsFromDB()
* `-- SELECT * from THINGS
* </code>
* </pre>
*
* Note that this will only be active when working with Freemarker versions starting at 2.3.23.
*/
public class FreemarkerProfilingTransformer extends StagemonitorByteBuddyTransformer {
/**
* Application code can be called by freemarker via the {@link freemarker.core.Dot} or the {@link
* freemarker.core.MethodCall} classes.
* <p/>
* For example, when the expression ${templateModel.foo} is evaluated, {@link freemarker.core.Dot#_eval(Environment)}
* evaluates <code>foo</code> by calling <code>TemplateModel#getFoo()</code>.
* <p/>
* The expression ${templateModel.getFoo()} will be evaluated a bit differently as {@link
* freemarker.core.Dot#_eval(Environment)} only returns a reference to the method
* <code>TemplateModel#getFoo()</code> instead of calling it directly. {@link freemarker.core.MethodCall#_eval(Environment)}
* then performs the actual call to <code>TemplateModel#getFoo()</code>.
*/
@Override
protected ElementMatcher.Junction<TypeDescription> getIncludeTypeMatcher() {
return named("freemarker.core.Dot")
.or(named("freemarker.core.MethodCall"));
}
@Override
protected ElementMatcher.Junction<MethodDescription> getExtraMethodElementMatcher() {
return named("_eval").and(takesArguments(Environment.class));
}
@Advice.OnMethodEnter(inline = false)
public static void onBeforeEvaluate(@Advice.Argument(0) Environment env, @Advice.This Expression dot) {
Profiler.start(env.getCurrentTemplate().getName() + ':' + dot.getBeginLine() + '#' + dot.toString());
}
@Advice.OnMethodExit(inline = false, onThrowable = Throwable.class)
public static void onAfterEvaluate() {
final CallStackElement currentFreemarkerCall = Profiler.getMethodCallParent();
Profiler.stop();
removeCurrentNodeIfItHasNoChildren(currentFreemarkerCall);
}
/**
* <pre>
* </code>
* test.ftl:1#templateModel.getFoo() <- added by {@link freemarker.core.MethodCall#_eval(Environment)}
* |-- test.ftl:1#templateModel.getFoo <- added by {@link freemarker.core.Dot#_eval(Environment)}
* `-- String org.stagemonitor.tracing.freemarker.FreemarkerProfilingTest$TemplateModel.getFoo()
* </code>
* </pre>
*
* We want to remove <code>templateModel.getFoo</code> as getFoo only returns the method reference, which is then
* invoked by {@link freemarker.core.MethodCall#_eval(Environment)}.
* Therefore, <code>getFoo</code> does not invoke the model and thus is not relevant for the call tree
*/
private static void removeCurrentNodeIfItHasNoChildren(CallStackElement currentFreemarkerCall) {
if (currentFreemarkerCall != null && currentFreemarkerCall.getChildren().isEmpty()) {
currentFreemarkerCall.remove();
}
}
/**
* Makes sure that this transformer is only applied on Freemarker versions >= 2.3.23 where the
* {@link Environment#getCurrentTemplate()} method was made public. This prevents nasty
* {@link IllegalAccessError}s and {@link NoSuchMethodError}s.
*/
@Override
public boolean isActive() {
try {
return hasMethodThat(named("getCurrentTemplate")
.and(ElementMatchers.<MethodDescription.InDefinedShape>isPublic())
.and(takesArguments(0)))
.matches(new TypeDescription.ForLoadedType(Class.forName("freemarker.core.Environment")));
} catch (ClassNotFoundException e) {
return false;
}
}
private ElementMatcher.Junction<TypeDescription> hasMethodThat(final ElementMatcher<MethodDescription.InDefinedShape> methodElementMatcher) {
return new ElementMatcher.Junction.AbstractBase<TypeDescription>() {
@Override
public boolean matches(TypeDescription target) {
return !target.getDeclaredMethods().filter(methodElementMatcher).isEmpty();
}
};
}
}