html interface

This commit is contained in:
Sander Hautvast 2018-05-19 00:48:00 +02:00
parent 5049459bbc
commit 678b122c33
24 changed files with 784 additions and 234 deletions

View file

@ -2,15 +2,13 @@
Pretty basic profiling tool for JVM's Pretty basic profiling tool for JVM's
# Highlights: # Highlights:
* Provides method and SQL statement execution times. (Somehow it gives me more info than Java Mission Control)
* Meant for development time (after process stops, data is gone). * Meant for development time (after process stops, data is gone).
* Minimalistic commandline interface (ssh). * Minimal memory footprint (agent is 2.5 mb).
* Minimalistic commandline interface.
* Execution time is measured in nanoseconds, reported in milliseconds (this way the totals and averages are most precise, but also human readable). * Execution time is measured in nanoseconds, reported in milliseconds (this way the totals and averages are most precise, but also human readable).
* No manual instrumentation necessary using loadtime bytecode manipulation (javassist). * No manual instrumentation necessary using loadtime bytecode manipulation (javassist).
* No special jdbc configuration necessary (ie no wrapped jdbc driver). * No special jdbc configuration necessary (ie no wrapped jdbc driver).
* The agent is also the server (unlike commercial tooling). This way there is no overhead in interprocess communication. * The agent is also the server (unlike commercial tooling). This way there is no overhead in interprocess communication.
* Minimal memory footprint (agent is >only< 2.5 mb, it's still java right?).
* Overhead (in method execution time) not clear yet. I wouldn't use it in production.
# Usage # Usage
* Agent that instruments loaded classes: -javaagent:<path>/perfix.jar * Agent that instruments loaded classes: -javaagent:<path>/perfix.jar
@ -19,11 +17,12 @@ Pretty basic profiling tool for JVM's
<br/> * #invocations <br/> * #invocations
<br/> * total execution time for the method in nanoseconds (this is also the sorting order) <br/> * total execution time for the method in nanoseconds (this is also the sorting order)
<br/> * average time in nanoseconds per method (= total/#invocations) <br/> * average time in nanoseconds per method (= total/#invocations)
* The ssh server starts on port 2048 by default. Use -Dperfix.port=... to adjust. * The (ssh) server starts on port 2048 by default. Use -Dperfix.port=... to adjust.
# roadmap # roadmap
* Finish jdbc query recording (CallableStatement) * Overhead (in method execution time) not clear yet. I wouldn't use it in production.
* Finish jdbc query logging
* Make output format configurable * Make output format configurable
* Implement password login (now any) * Implement password login (now any)
* Add web interface (maybe) * Add web interface (maybe)

View file

@ -8,6 +8,7 @@
<sourceFolder url="file://$MODULE_DIR$/src/test/java" isTestSource="true" /> <sourceFolder url="file://$MODULE_DIR$/src/test/java" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/deploy/java" isTestSource="true" /> <sourceFolder url="file://$MODULE_DIR$/src/deploy/java" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/test/resources" type="java-test-resource" /> <sourceFolder url="file://$MODULE_DIR$/src/test/resources" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/main/resources" type="java-resource" />
<excludeFolder url="file://$MODULE_DIR$/target" /> <excludeFolder url="file://$MODULE_DIR$/target" />
</content> </content>
<orderEntry type="inheritedJdk" /> <orderEntry type="inheritedJdk" />
@ -17,8 +18,6 @@
<orderEntry type="library" scope="TEST" name="Maven: junit:junit:4.12" level="project" /> <orderEntry type="library" scope="TEST" name="Maven: junit:junit:4.12" level="project" />
<orderEntry type="library" scope="TEST" name="Maven: org.hamcrest:hamcrest-core:1.3" level="project" /> <orderEntry type="library" scope="TEST" name="Maven: org.hamcrest:hamcrest-core:1.3" level="project" />
<orderEntry type="library" scope="TEST" name="Maven: com.h2database:h2:1.4.195" level="project" /> <orderEntry type="library" scope="TEST" name="Maven: com.h2database:h2:1.4.195" level="project" />
<orderEntry type="library" name="Maven: org.apache.sshd:sshd-core:1.7.0" level="project" /> <orderEntry type="library" name="Maven: org.nanohttpd:nanohttpd:2.3.1" level="project" />
<orderEntry type="library" name="Maven: org.slf4j:slf4j-api:1.7.25" level="project" />
<orderEntry type="library" name="Maven: org.slf4j:slf4j-nop:1.7.25" level="project" />
</component> </component>
</module> </module>

11
pom.xml
View file

@ -32,14 +32,9 @@
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.apache.sshd</groupId> <groupId>org.nanohttpd</groupId>
<artifactId>sshd-core</artifactId> <artifactId>nanohttpd</artifactId>
<version>1.7.0</version> <version>2.3.1</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-nop</artifactId>
<version>1.7.25</version>
</dependency> </dependency>
</dependencies> </dependencies>

View file

@ -13,7 +13,6 @@ public class Main {
System.out.println("Exiting now"); System.out.println("Exiting now");
System.exit(0); System.exit(0);
} }
System.out.println("Now start ssh session on localhost on port 2048 (without closing the Test Application)");
run(); run();
} }

