html interface
This commit is contained in:
parent
5049459bbc
commit
678b122c33
24 changed files with 784 additions and 234 deletions
11
README.md
11
README.md
|
|
@ -2,15 +2,13 @@
|
|||
Pretty basic profiling tool for JVM's
|
||||
|
||||
# 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).
|
||||
* 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).
|
||||
* No manual instrumentation necessary using loadtime bytecode manipulation (javassist).
|
||||
* 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.
|
||||
* 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
|
||||
* Agent that instruments loaded classes: -javaagent:<path>/perfix.jar
|
||||
|
|
@ -19,11 +17,12 @@ Pretty basic profiling tool for JVM's
|
|||
<br/> * #invocations
|
||||
<br/> * total execution time for the method in nanoseconds (this is also the sorting order)
|
||||
<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
|
||||
* 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
|
||||
* Implement password login (now any)
|
||||
* Add web interface (maybe)
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
<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/test/resources" type="java-test-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/main/resources" type="java-resource" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/target" />
|
||||
</content>
|
||||
<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: 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" name="Maven: org.apache.sshd:sshd-core:1.7.0" 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" />
|
||||
<orderEntry type="library" name="Maven: org.nanohttpd:nanohttpd:2.3.1" level="project" />
|
||||
</component>
|
||||
</module>
|
||||
11
pom.xml
11
pom.xml
|
|
@ -32,14 +32,9 @@
|
|||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.sshd</groupId>
|
||||
<artifactId>sshd-core</artifactId>
|
||||
<version>1.7.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-nop</artifactId>
|
||||
<version>1.7.25</version>
|
||||
<groupId>org.nanohttpd</groupId>
|
||||
<artifactId>nanohttpd</artifactId>
|
||||
<version>2.3.1</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ public class Main {
|
|||
System.out.println("Exiting now");
|
||||
System.exit(0);
|
||||
}
|
||||
System.out.println("Now start ssh session on localhost on port 2048 (without closing the Test Application)");
|
||||
run();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,7 @@
|
|||
package perfix;
|
||||
|
||||
import javassist.*;
|
||||
import perfix.server.SSHServer;
|
||||
import perfix.server.HTTPServer;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.instrument.Instrumentation;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
|
|
@ -28,7 +26,7 @@ public class Agent {
|
|||
|
||||
new ClassInstrumentor(determineIncludes()).instrumentCode(inst);
|
||||
|
||||
new SSHServer().startListeningOnSocket(port);
|
||||
new HTTPServer(port).start();
|
||||
}
|
||||
|
||||
private static List<String> determineIncludes() {
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ public class MethodInvocation {
|
|||
if (name != null) {
|
||||
this.name = name;
|
||||
} else {
|
||||
this.name = "<UNKNOWN_BECAUSE_OF_ERROR>";
|
||||
this.name = "<error occurred>";
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ public class Registry {
|
|||
static void add(MethodInvocation methodInvocation) {
|
||||
methods.computeIfAbsent(methodInvocation.getName(), key -> new ArrayList<>()).add(methodInvocation);
|
||||
}
|
||||
|
||||
|
||||
public static void report(PrintStream out) {
|
||||
out.println(HEADER1);
|
||||
out.println(HEADER2);
|
||||
|
|
@ -29,13 +29,13 @@ public class Registry {
|
|||
}
|
||||
|
||||
private static String createReportLine(Report report) {
|
||||
return report.name + ";"
|
||||
+ report.invocations + ";"
|
||||
+ (long) (report.totalDuration / NANO_2_MILLI) + ";"
|
||||
+ (long) (report.average() / NANO_2_MILLI);
|
||||
return report.getName() + ";"
|
||||
+ report.getInvocations() + ";"
|
||||
+ (long) (report.getTotalDuration() / 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());
|
||||
methods.forEach((name, measurements) -> {
|
||||
LongAdder totalDuration = new LongAdder();
|
||||
|
|
@ -47,19 +47,5 @@ public class Registry {
|
|||
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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
31
src/main/java/perfix/Report.java
Normal file
31
src/main/java/perfix/Report.java
Normal 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;
|
||||
}
|
||||
}
|
||||
78
src/main/java/perfix/server/HTTPServer.java
Normal file
78
src/main/java/perfix/server/HTTPServer.java
Normal 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";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
package perfix.server;
|
||||
|
||||
public interface Server {
|
||||
void startListeningOnSocket(int port);
|
||||
}
|
||||
|
|
@ -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");
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
21
src/main/java/perfix/server/json/JSONSerializer.java
Normal file
21
src/main/java/perfix/server/json/JSONSerializer.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
45
src/main/java/perfix/server/json/Serializer.java
Normal file
45
src/main/java/perfix/server/json/Serializer.java
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package perfix.server.json;
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
public class SerializerCreationException extends RuntimeException {
|
||||
|
||||
public SerializerCreationException(Throwable t) {
|
||||
super(t);
|
||||
}
|
||||
|
||||
}
|
||||
5
src/main/java/perfix/server/json/SerializerFactory.java
Normal file
5
src/main/java/perfix/server/json/SerializerFactory.java
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
package perfix.server.json;
|
||||
|
||||
public interface SerializerFactory {
|
||||
public <T> JSONSerializer<T> createSerializer(Class<T> beanjavaClass);
|
||||
}
|
||||
376
src/main/java/perfix/server/json/SynthSerializerFactory.java
Normal file
376
src/main/java/perfix/server/json/SynthSerializerFactory.java
Normal 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
2
src/main/resources/d3.js
vendored
Normal file
File diff suppressed because one or more lines are too long
18
src/main/resources/index.html
Normal file
18
src/main/resources/index.html
Normal 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
106
src/main/resources/main.css
Executable 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;
|
||||
}
|
||||
28
src/main/resources/perfix.js
Normal file
28
src/main/resources/perfix.js
Normal 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;
|
||||
}
|
||||
44
src/main/resources/static.json
Normal file
44
src/main/resources/static.json
Normal 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
|
||||
}
|
||||
]
|
||||
Loading…
Add table
Reference in a new issue