diff --git a/agent.iml b/agent.iml
index c297426..62ccc36 100644
--- a/agent.iml
+++ b/agent.iml
@@ -1,12 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
@@ -15,9 +29,39 @@
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index ebc822a..8d8eee2 100644
--- a/pom.xml
+++ b/pom.xml
@@ -19,6 +19,13 @@
javassist
3.21.0-GA
+
+
+ org.nanohttpd
+ nanohttpd
+ 2.3.1
+
+
junit
junit
@@ -32,9 +39,10 @@
test
- org.nanohttpd
- nanohttpd
- 2.3.1
+ org.springframework.boot
+ spring-boot-starter-web
+ 1.5.3.RELEASE
+ test
@@ -44,9 +52,9 @@
- src/deploy/java
+ src/testapp/java
-
+
maven-jar-plugin
3.1.0
diff --git a/src/main/java/perfix/Agent.java b/src/main/java/perfix/Agent.java
index a694313..55a20ca 100644
--- a/src/main/java/perfix/Agent.java
+++ b/src/main/java/perfix/Agent.java
@@ -1,5 +1,6 @@
package perfix;
+import perfix.instrument.Instrumentor;
import perfix.server.HTTPServer;
import java.lang.instrument.Instrumentation;
@@ -16,15 +17,15 @@ public class Agent {
private static final String INCLUDES_PROPERTY = "perfix.includes";
private static final String DEFAULT_PORT = "2048";
- private static final String MESSAGE = " --- Perfix agent active --- ";
+ private static final String MESSAGE = " --- Perfix agent active";
- public static void premain(String agentArgs, Instrumentation inst) {
+ public static void premain(String agentArgs, Instrumentation instrumentation) {
System.out.println(MESSAGE);
int port = Integer.parseInt(System.getProperty(PORT_PROPERTY, DEFAULT_PORT));
- new ClassInstrumentor(determineIncludes()).instrumentCode(inst);
+ Instrumentor.create(determineIncludes()).instrumentCode(instrumentation);
new HTTPServer(port).start();
}
diff --git a/src/main/java/perfix/MethodInvocation.java b/src/main/java/perfix/MethodInvocation.java
index a84976c..7b519af 100644
--- a/src/main/java/perfix/MethodInvocation.java
+++ b/src/main/java/perfix/MethodInvocation.java
@@ -1,11 +1,14 @@
package perfix;
+/**
+ * contains start and stop time for method/query/servlet
+ */
public class MethodInvocation {
private final long t0;
private final String name;
- private long t1;
+ long t1;
- private MethodInvocation(String name) {
+ MethodInvocation(String name) {
t0 = System.nanoTime();
if (name != null) {
this.name = name;
@@ -14,19 +17,6 @@ public class MethodInvocation {
}
}
- public static MethodInvocation start(String name) {
- return new MethodInvocation(name);
- }
-
- public static void stop(MethodInvocation methodInvocation) {
- methodInvocation.stop();
- }
-
- public void stop() {
- t1 = System.nanoTime();
- Registry.add(this);
- }
-
String getName() {
return name;
}
@@ -34,4 +24,5 @@ public class MethodInvocation {
long getDuration() {
return t1 - t0;
}
+
}
diff --git a/src/main/java/perfix/MutableBoolean.java b/src/main/java/perfix/MutableBoolean.java
index 1d9b7f7..d9e5671 100644
--- a/src/main/java/perfix/MutableBoolean.java
+++ b/src/main/java/perfix/MutableBoolean.java
@@ -3,15 +3,15 @@ package perfix;
public class MutableBoolean {
private boolean value;
- MutableBoolean(boolean value) {
+ public MutableBoolean(boolean value) {
this.value = value;
}
- void set(boolean value) {
+ public void set(boolean value) {
this.value = value;
}
- boolean get() {
+ public boolean get() {
return value;
}
diff --git a/src/main/java/perfix/Registry.java b/src/main/java/perfix/Registry.java
index 54fafda..df58cab 100644
--- a/src/main/java/perfix/Registry.java
+++ b/src/main/java/perfix/Registry.java
@@ -1,6 +1,5 @@
package perfix;
-import java.io.PrintStream;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentSkipListMap;
@@ -9,31 +8,26 @@ import java.util.concurrent.atomic.LongAdder;
public class Registry {
private static final Map> methods = new ConcurrentHashMap<>();
- private static final double NANO_2_MILLI = 1000000D;
- private static final String HEADER1 = "Invoked methods, by duration desc:";
- private static final String HEADER2 = "MethodInvocation name;#Invocations;Total duration;Average Duration";
- private static final String FOOTER = "----------------------------------------";
+ private static final Map> callstack = new ConcurrentHashMap<>();
+ private static final ThreadLocal currentMethod = new ThreadLocal<>();
- static void add(MethodInvocation methodInvocation) {
+ @SuppressWarnings("unused")
+ public static MethodInvocation start(String name) {
+ MethodInvocation methodInvocation = new MethodInvocation(name);
+ String parent = currentMethod.get();
+ if (parent != null) {
+ callstack.computeIfAbsent(parent, k -> new HashSet<>()).add(methodInvocation.getName());
+ }
+ currentMethod.set(methodInvocation.getName());
+ return methodInvocation;
+ }
+
+
+ @SuppressWarnings("unused")
+ public static void stop(MethodInvocation methodInvocation) {
+ methodInvocation.t1 = System.nanoTime();
methods.computeIfAbsent(methodInvocation.getName(), key -> new ArrayList<>()).add(methodInvocation);
}
-
- public static void report(PrintStream out) {
- out.println(HEADER1);
- out.println(HEADER2);
- sortedMethodsByDuration().entrySet().stream()
- .map(entry -> createReportLine(entry.getValue()))
- .forEach(out::println);
- out.println(FOOTER);
- out.flush();
- }
-
- private static String createReportLine(Report report) {
- return report.getName() + ";"
- + report.getInvocations() + ";"
- + (long) (report.getTotalDuration() / NANO_2_MILLI) + ";"
- + (long) (report.getAverage() / NANO_2_MILLI);
- }
public static SortedMap sortedMethodsByDuration() {
SortedMap sortedByTotal = new ConcurrentSkipListMap<>(Comparator.reverseOrder());
@@ -46,6 +40,11 @@ public class Registry {
});
return sortedByTotal;
}
+//work in progress
+// public static Map> getCallStack() {
+// callstack.forEach((name,children)->{
+//
+// });
+// }
-
}
diff --git a/src/main/java/perfix/instrument/ClassInstrumentor.java b/src/main/java/perfix/instrument/ClassInstrumentor.java
new file mode 100644
index 0000000..298fa9b
--- /dev/null
+++ b/src/main/java/perfix/instrument/ClassInstrumentor.java
@@ -0,0 +1,105 @@
+package perfix.instrument;
+
+import javassist.CannotCompileException;
+import javassist.ClassPool;
+import javassist.CtClass;
+import javassist.NotFoundException;
+import perfix.MutableBoolean;
+
+import java.io.IOException;
+import java.lang.instrument.Instrumentation;
+import java.util.List;
+
+import static java.util.Arrays.stream;
+
+public class ClassInstrumentor extends Instrumentor {
+
+ private static final String JAVA_INNERCLASS_SEPARATOR = "$";
+
+ JdbcInstrumentor jdbcInstrumentor;
+ ServletInstrumentor servletInstrumentor;
+
+ ClassInstrumentor(List includes, ClassPool classPool) {
+ super(includes, classPool);
+ try {
+ perfixMethodInvocationClass = getCtClass(PERFIX_METHODINVOCATION_CLASS);
+ stringClass = classpool.get(JAVA_STRING);
+
+ } catch (NotFoundException e) {
+ //suppress TODO implement trace
+ }
+ }
+
+ public void instrumentCode(Instrumentation inst) {
+ inst.addTransformer((classLoader, resource, aClass, protectionDomain, uninstrumentedByteCode)
+ -> instrumentCodeForClass(includes, resource, uninstrumentedByteCode));
+ }
+
+ private byte[] instrumentCodeForClass(List includes, String resource, byte[] uninstrumentedByteCode) {
+ if (!isInnerClass(resource)) {
+ try {
+ CtClass ctClass = getCtClassForResource(resource);
+
+ if (servletInstrumentor.isServletImpl(resource)){
+ return servletInstrumentor.instrumentServlet(ctClass, uninstrumentedByteCode);
+ }
+
+ if (jdbcInstrumentor.isJdbcStatementImpl(resource, ctClass)) {
+ return jdbcInstrumentor.instrumentJdbcStatement(ctClass, uninstrumentedByteCode);
+ }
+
+ if (jdbcInstrumentor.isJdbcConnectionImpl(resource, ctClass)) {
+ return jdbcInstrumentor.instrumentJdbcConnection(ctClass, uninstrumentedByteCode);
+ }
+
+ if (jdbcInstrumentor.isJdbcPreparedStatement(resource)) {
+ return jdbcInstrumentor.instrumentJdbcPreparedStatement(ctClass, uninstrumentedByteCode);
+ }
+ if (jdbcInstrumentor.isJdbcPreparedStatementImpl(resource, ctClass)) {
+ return jdbcInstrumentor.instrumentJdbcPreparedStatementImpl(ctClass, uninstrumentedByteCode);
+ }
+ if (shouldInclude(resource, includes)) {
+ return instrumentMethods(ctClass, uninstrumentedByteCode);
+ }
+ } catch (Exception ex) {
+ //suppress
+ }
+ }
+ return uninstrumentedByteCode;
+ }
+
+ /* for regular classes that require instrumentation instrument all methods to record duration*/
+ private byte[] instrumentMethods(CtClass classToInstrument, byte[] uninstrumentedByteCode) {
+ if (!classToInstrument.isInterface()) {
+ stream(classToInstrument.getDeclaredMethods()).forEach(this::instrumentMethod);
+
+ try {
+ return bytecode(classToInstrument);
+ } catch (IOException | CannotCompileException e) {
+ //suppress
+ }
+ }
+ return uninstrumentedByteCode;
+
+ }
+
+ private CtClass getCtClassForResource(String resource) throws NotFoundException {
+ return getCtClass(resource.replaceAll("/", "."));
+ }
+
+ private boolean shouldInclude(String resource, List excludes) {
+ MutableBoolean included = new MutableBoolean(false);
+ excludes.forEach(include -> {
+ if (resource.startsWith(include)) {
+ included.set(true);
+ }
+ });
+ return included.get();
+ }
+
+ private boolean isInnerClass(String resource) {
+ return resource.contains(JAVA_INNERCLASS_SEPARATOR);
+ }
+
+
+}
diff --git a/src/main/java/perfix/instrument/Instrumentor.java b/src/main/java/perfix/instrument/Instrumentor.java
new file mode 100644
index 0000000..981c98d
--- /dev/null
+++ b/src/main/java/perfix/instrument/Instrumentor.java
@@ -0,0 +1,65 @@
+package perfix.instrument;
+
+import javassist.*;
+
+import java.io.IOException;
+import java.lang.instrument.Instrumentation;
+import java.util.List;
+
+public abstract class Instrumentor {
+ static final String JAVA_STRING = "java.lang.String";
+ static final String PERFIX_METHODINVOCATION_CLASS = "perfix.MethodInvocation";
+ static final String JAVASSIST_FIRST_ARGUMENT_NAME = "$1";
+ static final String JAVASSIST_RETURNVALUE = "$_";
+
+ final ClassPool classpool;
+ final List includes;
+ CtClass perfixMethodInvocationClass;
+ CtClass stringClass;
+
+ Instrumentor(List includes, ClassPool classPool) {
+ this.includes = includes;
+ this.classpool = classPool;
+ try {
+ perfixMethodInvocationClass = getCtClass(PERFIX_METHODINVOCATION_CLASS);
+ stringClass = classpool.get(JAVA_STRING);
+
+ } catch (NotFoundException e) {
+ //suppress TODO implement trace
+ }
+ }
+
+ public static Instrumentor create(List includes) {
+ ClassPool classPool = ClassPool.getDefault();
+ ClassInstrumentor classInstrumentor = new ClassInstrumentor(includes, classPool);
+ classInstrumentor.jdbcInstrumentor = new JdbcInstrumentor(includes, classPool);
+ classInstrumentor.servletInstrumentor = new ServletInstrumentor(includes, classPool);
+ return classInstrumentor;
+ }
+
+ public void instrumentCode(Instrumentation inst){}
+
+ void instrumentMethod(CtMethod methodToinstrument) {
+ instrumentMethod(methodToinstrument, "\"" + methodToinstrument.getLongName() + "\"");
+ }
+
+ /* record times at beginning and end of method body*/
+ void instrumentMethod(CtMethod methodToinstrument, String metricName) {
+ try {
+ methodToinstrument.addLocalVariable("_perfixmethod", perfixMethodInvocationClass);
+ methodToinstrument.insertBefore("_perfixmethod = perfix.Registry.start(" + metricName + ");");
+ methodToinstrument.insertAfter("perfix.Registry.stop(_perfixmethod);");
+ } catch (CannotCompileException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ byte[] bytecode(CtClass classToInstrument) throws IOException, CannotCompileException {
+ classToInstrument.detach();
+ return classToInstrument.toBytecode();
+ }
+
+ CtClass getCtClass(String classname) throws NotFoundException {
+ return classpool.get(classname);
+ }
+}
diff --git a/src/main/java/perfix/ClassInstrumentor.java b/src/main/java/perfix/instrument/JdbcInstrumentor.java
similarity index 57%
rename from src/main/java/perfix/ClassInstrumentor.java
rename to src/main/java/perfix/instrument/JdbcInstrumentor.java
index 0c559e7..a85e4dd 100644
--- a/src/main/java/perfix/ClassInstrumentor.java
+++ b/src/main/java/perfix/instrument/JdbcInstrumentor.java
@@ -1,284 +1,174 @@
-package perfix;
-
-import javassist.*;
-
-import java.io.IOException;
-import java.lang.instrument.Instrumentation;
-import java.util.List;
-
-import static java.util.Arrays.stream;
-
-class ClassInstrumentor {
-
- private static final String JAVA_INNERCLASS_SEPARATOR = "$";
- private static final String JAVA_STRING = "java.lang.String";
- private static final String JAVASQL_PACKAGE = "java/sql";
- private static final String JAVASQL_STATEMENT_INTERFACE = "java.sql.Statement";
- private static final String JAVASQL_EXECUTE_METHOD = "execute";
- private static final String JAVASQL_EXECUTEQUERY_METHOD = "executeQuery";
- private static final String JAVASQL_EXECUTEUPDATE_METHOD = "executeUpdate";
- private static final String JAVASQL_PREPAREDSTATEMENT_INTERFACE = "java.sql.PreparedStatement";
- private static final String JAVASQL_CONNECTION_INTERFACE = "java.sql.Connection";
- private static final String JAVASQL_PREPARED_STATEMENT_CLASSNAME = "java.sql.PreparedStatement";
- private static final String JAVASQL_PREPAREDSTATEMENT_RESOURCENAME = "java/sql/PreparedStatement";
- private static final String JAVASQL_PREPARESTATEMENT_METHODNAME = "prepareStatement";
-
- private static final String JAVASSIST_FIRST_ARGUMENT_NAME = "$1";
- private static final String JAVASSIST_RETURNVALUE = "$_";
-
- private static final String PERFIX_METHODINVOCATION_CLASS = "perfix.MethodInvocation";
- private static final String PERFIX_SQLSTATEMENT_FIELD = "_perfixSqlStatement";
- private static final String PERFIX_SETSQL_METHOD = "setSqlForPerfix";
-
- private ClassPool classpool;
- private List includes;
- private CtClass perfixMethodInvocationClass;
- private CtClass stringClass;
-
- ClassInstrumentor(List includes) {
- this.includes = includes;
- this.classpool = ClassPool.getDefault();
- try {
- perfixMethodInvocationClass = getCtClass(PERFIX_METHODINVOCATION_CLASS);
- stringClass = classpool.get(JAVA_STRING);
-
- } catch (NotFoundException e) {
- //suppress TODO implement trace
- }
- }
-
- void instrumentCode(Instrumentation inst) {
- inst.addTransformer((classLoader, resource, aClass, protectionDomain, uninstrumentedByteCode)
- -> instrumentCodeForClass(includes, resource, uninstrumentedByteCode));
- }
-
- private byte[] instrumentCodeForClass(List includes, String resource, byte[] uninstrumentedByteCode) {
- if (!isInnerClass(resource)) {
- try {
- CtClass ctClass = getCtClassForResource(resource);
-
- if (isJdbcStatementImpl(resource, ctClass)) {
- return instrumentJdbcStatement(ctClass, uninstrumentedByteCode);
- }
-
- if (isJdbcConnectionImpl(resource, ctClass)) {
- return instrumentJdbcConnection(ctClass, uninstrumentedByteCode);
- }
-
- if (isJdbcPreparedStatement(resource)) {
- return instrumentJdbcPreparedStatement(ctClass, uninstrumentedByteCode);
- }
- if (isJdbcPreparedStatementImpl(resource, ctClass)) {
- return instrumentJdbcPreparedStatementImpl(ctClass, uninstrumentedByteCode);
- }
- if (shouldInclude(resource, includes)) {
- return instrumentMethods(ctClass, uninstrumentedByteCode);
- }
- } catch (Exception ex) {
- //suppress
- }
- }
- return uninstrumentedByteCode;
- }
-
- /* Need to enhance interface to be able to set a statement (string) for perfix. */
- private byte[] instrumentJdbcPreparedStatement(CtClass preparedStatementInterface, byte[] uninstrumentedByteCode) {
- try {
- preparedStatementInterface.getDeclaredMethod(PERFIX_SETSQL_METHOD);
- } catch (NotFoundException e1) {
- try {
- CtMethod setSqlForPerfix = new CtMethod(CtClass.voidType, PERFIX_SETSQL_METHOD, new CtClass[]{stringClass}, preparedStatementInterface);
- preparedStatementInterface.addMethod(setSqlForPerfix);
-
- } catch (CannotCompileException e2) {
- return uninstrumentedByteCode;
- }
- }
- try {
- return bytecode(preparedStatementInterface);
- } catch (CannotCompileException | IOException e) {
- return uninstrumentedByteCode;
- }
- }
-
- /* Prepared statement methods that actually execute sql don't have the statement in their parameters (unlike java.sql.Statement)
- * instead every jdbc vendor has a specific String field in their PreparedStatement impl that contains the statement.
- *
- * To circumvent vendor specifics in perfix, the first argument for prepareStatement (the sql String) is intercepted here
- * and injected into the PreparedStatement instance under a fixed name, whatever the implementation type */
- private byte[] instrumentJdbcConnection(CtClass classToInstrument, byte[] uninstrumentedByteCode) {
- try {
- stream(classToInstrument.getDeclaredMethods(JAVASQL_PREPARESTATEMENT_METHODNAME)).forEach(method -> {
- try {
- // The sql statement String that is the first argument for this method is injected into PreparedStatementImpl
- // using a name known (only) to perfix, so that it can fetch it later in that class (instance)
- // this way no JDBC implementor specific code is needed
- CtClass preparedStatementInterface = classpool.get(JAVASQL_PREPARED_STATEMENT_CLASSNAME);
-
- try {
- preparedStatementInterface.getDeclaredMethod(PERFIX_SETSQL_METHOD);
- } catch (NotFoundException e1) {
- CtMethod setSqlForPerfix = new CtMethod(CtClass.voidType, PERFIX_SETSQL_METHOD, new CtClass[]{stringClass}, preparedStatementInterface);
- preparedStatementInterface.addMethod(setSqlForPerfix);
- }
-
- method.insertAfter(JAVASSIST_RETURNVALUE + "." + PERFIX_SETSQL_METHOD + "(" + JAVASSIST_FIRST_ARGUMENT_NAME + ");"); //$_ is result instance, $1 is first argument
- } catch (CannotCompileException | NotFoundException e) {
- // suppress
- e.printStackTrace();
- }
- }
- );
- return bytecode(classToInstrument);
- } catch (NotFoundException | CannotCompileException | IOException e) {
- // suppress
- }
- return uninstrumentedByteCode;
- }
-
- /* */
- private byte[] instrumentJdbcPreparedStatementImpl(CtClass preparedStatementClass, byte[] uninstrumentedByteCode) {
- try {
- addPerfixStatementField(preparedStatementClass);
- addPerfixStatementSetter(preparedStatementClass);
-
- // instrument execute to record query duration
- stream(preparedStatementClass.getDeclaredMethods(JAVASQL_EXECUTE_METHOD)).forEach(method ->
- instrumentMethod(method, PERFIX_SQLSTATEMENT_FIELD)
- );
-
- // instrument executeQuery to record query duration
- stream(preparedStatementClass.getDeclaredMethods(JAVASQL_EXECUTEQUERY_METHOD)).forEach(method ->
- instrumentMethod(method, PERFIX_SQLSTATEMENT_FIELD)
- );
-
- // instrument executeUpdate to record query duration
- stream(preparedStatementClass.getDeclaredMethods(JAVASQL_EXECUTEUPDATE_METHOD)).forEach(method ->
- instrumentMethod(method, PERFIX_SQLSTATEMENT_FIELD)
- );
-
- return bytecode(preparedStatementClass);
- } catch (NotFoundException | CannotCompileException | IOException e) {
- e.printStackTrace();
- return uninstrumentedByteCode;
- }
- }
-
-
- private byte[] instrumentJdbcStatement(CtClass classToInstrument, byte[] uninstrumentedByteCode) {
- try {
- //instrument executeQuery to record query duration
- stream(classToInstrument.getDeclaredMethods(JAVASQL_EXECUTEQUERY_METHOD)).forEach(method ->
- instrumentMethod(method, JAVASSIST_FIRST_ARGUMENT_NAME)
- );
- //instrument executeUpdate to record query duration
- stream(classToInstrument.getDeclaredMethods(JAVASQL_EXECUTEUPDATE_METHOD)).forEach(method ->
- instrumentMethod(method, JAVASSIST_FIRST_ARGUMENT_NAME)
- );
- return bytecode(classToInstrument);
- } catch (Exception e) {
- return uninstrumentedByteCode;
- }
-
- }
-
- /* for regular classes that require instrumentation instrument all methods to record duration*/
- private byte[] instrumentMethods(CtClass classToInstrument, byte[] uninstrumentedByteCode) {
- if (!classToInstrument.isInterface()) {
- stream(classToInstrument.getDeclaredMethods()).forEach(this::instrumentMethod);
-
- try {
- return bytecode(classToInstrument);
- } catch (IOException | CannotCompileException e) {
- //suppress
- }
- }
- return uninstrumentedByteCode;
-
- }
-
- private CtClass getCtClassForResource(String resource) throws NotFoundException {
- return getCtClass(resource.replaceAll("/", "."));
- }
-
- private CtClass getCtClass(String classname) throws NotFoundException {
- return classpool.get(classname);
- }
-
- private void instrumentMethod(CtMethod methodToinstrument) {
- instrumentMethod(methodToinstrument, "\"" + methodToinstrument.getLongName() + "\"");
- }
-
- /* record times at beginning and end of method body*/
- private void instrumentMethod(CtMethod methodToinstrument, String metricName) {
- try {
- methodToinstrument.addLocalVariable("_perfixmethod", perfixMethodInvocationClass);
- methodToinstrument.insertBefore("_perfixmethod = perfix.MethodInvocation.start(" + metricName + ");");
- methodToinstrument.insertAfter("perfix.MethodInvocation.stop(_perfixmethod);");
- } catch (CannotCompileException e) {
- throw new RuntimeException(e);
- }
- }
-
- private boolean shouldInclude(String resource, List excludes) {
- MutableBoolean included = new MutableBoolean(false);
- excludes.forEach(include -> {
- if (resource.startsWith(include)) {
- included.set(true);
- }
- });
- return included.get();
- }
-
- private boolean isInnerClass(String resource) {
- return resource.contains(JAVA_INNERCLASS_SEPARATOR);
- }
-
- private boolean isJdbcStatementImpl(String resource, CtClass ctClass) throws NotFoundException {
- if (!resource.startsWith(JAVASQL_PACKAGE)) {
- return stream(ctClass.getInterfaces())
- .anyMatch(i -> i.getName().equals(JAVASQL_STATEMENT_INTERFACE) && !i.getName().equals(JAVASQL_PREPARED_STATEMENT_CLASSNAME));
- }
- return false;
- }
-
- private boolean isJdbcPreparedStatement(String resource) {
- return resource.equals(JAVASQL_PREPAREDSTATEMENT_RESOURCENAME);
- }
-
- private boolean isJdbcPreparedStatementImpl(String resource, CtClass ctClass) throws NotFoundException {
- if (!resource.startsWith(JAVASQL_PACKAGE)) {
- return stream(ctClass.getInterfaces())
- .anyMatch(i -> i.getName().equals(JAVASQL_PREPAREDSTATEMENT_INTERFACE));
- }
- return false;
- }
-
- private boolean isJdbcConnectionImpl(String resource, CtClass ctClass) throws NotFoundException {
- if (!resource.startsWith(JAVASQL_PACKAGE)) {
- return stream(ctClass.getInterfaces())
- .anyMatch(i -> i.getName().equals(JAVASQL_CONNECTION_INTERFACE));
- }
- return false;
- }
-
- private byte[] bytecode(CtClass classToInstrument) throws IOException, CannotCompileException {
- classToInstrument.detach();
- return classToInstrument.toBytecode();
- }
-
- private void addPerfixStatementField(CtClass preparedStatementClass) throws CannotCompileException {
- // add a String field that will contain the statement
- CtField perfixSqlField = new CtField(stringClass, PERFIX_SQLSTATEMENT_FIELD, preparedStatementClass);
- perfixSqlField.setModifiers(Modifier.PRIVATE);
- preparedStatementClass.addField(perfixSqlField);
- }
-
- private void addPerfixStatementSetter(CtClass preparedStatementImplClass) throws CannotCompileException {
- // add setter for the new field
- CtMethod setSqlForPerfix = new CtMethod(CtClass.voidType, PERFIX_SETSQL_METHOD, new CtClass[]{stringClass}, preparedStatementImplClass);
- setSqlForPerfix.setModifiers(Modifier.PUBLIC);
- setSqlForPerfix.setBody(PERFIX_SQLSTATEMENT_FIELD + "=" + JAVASSIST_FIRST_ARGUMENT_NAME + ";");
- preparedStatementImplClass.addMethod(setSqlForPerfix);
- }
-}
+package perfix.instrument;
+
+import javassist.*;
+
+import java.io.IOException;
+import java.util.List;
+
+import static java.util.Arrays.stream;
+
+public class JdbcInstrumentor extends Instrumentor {
+
+ private static final String JAVASQL_EXECUTE_METHOD = "execute";
+ private static final String JAVASQL_EXECUTEQUERY_METHOD = "executeQuery";
+ private static final String JAVASQL_EXECUTEUPDATE_METHOD = "executeUpdate";
+ private static final String JAVASQL_PACKAGE = "java/sql";
+ private static final String JAVASQL_STATEMENT_INTERFACE = "java.sql.Statement";
+ private static final String JAVASQL_PREPAREDSTATEMENT_INTERFACE = "java.sql.PreparedStatement";
+ private static final String JAVASQL_CONNECTION_INTERFACE = "java.sql.Connection";
+ private static final String JAVASQL_PREPAREDSTATEMENT_RESOURCENAME = "java/sql/PreparedStatement";
+ private static final String JAVASQL_PREPARED_STATEMENT_CLASSNAME = "java.sql.PreparedStatement";
+ private static final String JAVASQL_PREPARESTATEMENT_METHODNAME = "prepareStatement";
+
+ private static final String PERFIX_SQLSTATEMENT_FIELD = "_perfixSqlStatement";
+ private static final String PERFIX_SETSQL_METHOD = "setSqlForPerfix";
+
+
+ JdbcInstrumentor(List includes, ClassPool classPool) {
+ super(includes, classPool);
+ }
+
+ /* Need to enhance interface to be able to set a statement (string) for perfix. */
+ byte[] instrumentJdbcPreparedStatement(CtClass preparedStatementInterface, byte[] uninstrumentedByteCode) {
+ try {
+ preparedStatementInterface.getDeclaredMethod(PERFIX_SETSQL_METHOD);
+ } catch (NotFoundException e1) {
+ try {
+ CtMethod setSqlForPerfix = new CtMethod(CtClass.voidType, PERFIX_SETSQL_METHOD, new CtClass[]{stringClass}, preparedStatementInterface);
+ preparedStatementInterface.addMethod(setSqlForPerfix);
+
+ } catch (CannotCompileException e2) {
+ return uninstrumentedByteCode;
+ }
+ }
+ try {
+ return bytecode(preparedStatementInterface);
+ } catch (CannotCompileException | IOException e) {
+ return uninstrumentedByteCode;
+ }
+ }
+
+ /* Prepared statement methods that actually execute sql don't have the statement in their parameters (unlike java.sql.Statement)
+ * instead every jdbc vendor has a specific String field in their PreparedStatement impl that contains the statement.
+ *
+ * To circumvent vendor specifics in perfix, the first argument for prepareStatement (the sql String) is intercepted here
+ * and injected into the PreparedStatement instance under a fixed name, whatever the implementation type */
+ byte[] instrumentJdbcConnection(CtClass classToInstrument, byte[] uninstrumentedByteCode) {
+ try {
+ stream(classToInstrument.getDeclaredMethods(JAVASQL_PREPARESTATEMENT_METHODNAME)).forEach(method -> {
+ try {
+ // The sql statement String that is the first argument for this method is injected into PreparedStatementImpl
+ // using a name known (only) to perfix, so that it can fetch it later in that class (instance)
+ // this way no JDBC implementor specific code is needed
+ CtClass preparedStatementInterface = classpool.get(JAVASQL_PREPARED_STATEMENT_CLASSNAME);
+
+ try {
+ preparedStatementInterface.getDeclaredMethod(PERFIX_SETSQL_METHOD);
+ } catch (NotFoundException e1) {
+ CtMethod setSqlForPerfix = new CtMethod(CtClass.voidType, PERFIX_SETSQL_METHOD, new CtClass[]{stringClass}, preparedStatementInterface);
+ preparedStatementInterface.addMethod(setSqlForPerfix);
+ }
+
+ method.insertAfter(JAVASSIST_RETURNVALUE + "." + PERFIX_SETSQL_METHOD + "(" + JAVASSIST_FIRST_ARGUMENT_NAME + ");"); //$_ is result instance, $1 is first argument
+ } catch (CannotCompileException | NotFoundException e) {
+ // suppress
+ e.printStackTrace();
+ }
+ }
+ );
+ return bytecode(classToInstrument);
+ } catch (NotFoundException | CannotCompileException | IOException e) {
+ // suppress
+ }
+ return uninstrumentedByteCode;
+ }
+
+ /* */
+ byte[] instrumentJdbcPreparedStatementImpl(CtClass preparedStatementClass, byte[] uninstrumentedByteCode) {
+ try {
+ addPerfixStatementField(preparedStatementClass);
+ addPerfixStatementSetter(preparedStatementClass);
+
+ // instrument execute to record query duration
+ stream(preparedStatementClass.getDeclaredMethods(JAVASQL_EXECUTE_METHOD)).forEach(method ->
+ instrumentMethod(method, PERFIX_SQLSTATEMENT_FIELD)
+ );
+
+ // instrument executeQuery to record query duration
+ stream(preparedStatementClass.getDeclaredMethods(JAVASQL_EXECUTEQUERY_METHOD)).forEach(method ->
+ instrumentMethod(method, PERFIX_SQLSTATEMENT_FIELD)
+ );
+
+ // instrument executeUpdate to record query duration
+ stream(preparedStatementClass.getDeclaredMethods(JAVASQL_EXECUTEUPDATE_METHOD)).forEach(method ->
+ instrumentMethod(method, PERFIX_SQLSTATEMENT_FIELD)
+ );
+
+ return bytecode(preparedStatementClass);
+ } catch (NotFoundException | CannotCompileException | IOException e) {
+ e.printStackTrace();
+ return uninstrumentedByteCode;
+ }
+ }
+
+
+ byte[] instrumentJdbcStatement(CtClass classToInstrument, byte[] uninstrumentedByteCode) {
+ try {
+ //instrument executeQuery to record query duration
+ stream(classToInstrument.getDeclaredMethods(JAVASQL_EXECUTEQUERY_METHOD)).forEach(method ->
+ instrumentMethod(method, JAVASSIST_FIRST_ARGUMENT_NAME)
+ );
+ //instrument executeUpdate to record query duration
+ stream(classToInstrument.getDeclaredMethods(JAVASQL_EXECUTEUPDATE_METHOD)).forEach(method ->
+ instrumentMethod(method, JAVASSIST_FIRST_ARGUMENT_NAME)
+ );
+ return bytecode(classToInstrument);
+ } catch (Exception e) {
+ return uninstrumentedByteCode;
+ }
+
+ }
+
+ private void addPerfixStatementField(CtClass preparedStatementClass) throws CannotCompileException {
+ // add a String field that will contain the statement
+ CtField perfixSqlField = new CtField(stringClass, PERFIX_SQLSTATEMENT_FIELD, preparedStatementClass);
+ perfixSqlField.setModifiers(Modifier.PRIVATE);
+ preparedStatementClass.addField(perfixSqlField);
+ }
+
+ private void addPerfixStatementSetter(CtClass preparedStatementImplClass) throws CannotCompileException {
+ // add setter for the new field
+ CtMethod setSqlForPerfix = new CtMethod(CtClass.voidType, PERFIX_SETSQL_METHOD, new CtClass[]{stringClass}, preparedStatementImplClass);
+ setSqlForPerfix.setModifiers(Modifier.PUBLIC);
+ setSqlForPerfix.setBody(PERFIX_SQLSTATEMENT_FIELD + "=" + JAVASSIST_FIRST_ARGUMENT_NAME + ";");
+ preparedStatementImplClass.addMethod(setSqlForPerfix);
+ }
+
+ boolean isJdbcStatementImpl(String resource, CtClass ctClass) throws NotFoundException {
+ if (!resource.startsWith(JAVASQL_PACKAGE)) {
+ return stream(ctClass.getInterfaces())
+ .anyMatch(i -> i.getName().equals(JAVASQL_STATEMENT_INTERFACE) && !i.getName().equals(JAVASQL_PREPARED_STATEMENT_CLASSNAME));
+ }
+ return false;
+ }
+
+ boolean isJdbcPreparedStatement(String resource) {
+ return resource.equals(JAVASQL_PREPAREDSTATEMENT_RESOURCENAME);
+ }
+
+ boolean isJdbcPreparedStatementImpl(String resource, CtClass ctClass) throws NotFoundException {
+ if (!resource.startsWith(JAVASQL_PACKAGE)) {
+ return stream(ctClass.getInterfaces())
+ .anyMatch(i -> i.getName().equals(JAVASQL_PREPAREDSTATEMENT_INTERFACE));
+ }
+ return false;
+ }
+
+ boolean isJdbcConnectionImpl(String resource, CtClass ctClass) throws NotFoundException {
+ if (!resource.startsWith(JAVASQL_PACKAGE)) {
+ return stream(ctClass.getInterfaces())
+ .anyMatch(i -> i.getName().equals(JAVASQL_CONNECTION_INTERFACE));
+ }
+ return false;
+ }
+}
diff --git a/src/main/java/perfix/instrument/ServletInstrumentor.java b/src/main/java/perfix/instrument/ServletInstrumentor.java
new file mode 100644
index 0000000..35a8f98
--- /dev/null
+++ b/src/main/java/perfix/instrument/ServletInstrumentor.java
@@ -0,0 +1,41 @@
+package perfix.instrument;
+
+import javassist.CannotCompileException;
+import javassist.ClassPool;
+import javassist.CtClass;
+import javassist.NotFoundException;
+
+import java.util.List;
+
+import static java.util.Arrays.stream;
+
+public class ServletInstrumentor extends Instrumentor {
+
+ private static final String JAVA_SERVLET_SERVICE_METHOD = "service";
+ private static final String JAVA_SERVLET_CLASS = "javax/servlet/http/HttpServlet";
+
+ ServletInstrumentor(List includes, ClassPool classPool) {
+ super(includes, classPool);
+ }
+
+ public boolean isServletImpl(String resource) {
+ return resource.equals(JAVA_SERVLET_CLASS);
+ }
+
+ public byte[] instrumentServlet(CtClass classToInstrument, byte[] uninstrumentedByteCode) {
+ try {
+ stream(classToInstrument.getDeclaredMethods(JAVA_SERVLET_SERVICE_METHOD)).forEach(methodToInstrument -> {
+ try {
+ methodToInstrument.addLocalVariable("_perfixmethod", perfixMethodInvocationClass);
+ methodToInstrument.insertBefore("_perfixmethod = perfix.Registry.start($1.getRequestURI());");
+ methodToInstrument.insertAfter("perfix.Registry.stop(_perfixmethod);");
+ } catch (CannotCompileException e) {
+
+ }
+ });
+ return bytecode(classToInstrument);
+ } catch (Exception e) {
+ return uninstrumentedByteCode;
+ }
+ }
+}
diff --git a/src/main/java/perfix/server/HTTPServer.java b/src/main/java/perfix/server/HTTPServer.java
index 2828880..992c791 100644
--- a/src/main/java/perfix/server/HTTPServer.java
+++ b/src/main/java/perfix/server/HTTPServer.java
@@ -19,19 +19,22 @@ public class HTTPServer extends NanoHTTPD {
try {
start(NanoHTTPD.SOCKET_READ_TIMEOUT, false);
- System.out.println("\nHttpServer running! Point your browser to http://localhost:2048/ \n");
+ System.out.println(" --- Perfix http server running. Point your browser to http://localhost:" + getListeningPort() + "/");
} catch (IOException ioe) {
- System.err.println("Couldn't start server:\n" + ioe);
+ System.err.println(" --- Couldn't start Perfix http server:\n" + ioe);
}
}
@Override
public Response serve(IHTTPSession session) {
String uri = session.getUri();
- if (uri.equals("/report")) {
- return perfixMetrics();
- } else {
- return serveStaticContent(uri);
+ switch (uri) {
+ case "/report":
+ return perfixMetrics();
+ case "/callstack":
+ return perfixCallstack();
+ default:
+ return serveStaticContent(uri);
}
}
@@ -59,6 +62,15 @@ public class HTTPServer extends NanoHTTPD {
}
}
+ private Response perfixCallstack() {
+ try {
+ return newFixedLengthResponse(Response.Status.OK, "application/json", Serializer.toJSONString(Registry.getCallStack()));
+ } catch (Exception e) {
+ e.printStackTrace();
+ return newFixedLengthResponse(e.toString());
+ }
+ }
+
private String readFile(InputStream stream) throws IOException {
int nbytes = stream.available();
byte[] bytes = new byte[nbytes];
diff --git a/src/main/java/perfix/server/json/SynthSerializerFactory.java b/src/main/java/perfix/server/json/SynthSerializerFactory.java
index 2b324e3..d55462b 100644
--- a/src/main/java/perfix/server/json/SynthSerializerFactory.java
+++ b/src/main/java/perfix/server/json/SynthSerializerFactory.java
@@ -1,21 +1,10 @@
package perfix.server.json;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
+import javassist.*;
-import javassist.CannotCompileException;
-import javassist.ClassPool;
-import javassist.CtClass;
-import javassist.CtField;
-import javassist.CtMethod;
-import javassist.CtNewMethod;
-import javassist.Modifier;
-import javassist.NotFoundException;
+import java.util.*;
+
+import static java.util.Arrays.asList;
public class SynthSerializerFactory implements SerializerFactory {
private static final String STRING = "java.lang.String";
@@ -28,25 +17,29 @@ public class SynthSerializerFactory implements SerializerFactory {
private static final String SHORT = "java.lang.Short";
private static final String INTEGER = "java.lang.Integer";
- private final static Set wrappersAndString = new HashSet(Arrays.asList(BOOLEAN, CHARACTER, BYTE, DOUBLE, FLOAT, LONG, SHORT, INTEGER,
+ private final static Set wrappersAndString = new HashSet(asList(BOOLEAN, CHARACTER, BYTE, DOUBLE, FLOAT, LONG, SHORT, INTEGER,
STRING));
private static final String COLLECTION = "java.util.Collection";
private static final String LIST = "java.util.List";
private static final String SET = "java.util.Set";
- private static final String MAP = "java.util.Map";
+ private static final List mapInterfaces = asList("java.util.Map", "java.util.concurrent.ConcurrentHashMap");
private static final Map> serializers = new HashMap<>();
private static final String ROOT_PACKAGE = "serializer.";
private final ClassPool pool = ClassPool.getDefault();
- private CtClass serializerBase;
private final Map primitiveWrappers = new HashMap();
+ private CtClass serializerBase;
- public SynthSerializerFactory() {
+ SynthSerializerFactory() {
init();
}
+ private static boolean isPrimitiveOrWrapperOrString(CtClass beanClass) {
+ return beanClass.isPrimitive() || wrappersAndString.contains(beanClass.getName());
+ }
+
void init() {
try {
serializerBase = pool.get(JSONSerializer.class.getName());
@@ -68,9 +61,7 @@ public class SynthSerializerFactory implements SerializerFactory {
try {
CtClass beanClass = pool.get(beanjavaClass.getName());
- JSONSerializer jsonSerializer = createSerializer(beanClass);
-
- return jsonSerializer;
+ return createSerializer(beanClass);
} catch (NotFoundException e) {
throw new SerializerCreationException(e);
}
@@ -104,7 +95,8 @@ public class SynthSerializerFactory implements SerializerFactory {
* create method source, compile it and add it to the class under construction
*/
private void addToJsonStringMethod(CtClass beanClass, CtClass serializerClass) throws NotFoundException, CannotCompileException {
- serializerClass.addMethod(CtNewMethod.make(createToJSONStringMethodSource(beanClass), serializerClass));
+ String body = createToJSONStringMethodSource(beanClass);
+ serializerClass.addMethod(CtNewMethod.make(body, serializerClass));
}
/*
@@ -133,9 +125,9 @@ public class SynthSerializerFactory implements SerializerFactory {
/*
* Any Collection is converted to an array, after which code is generated to handle the single elements.
- *
+ *
* A subserializer is created for every single element, but most of the time it will be the same cached instance.
- *
+ *
* The generated code fills a StringBuilder. The values are generated by the subserializers
*/
private String handleArray(CtClass beanClass) {
@@ -192,7 +184,7 @@ public class SynthSerializerFactory implements SerializerFactory {
/*
* custom root package is prepended to avoid the java.lang class in which it's illegal to create new classes
- *
+ *
* Array marks ( '[]' ) are replaced by the 'Array', Otherwise the SerializerClassName would be syntactically incorrect
*/
public String createSerializerName(CtClass beanClass) {
@@ -204,7 +196,7 @@ public class SynthSerializerFactory implements SerializerFactory {
}
private boolean isCollection(CtClass beanClass) throws NotFoundException {
- List interfaces = new ArrayList(Arrays.asList(beanClass.getInterfaces()));
+ List interfaces = new ArrayList(asList(beanClass.getInterfaces()));
interfaces.add(beanClass);
for (CtClass interfaze : interfaces) {
if (interfaze.getName().equals(COLLECTION) || interfaze.getName().equals(LIST) || interfaze.getName().equals(SET)) {
@@ -215,14 +207,9 @@ public class SynthSerializerFactory implements SerializerFactory {
}
private boolean isMap(CtClass beanClass) throws NotFoundException {
- List interfaces = new ArrayList(Arrays.asList(beanClass.getInterfaces()));
+ List interfaces = new ArrayList<>(asList(beanClass.getInterfaces()));
interfaces.add(beanClass);
- for (CtClass interfaze : interfaces) {
- if (interfaze.getName().equals(MAP)) {
- return true;
- }
- }
- return false;
+ return interfaces.stream().anyMatch(i -> mapInterfaces.contains(i.getName()));
}
/*
@@ -346,22 +333,22 @@ public class SynthSerializerFactory implements SerializerFactory {
String asPrimitive(String name) {
switch (name) {
- case "int":
- return "I";
- case "byte":
- return "B";
- case "float":
- return "F";
- case "long":
- return "J";
- case "boolean":
- return "Z";
- case "char":
- return "C";
- case "double":
- return "D";
- case "short":
- return "S";
+ case "int":
+ return "I";
+ case "byte":
+ return "B";
+ case "float":
+ return "F";
+ case "long":
+ return "J";
+ case "boolean":
+ return "Z";
+ case "char":
+ return "C";
+ case "double":
+ return "D";
+ case "short":
+ return "S";
}
return "";
}
@@ -369,8 +356,4 @@ public class SynthSerializerFactory implements SerializerFactory {
String innerClassName(String name) {
return "L" + name.replaceAll("\\.", "/").replaceAll("\\[\\]", "");
}
-
- static boolean isPrimitiveOrWrapperOrString(CtClass beanClass) {
- return beanClass.isPrimitive() || wrappersAndString.contains(beanClass.getName());
- }
}
diff --git a/src/main/resources/index.html b/src/main/resources/index.html
index b4ca674..4d49e1f 100644
--- a/src/main/resources/index.html
+++ b/src/main/resources/index.html
@@ -10,9 +10,14 @@