View file

@ -1,9 +1,7 @@
package perfix; package perfix;
import javassist.*; import perfix.server.HTTPServer;
import perfix.server.SSHServer;
import java.io.IOException;
import java.lang.instrument.Instrumentation; import java.lang.instrument.Instrumentation;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
@ -28,7 +26,7 @@ public class Agent {
new ClassInstrumentor(determineIncludes()).instrumentCode(inst); new ClassInstrumentor(determineIncludes()).instrumentCode(inst);
new SSHServer().startListeningOnSocket(port); new HTTPServer(port).start();
} }
private static List<String> determineIncludes() { private static List<String> determineIncludes() {

View file

@ -10,7 +10,7 @@ public class MethodInvocation {
if (name != null) { if (name != null) {
this.name = name; this.name = name;
} else { } else {
this.name = "<UNKNOWN_BECAUSE_OF_ERROR>"; this.name = "<error occurred>";
} }
} }

View file

@ -17,7 +17,7 @@ public class Registry {
static void add(MethodInvocation methodInvocation) { static void add(MethodInvocation methodInvocation) {
methods.computeIfAbsent(methodInvocation.getName(), key -> new ArrayList<>()).add(methodInvocation); methods.computeIfAbsent(methodInvocation.getName(), key -> new ArrayList<>()).add(methodInvocation);
} }
public static void report(PrintStream out) { public static void report(PrintStream out) {
out.println(HEADER1); out.println(HEADER1);
out.println(HEADER2); out.println(HEADER2);
@ -29,13 +29,13 @@ public class Registry {
} }
private static String createReportLine(Report report) { private static String createReportLine(Report report) {
return report.name + ";" return report.getName() + ";"
+ report.invocations + ";" + report.getInvocations() + ";"
+ (long) (report.totalDuration / NANO_2_MILLI) + ";" + (long) (report.getTotalDuration() / NANO_2_MILLI) + ";"
+ (long) (report.average() / NANO_2_MILLI); + (long) (report.getAverage() / NANO_2_MILLI);
} }
private static SortedMap<Long, Report> sortedMethodsByDuration() { public static SortedMap<Long, Report> sortedMethodsByDuration() {
SortedMap<Long, Report> sortedByTotal = new ConcurrentSkipListMap<>(Comparator.reverseOrder()); SortedMap<Long, Report> sortedByTotal = new ConcurrentSkipListMap<>(Comparator.reverseOrder());
methods.forEach((name, measurements) -> { methods.forEach((name, measurements) -> {
LongAdder totalDuration = new LongAdder(); LongAdder totalDuration = new LongAdder();
@ -47,19 +47,5 @@ public class Registry {
return sortedByTotal; return sortedByTotal;
} }
static class Report {
final String name;
final int invocations;
final long totalDuration;
Report(String name, int invocations, long totalDuration) {
this.name = name;
this.invocations = invocations;
this.totalDuration = totalDuration;
}
double average() {
return (double) totalDuration / invocations;
}
}
} }

View file

@ -0,0 +1,31 @@
package perfix;
public class Report {
private final String name;
private final int invocations;
private final long totalDuration;
private final double average;
Report(String name, int invocations, long totalDuration) {
this.name = name;
this.invocations = invocations;
this.totalDuration = totalDuration;
this.average = (double) totalDuration / invocations;
}
public double getAverage() {
return average;
}
public int getInvocations() {
return invocations;
}
public long getTotalDuration() {
return totalDuration;
}
public String getName() {
return name;
}
}

View file

@ -0,0 +1,78 @@
package perfix.server;
import fi.iki.elonen.NanoHTTPD;
import perfix.Registry;
import perfix.server.json.Serializer;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
public class HTTPServer extends NanoHTTPD {
public HTTPServer(int port) {
super(port);
}
public void start() {
try {
start(NanoHTTPD.SOCKET_READ_TIMEOUT, false);
System.out.println("\nHttpServer running! Point your browser to http://localhost:2048/ \n");
} catch (IOException ioe) {
System.err.println("Couldn't start server:\n" + ioe);
}
}
@Override
public Response serve(IHTTPSession session) {
String uri = session.getUri();
if (uri.equals("/report")) {
return perfixMetrics();
} else {
return serveStaticContent(uri);
}
}
private Response serveStaticContent(String uri) {
if (uri.equals("/")) {
uri = "/index.html";
}
try {
InputStream stream = getClass().getResourceAsStream(uri);
if (stream == null) {
return newFixedLengthResponse(Response.Status.NOT_FOUND, "text/plain", "resource not found");
}
return newFixedLengthResponse(Response.Status.OK, determineContentType(uri), readFile(stream));
} catch (IOException e) {
return newFixedLengthResponse(e.toString());
}
}
private Response perfixMetrics() {
try {
return newFixedLengthResponse(Response.Status.OK, "application/json", Serializer.toJSONString(new ArrayList<>(Registry.sortedMethodsByDuration().values())));
} 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];
stream.read(bytes);
return new String(bytes);
}
private String determineContentType(String uri) {
if (uri.endsWith(".js")) {
return "application/javascript";
} else if (uri.endsWith(".css")) {
return "text/css";
} else {
return "text/html";
}
}
}

View file

@ -1,45 +0,0 @@
package perfix.server;
import org.apache.sshd.common.PropertyResolverUtils;
import org.apache.sshd.server.ServerFactoryManager;
import org.apache.sshd.server.SshServer;
import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider;
import java.io.IOException;
import java.nio.file.Paths;
public class SSHServer implements Server, Runnable {
private static final String BANNER = "\n\nWelcome to Perfix!\n\n";
private int port;
public void startListeningOnSocket(int port) {
this.port=port;
new Thread(this).start();
}
@Override
public void run() {
SshServer sshd = SshServer.setUpDefaultServer();
PropertyResolverUtils.updateProperty(sshd, ServerFactoryManager.WELCOME_BANNER, BANNER);
sshd.setPasswordAuthenticator((s, s1, serverSession) -> true);
sshd.setPort(port);
sshd.setShellFactory(new SshSessionFactory());
sshd.setKeyPairProvider(new SimpleGeneratorHostKeyProvider(Paths.get("hostkey.ser")));
try {
sshd.start();
} catch (IOException e) {
e.printStackTrace();
}
for (;;){
try {
Thread.sleep(500);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}

View file

@ -1,5 +0,0 @@
package perfix.server;
public interface Server {
void startListeningOnSocket(int port);
}

View file

@ -1,20 +0,0 @@
package perfix.server;
import org.apache.sshd.common.Factory;
import org.apache.sshd.server.Command;
import org.apache.sshd.server.CommandFactory;
public class SshSessionFactory
implements CommandFactory, Factory<Command> {
@Override
public Command createCommand(String command) {
return new SshSessionInstance();
}
@Override
public Command create() {
return createCommand("none");
}
}

View file

@ -1,72 +0,0 @@
package perfix.server;
import org.apache.sshd.server.Command;
import org.apache.sshd.server.Environment;
import org.apache.sshd.server.ExitCallback;
import perfix.Registry;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
public class SshSessionInstance implements Command, Runnable {
private static final String ANSI_NEWLINE_CRLF = "\u001B[20h";
private InputStream input;
private OutputStream output;
private ExitCallback callback;
@Override
public void start(Environment env) {
new Thread(this, "PerfixShell").start();
}
@Override
public void run() {
try {
output.write("press [enter] for report or [q] to quit\n".getBytes());
output.write((ANSI_NEWLINE_CRLF).getBytes());
output.flush();
boolean exit = false;
while (!exit) {
char c = (char) input.read();
if (c == 'q') {
exit = true;
} else if (c == '\n') {
Registry.report(new PrintStream(output));
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
callback.onExit(0);
}
}
@Override
public void destroy() { }
@Override
public void setErrorStream(OutputStream errOS) {
}
@Override
public void setExitCallback(ExitCallback ec) {
callback = ec;
}
@Override
public void setInputStream(InputStream is) {
this.input = is;
}
@Override
public void setOutputStream(OutputStream os) {
this.output = os;
}
}

View file

@ -1,48 +0,0 @@
package perfix.server;
import perfix.Registry;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.net.ServerSocket;
import java.net.Socket;
public class TelnetServer implements Server {
public void startListeningOnSocket(int port) {
try {
ServerSocket serverSocket = new ServerSocket(port);
new Thread(() -> {
for (; ; ) {
try {
Socket client = serverSocket.accept();
PrintStream out = new PrintStream(client.getOutputStream());
out.println("press [enter] for report or [q and enter] to quit");
BufferedReader reader = new BufferedReader(new InputStreamReader(client.getInputStream()));
String line;
while ((line = reader.readLine()) != null) {
if (line.equals("q")) {
try {
client.close();
break;
} catch (IOException e) {
e.printStackTrace();
}
} else {
Registry.report(out);
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
} catch (Exception e) {
e.printStackTrace();
}
}
}

View file

@ -0,0 +1,21 @@
package perfix.server.json;
import java.util.Formatter;
public abstract class JSONSerializer<T> {
protected abstract String handle(T object);
protected Formatter formatter = new Formatter();
public String toJSONString(T object) {
if (object == null) {
return "";
} else if (object instanceof Number || object instanceof Boolean) {
return "" + object.toString();
} else if (object instanceof CharSequence || object instanceof Character) {
return "\"" + object.toString() + "\"";
} else {
return handle(object);
}
}
}

View file

@ -0,0 +1,45 @@
package perfix.server.json;
public class Serializer {
private static SerializerFactory instance = new SynthSerializerFactory();
public static String toJSONString(boolean b) {
return Boolean.toString(b);
}
public static String toJSONString(short s) {
return Short.toString(s);
}
public static String toJSONString(int i) {
return Integer.toString(i);
}
public static String toJSONString(float f) {
return Float.toString(f);
}
public static String toJSONString(double d) {
return Double.toString(d);
}
public static String toJSONString(long l) {
return Long.toString(l);
}
public static String toJSONString(char c) {
return "\"" + Character.toString(c) + "\"";
}
@SuppressWarnings("unchecked")
public static <T> String toJSONString(T o) {
if (o == null) {
return "null";
}
return instance.createSerializer((Class<T>) o.getClass()).toJSONString(o);
}
public static void setInstance(SerializerFactory instance) {
Serializer.instance = instance;
}
}

View file

@ -0,0 +1,10 @@
package perfix.server.json;
@SuppressWarnings("serial")
public class SerializerCreationException extends RuntimeException {
public SerializerCreationException(Throwable t) {
super(t);
}
}

View file

@ -0,0 +1,5 @@
package perfix.server.json;
public interface SerializerFactory {
public <T> JSONSerializer<T> createSerializer(Class<T> beanjavaClass);
}

View file

@ -0,0 +1,376 @@
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.CannotCompileException;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtField;
import javassist.CtMethod;
import javassist.CtNewMethod;
import javassist.Modifier;
import javassist.NotFoundException;
public class SynthSerializerFactory implements SerializerFactory {
private static final String STRING = "java.lang.String";
private static final String BOOLEAN = "java.lang.Boolean";
private static final String CHARACTER = "java.lang.Character";
private static final String BYTE = "java.lang.Byte";
private static final String DOUBLE = "java.lang.Double";
private static final String FLOAT = "java.lang.Float";
private static final String LONG = "java.lang.Long";
private static final String SHORT = "java.lang.Short";
private static final String INTEGER = "java.lang.Integer";
private final static Set<String> wrappersAndString = new HashSet<String>(Arrays.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 Map<String, JSONSerializer<?>> serializers = new HashMap<>();
private static final String ROOT_PACKAGE = "serializer.";
private final ClassPool pool = ClassPool.getDefault();
private CtClass serializerBase;
private final Map<String, CtClass> primitiveWrappers = new HashMap<String, CtClass>();
public SynthSerializerFactory() {
init();
}
void init() {
try {
serializerBase = pool.get(JSONSerializer.class.getName());
primitiveWrappers.put("int", pool.get(INTEGER));
primitiveWrappers.put("short", pool.get(SHORT));
primitiveWrappers.put("byte", pool.get(BYTE));
primitiveWrappers.put("long", pool.get(LONG));
primitiveWrappers.put("float", pool.get(FLOAT));
primitiveWrappers.put("double", pool.get(DOUBLE));
primitiveWrappers.put("boolean", pool.get(BOOLEAN));
primitiveWrappers.put("char", pool.get(CHARACTER));
} catch (NotFoundException e) {
throw new SerializerCreationException(e);
}
}
public <T> JSONSerializer<T> createSerializer(Class<T> beanjavaClass) {
try {
CtClass beanClass = pool.get(beanjavaClass.getName());
JSONSerializer<T> jsonSerializer = createSerializer(beanClass);
return jsonSerializer;
} catch (NotFoundException e) {
throw new SerializerCreationException(e);
}
}
@SuppressWarnings("unchecked")
private <T> JSONSerializer<T> createSerializer(CtClass beanClass) {
if (serializers.containsKey(createSerializerName(beanClass))) {
return (JSONSerializer<T>) serializers.get(createSerializerName(beanClass));
}
try {
return tryCreateSerializer(beanClass);
} catch (NotFoundException | CannotCompileException | InstantiationException | IllegalAccessException e) {
throw new SerializerCreationException(e);
}
}
private <T> JSONSerializer<T> tryCreateSerializer(CtClass beanClass) throws NotFoundException, CannotCompileException, InstantiationException,
IllegalAccessException {
CtClass serializerClass = pool.makeClass(createSerializerName(beanClass), serializerBase);
addToJsonStringMethod(beanClass, serializerClass);
JSONSerializer<T> jsonSerializer = createSerializerInstance(serializerClass);
serializers.put(createSerializerName(beanClass), jsonSerializer);
return jsonSerializer;
}
/*
* 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));
}
/*
* Creates the source, handling the for JSON different types of classes
*/
private <T> String createToJSONStringMethodSource(CtClass beanClass) throws NotFoundException {
String source = "public String handle(Object object){\n";
if (beanClass.isArray()) {
source += "\tObject[] array=(Object[])object;\n";
source += handleArray(beanClass);
} else if (isCollection(beanClass)) {
source += "\tObject[] array=((java.util.Collection)object).toArray();\n";
source += handleArray(beanClass);
} else if (isMap(beanClass)) {
source += handleMap(beanClass);
} else if (!isPrimitiveOrWrapperOrString(beanClass)) {
List<CtMethod> getters = getGetters(beanClass);
if (shouldAddGetterCallers(getters)) {
source = addGetterCallers(beanClass, source, getters);
}
} else {
source += "\treturn \"\";}";
}
return source;
}
/*
* 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) {
String source = "\tStringBuilder result=new StringBuilder(\"[\");\n";
source += "\tfor (int i=0; i<array.length; i++){\n";
source += "\t\tresult.append(" + Serializer.class.getName() + ".toJSONString(array[i]));\n";
source += "\t\tresult.append(\", \");\n";
source += "\t};\n";
source += "\tresult.setLength(result.length()-2);\n";
source += "\tresult.append(\"]\");\n";
source += "\treturn result.toString();\n";
source += "}";
return source;
}
private String handleMap(CtClass beanClass) {
String source = "StringBuilder result=new StringBuilder(\"{\");\n";
source += "\tfor (java.util.Iterator entries=((java.util.Map)object).entrySet().iterator();entries.hasNext();){\n";
source += "\t\tjava.util.Map.Entry entry=(java.util.Map.Entry)entries.next();\n";
source += "\t\tresult.append(\"\\\"\"+entry.getKey().toString()+\"\\\"\");\n";
source += "\t\tresult.append(\": \");\n";
source += "\t\tresult.append(" + Serializer.class.getName() + ".toJSONString(entry.getValue()));\n";
source += "\t\tresult.append(\", \");\n";
source += "\t};\n";
source += "\tresult.setLength(result.length()-2);\n";
source += "\tresult.append(\"}\");\n";
source += "\treturn result.toString();\n";
source += "}";
return source;
}
/*
* If the class contains fields for which public getters are available, then these will be called in the generated code.
*/
private String addGetterCallers(CtClass beanClass, String source, List<CtMethod> getters) throws NotFoundException {
int index = 0;
source += "\treturn ";
source += "\"{";
for (CtMethod getter : getters) {
source = addPair(beanClass, source, getter);
if (index++ < getters.size() - 1) {
source += ",";
}
}
source += "}\";\n}";
return source;
}
@SuppressWarnings("unchecked")
private <T> JSONSerializer<T> createSerializerInstance(CtClass serializerClass) throws InstantiationException, IllegalAccessException,
CannotCompileException {
return (JSONSerializer<T>) serializerClass.toClass().newInstance();
}
/*
* 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) {
return createSerializerName(beanClass.getName());
}
public String createSerializerName(String name) {
return ROOT_PACKAGE + name.replaceAll("\\[\\]", "Array") + "Serializer";
}
private boolean isCollection(CtClass beanClass) throws NotFoundException {
List<CtClass> interfaces = new ArrayList<CtClass>(Arrays.asList(beanClass.getInterfaces()));
interfaces.add(beanClass);
for (CtClass interfaze : interfaces) {
if (interfaze.getName().equals(COLLECTION) || interfaze.getName().equals(LIST) || interfaze.getName().equals(SET)) {
return true;
}
}
return false;
}
private boolean isMap(CtClass beanClass) throws NotFoundException {
List<CtClass> interfaces = new ArrayList<CtClass>(Arrays.asList(beanClass.getInterfaces()));
interfaces.add(beanClass);
for (CtClass interfaze : interfaces) {
if (interfaze.getName().equals(MAP)) {
return true;
}
}
return false;
}
/*
* The JSON vernacular for key:value is pair...
*/
private String addPair(CtClass classToSerialize, String source, CtMethod getter) throws NotFoundException {
source += jsonKey(getter);
source += ": "; // what is the rule when it comes to spaces in json?
source += jsonValue(classToSerialize, getter);
return source;
}
/*
* derive property key from getter
*/
private String jsonKey(CtMethod getter) {
return "\\\"" + toFieldName(getter.getName()) + "\\\"";
}
private String jsonValue(CtClass classToSerialize, CtMethod getter) throws NotFoundException {
String source = "";
CtClass returnType = getter.getReturnType();
/* primitives are wrapped so the produced methods adhere to the JSONSerializer interface */
source = createSubSerializerForReturnTypeAndAddInvocationToSource(classToSerialize, getter, source, returnType);
return source;
}
private String createSubSerializerForReturnTypeAndAddInvocationToSource(CtClass classToSerialize, CtMethod getter, String source, CtClass returnType) {
/* NB there does not seem to be auto(un))boxing nor generic types (or other jdk1.5 stuff) in javassist compileable code */
source += "\"+" + Serializer.class.getName() + ".toJSONString(";
// cast because of lack of generics
source += "(" + cast(regularClassname(classToSerialize.getName())) + "object)." + getter.getName() + "()";
source += ")+\"";
return source;
}
/*
* turns for example 'getValue' into 'value'
*/
private String toFieldName(String name) {
return name.substring(3, 4).toLowerCase() + (name.length() > 4 ? name.substring(4) : "");
}
public String regularClassname(String name) {
return name.replaceAll("\\$", ".");
}
private String cast(String classToSerialize) {
return "(" + classToSerialize + ")";
}
/*
* Retrieves getter methods from a class
*/
private List<CtMethod> getGetters(CtClass beanClass) {
List<CtMethod> methods = new ArrayList<CtMethod>();
List<CtField> fields = getAllFields(beanClass);
for (CtField field : fields) {
try {
CtMethod method = beanClass.getMethod(getGetterMethod(field), getDescription(field));
if (Modifier.isPublic(method.getModifiers())) {
methods.add(method);
}
} catch (NotFoundException n) {
// ignore
}
}
return methods;
}
private String getGetterMethod(CtField field) {
return "get" + field.getName().substring(0, 1).toUpperCase() + field.getName().substring(1);
}
private List<CtField> getAllFields(CtClass beanClass) {
try {
List<CtField> allfields = new ArrayList<>();
for (CtField field : beanClass.getDeclaredFields()) {
allfields.add(field);
}
if (beanClass.getSuperclass() != null) {
return getAllFields(beanClass.getSuperclass(), allfields);
}
return allfields;
} catch (NotFoundException e) {
throw new SerializerCreationException(e);
}
}
private List<CtField> getAllFields(CtClass beanClass, List<CtField> allfields) {
for (CtField field : beanClass.getDeclaredFields()) {
allfields.add(field);
}
return allfields;
}
/*
* is getter list is not empty then callers should be added
*/
boolean shouldAddGetterCallers(List<CtMethod> getters) {
return !getters.isEmpty();
}
String getDescription(CtField field) throws NotFoundException {
if (field.getType().isArray()) {
return "()[" + innerClassName(field.getType().getName()) + ";";
} else if (!field.getType().isPrimitive()) {
return "()" + innerClassName(field.getType().getName()) + ";";
} else {
return "()" + asPrimitive(field.getType().getName());
}
}
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";
}
return "";
}
String innerClassName(String name) {
return "L" + name.replaceAll("\\.", "/").replaceAll("\\[\\]", "");
}
static boolean isPrimitiveOrWrapperOrString(CtClass beanClass) {
return beanClass.isPrimitive() || wrappersAndString.contains(beanClass.getName());
}
}

2
src/main/resources/d3.js vendored Normal file

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,18 @@
<!DOCTYPE html>
<html>
<head>
<title>Perfix console</title>
<script src="/d3.js"></script>
<script src="/perfix.js"></script>
<link rel="stylesheet" type="text/css" href="/main.css">
</head>
<body class="datagrid">
<table></table>
</body>
<script>
d3.json("/report").then(function (data) {
// render the table(s)
tabulate(data, ['name', 'invocations', 'totalDuration', 'average']);
});
</script>
</html>

106
src/main/resources/main.css Executable file
View file

@ -0,0 +1,106 @@
.datagrid table {
border-collapse: collapse;
text-align: left;
width: 100%;
}
.datagrid {
font: normal 12px/150% Arial, Helvetica, sans-serif;
background: #fff;
overflow: hidden;
border: 1px solid #006699;
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
border-radius: 3px;
}
.datagrid table td, .datagrid table th {
padding: 3px 10px;
}
.datagrid table thead th {
background: -webkit-gradient(linear, left top, left bottom, color-stop(0.05, #006699), color-stop(1, #00557F));
background: -moz-linear-gradient(center top, #006699 5%, #00557F 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#006699', endColorstr='#00557F');
background-color: #006699;
color: #ffffff;
font-size: 15px;
font-weight: bold;
border-left: 1px solid #0070A8;
}
.datagrid table thead th:first-child {
border: none;
}
.datagrid table tbody td {
color: #00496B;
border-left: 1px solid #E1EEF4;
font-size: 12px;
font-weight: normal;
}
.datagrid table tbody .alt td {
background: #E1EEF4;
color: #00496B;
}
.datagrid table tbody td:first-child {
border-left: none;
}
.datagrid table tbody tr:last-child td {
border-bottom: none;
}
.datagrid table tfoot td div {
border-top: 1px solid #006699;
background: #E1EEF4;
}
.datagrid table tfoot td {
padding: 0;
font-size: 12px
}
.datagrid table tfoot td div {
padding: 2px;
}
.datagrid table tfoot td ul {
margin: 0;
padding: 0;
list-style: none;
text-align: right;
}
.datagrid table tfoot li {
display: inline;
}
.datagrid table tfoot li a {
text-decoration: none;
display: inline-block;
padding: 2px 8px;
margin: 1px;
color: #FFFFFF;
border: 1px solid #006699;
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
border-radius: 3px;
background: -webkit-gradient(linear, left top, left bottom, color-stop(0.05, #006699), color-stop(1, #00557F));
background: -moz-linear-gradient(center top, #006699 5%, #00557F 100%);
background-color: #006699;
}
.datagrid table tfoot ul.active, .datagrid table tfoot ul a:hover {
text-decoration: none;
border-color: #006699;
color: #FFFFFF;
background: none;
background-color: #00557F;
}
div.dhtmlx_window_active, div.dhx_modal_cover_dv {
position: fixed !important;
}

View file

@ -0,0 +1,28 @@
function tabulate(data, columns) {
var table = d3.select('body').append('table')
var thead = table.append('thead')
var tbody = table.append('tbody');
thead.append('tr')
.selectAll('th')
.data(columns).enter()
.append('th')
.text(function (column) { return column; });
var rows = tbody.selectAll('tr')
.data(data)
.enter()
.append('tr');
rows.selectAll('td')
.data(function (row) {
return columns.map(function (column) {
return {column: column, value: row[column]};
});
})
.enter()
.append('td')
.text(function (d) { return d.value; });
return table;
}

View file

@ -0,0 +1,44 @@
[
{
"name": "testperfix.Main.main(java.lang.String[])",
"invocations": 1,
"totalDuration": 1414878875,
"average": 1.414878875E9
},
{
"name": "testperfix.Main.run()",
"invocations": 1,
"totalDuration": 1414756159,
"average": 1.414756159E9
},
{
"name": "testperfix.Main.someJdbcStatentMethod()",
"invocations": 1,
"totalDuration": 399009103,
"average": 3.99009103E8
},
{
"name": "select CURRENT_DATE() -- simple statement",
"invocations": 1,
"totalDuration": 99705675,
"average": 9.9705675E7
},
{
"name": "testperfix.Main.someJdbcPreparedStatementMethod()",
"invocations": 1,
"totalDuration": 5880308,
"average": 5880308.0
},
{
"name": "testperfix.Main.someOtherMethod()",
"invocations": 1,
"totalDuration": 1285359,
"average": 1285359.0
},
{
"name": "select CURRENT_DATE() -- prepared statement",
"invocations": 1,
"totalDuration": 98241,
"average": 98241.0
}
]