* removed React for the UI
* removed TinyHttpd for the backend service * fixed errors * reduced footprint * improved performance and responsiveness
This commit is contained in:
parent
b814c44a0c
commit
29e71c0f96
12 changed files with 520 additions and 82 deletions
12
README.md
12
README.md
|
|
@ -33,4 +33,14 @@ Pretty basic profiling tool for JVM's
|
||||||
# DISCLAIMER:
|
# DISCLAIMER:
|
||||||
This has only been tested on oracle java8 in spring-boot using tomcat web-container (and apache dbcp)
|
This has only been tested on oracle java8 in spring-boot using tomcat web-container (and apache dbcp)
|
||||||
|
|
||||||
That said I should mention that the callstack view is pretty slow, which is caused by the gui, not the backend. I guess I'll replace it with a static vanilla.js app.
|
Javassist raises the following error:
|
||||||
|
```
|
||||||
|
WARNING: An illegal reflective access operation has occurred
|
||||||
|
WARNING: Illegal reflective access by javassist.util.proxy.SecurityActions (file:/Users/Shautvast/.m2/repository/org/javassist/javassist/3.26.0-GA/javassist-3.26.0-GA.jar) to method java.lang.ClassLoader.defineClass(java.lang.String,byte[],int,int,java.security.ProtectionDomain)
|
||||||
|
WARNING: Please consider reporting this to the maintainers of javassist.util.proxy.SecurityActions
|
||||||
|
WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations
|
||||||
|
WARNING: All illegal access operations will be denied in a future release
|
||||||
|
```
|
||||||
|
But it works at least up until java 15.
|
||||||
|
|
||||||
|
I cannot fix this issue, but I'm working to replace javassist as a dependency.
|
||||||
9
pom.xml
9
pom.xml
|
|
@ -19,24 +19,17 @@
|
||||||
<artifactId>javassist</artifactId>
|
<artifactId>javassist</artifactId>
|
||||||
<version>3.26.0-GA</version>
|
<version>3.26.0-GA</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.nanohttpd</groupId>
|
|
||||||
<artifactId>nanohttpd</artifactId>
|
|
||||||
<version>2.3.1</version>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>junit</groupId>
|
<groupId>junit</groupId>
|
||||||
<artifactId>junit</artifactId>
|
<artifactId>junit</artifactId>
|
||||||
<version>4.13.1</version>
|
<version>4.13.1</version>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-dbcp2 -->
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.apache.commons</groupId>
|
<groupId>org.apache.commons</groupId>
|
||||||
<artifactId>commons-dbcp2</artifactId>
|
<artifactId>commons-dbcp2</artifactId>
|
||||||
<version>2.4.0</version>
|
<version>2.4.0</version>
|
||||||
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.h2database</groupId>
|
<groupId>com.h2database</groupId>
|
||||||
|
|
|
||||||
|
|
@ -1,76 +1,138 @@
|
||||||
package perfix.server;
|
package perfix.server;
|
||||||
|
|
||||||
import fi.iki.elonen.NanoHTTPD;
|
import com.sun.net.httpserver.HttpExchange;
|
||||||
|
import com.sun.net.httpserver.HttpHandler;
|
||||||
|
import com.sun.net.httpserver.HttpServer;
|
||||||
import perfix.Registry;
|
import perfix.Registry;
|
||||||
import perfix.server.json.Serializer;
|
import perfix.server.json.Serializer;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.net.InetSocketAddress;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
public class HTTPServer extends NanoHTTPD {
|
public class HTTPServer implements HttpHandler {
|
||||||
|
private static final Logger log = Logger.getLogger("perfix");
|
||||||
|
private final int port;
|
||||||
|
|
||||||
public HTTPServer(int port) {
|
public HTTPServer(int port) {
|
||||||
super(port);
|
this.port = port;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void start() {
|
public void start() {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
start(NanoHTTPD.SOCKET_READ_TIMEOUT, false);
|
HttpServer server = HttpServer.create(new InetSocketAddress("localhost", port), 0);
|
||||||
System.out.println(" --- Perfix http server running. Point your browser to http://localhost:" + getListeningPort() + "/");
|
server.createContext("/", this);
|
||||||
|
server.setExecutor(Executors.newFixedThreadPool(2));
|
||||||
|
server.start();
|
||||||
|
System.out.println(" --- Perfix http server running. Point your browser to http://localhost:" + port + "/");
|
||||||
} catch (IOException ioe) {
|
} catch (IOException ioe) {
|
||||||
System.err.println(" --- Couldn't start Perfix http server:\n" + ioe);
|
System.err.println(" --- Couldn't start Perfix http server:\n" + ioe);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Response serve(IHTTPSession session) {
|
public void handle(HttpExchange exchange) throws IOException {
|
||||||
String uri = session.getUri();
|
String uri = exchange.getRequestURI().toString();
|
||||||
|
InputStream response = null;
|
||||||
switch (uri) {
|
switch (uri) {
|
||||||
case "/report":
|
case "/report":
|
||||||
return perfixMetrics();
|
setContentTypeJson(exchange);
|
||||||
|
response = toStream(perfixMetrics());
|
||||||
|
break;
|
||||||
case "/callstack":
|
case "/callstack":
|
||||||
return perfixCallstack();
|
setContentTypeJson(exchange);
|
||||||
|
response = toStream(perfixCallstack());
|
||||||
|
break;
|
||||||
case "/clear":
|
case "/clear":
|
||||||
return clear();
|
setContentTypeJson(exchange);
|
||||||
|
response = toStream(clear());
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
return newFixedLengthResponse(Response.Status.NOT_FOUND, "text/plain", "NOT FOUND");
|
response = staticContent(exchange, uri);
|
||||||
|
}
|
||||||
|
OutputStream outputStream = exchange.getResponseBody();
|
||||||
|
exchange.getResponseHeaders().add("Access-Control-Allow-Origin", "*");
|
||||||
|
exchange.getResponseHeaders().add("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
|
||||||
|
int length = response.available();
|
||||||
|
exchange.sendResponseHeaders(200, length);
|
||||||
|
for (int i = 0; i < length; i++) {
|
||||||
|
outputStream.write(response.read());
|
||||||
|
}
|
||||||
|
outputStream.flush();
|
||||||
|
outputStream.close();
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setContentTypeJson(HttpExchange exchange) {
|
||||||
|
exchange.getResponseHeaders().add("Content-Type", "application/json");
|
||||||
|
}
|
||||||
|
|
||||||
|
private InputStream staticContent(HttpExchange exchange, String uri) {
|
||||||
|
if (uri.equals("/")) {
|
||||||
|
uri = "/index.html";
|
||||||
|
}
|
||||||
|
InputStream resource = getClass().getResourceAsStream(uri);
|
||||||
|
if (resource != null) {
|
||||||
|
String mimeType;
|
||||||
|
if (uri.endsWith("css")) {
|
||||||
|
mimeType = "text/css";
|
||||||
|
} else if (uri.endsWith("js")) {
|
||||||
|
mimeType = "application/ecmascript";
|
||||||
|
} else {
|
||||||
|
mimeType = "text/html";
|
||||||
|
}
|
||||||
|
exchange.getResponseHeaders().add("Content-Type", mimeType);
|
||||||
|
return resource;
|
||||||
|
|
||||||
|
} else {
|
||||||
|
return toStream(notFound());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Response perfixMetrics() {
|
private String notFound() {
|
||||||
|
return "NOT FOUND";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private String perfixMetrics() {
|
||||||
try {
|
try {
|
||||||
return addCors(newFixedLengthResponse(Response.Status.OK, "application/json", Serializer.toJSONString(new ArrayList<>(Registry.sortedMethodsByDuration().values()))));
|
return Serializer.toJSONString(new ArrayList<>(Registry.sortedMethodsByDuration().values()));
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
e.printStackTrace();
|
log.severe(e.toString());
|
||||||
return newFixedLengthResponse(e.toString());
|
return e.toString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Response addCors(Response response) {
|
|
||||||
response.addHeader("Access-Control-Allow-Origin", "*");
|
private InputStream toStream(String text) {
|
||||||
response.addHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
|
return new ByteArrayInputStream(text.getBytes(StandardCharsets.UTF_8));
|
||||||
return response;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Response perfixCallstack() {
|
private String perfixCallstack() {
|
||||||
try {
|
try {
|
||||||
return addCors(newFixedLengthResponse(Response.Status.OK, "application/json", Serializer.toJSONString(Registry.getCallStack())));
|
return Serializer.toJSONString(Registry.getCallStack());
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
e.printStackTrace();
|
log.severe(e.toString());
|
||||||
return newFixedLengthResponse(e.toString());
|
return e.toString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Response clear() {
|
private String clear() {
|
||||||
Registry.clear();
|
Registry.clear();
|
||||||
try {
|
try {
|
||||||
return addCors(newFixedLengthResponse(Response.Status.OK, "application/json", Serializer.toJSONString(Registry.getCallStack())));
|
return Serializer.toJSONString(Registry.getCallStack());
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
e.printStackTrace();
|
log.severe(e.toString());
|
||||||
return newFixedLengthResponse(e.toString());
|
return e.toString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
package perfix.server.json;
|
package perfix.server.json;
|
||||||
|
|
||||||
public interface SerializerFactory {
|
public interface SerializerFactory {
|
||||||
public <T> JSONSerializer<T> createSerializer(Class<T> beanjavaClass);
|
<T> JSONSerializer<T> createSerializer(Class<T> beanjavaClass);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,11 @@ package perfix.server.json;
|
||||||
import javassist.*;
|
import javassist.*;
|
||||||
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.ConcurrentMap;
|
||||||
|
|
||||||
import static java.util.Arrays.asList;
|
import static java.util.Arrays.asList;
|
||||||
|
import static java.util.Collections.unmodifiableSet;
|
||||||
|
|
||||||
public class SynthSerializerFactory implements SerializerFactory {
|
public class SynthSerializerFactory implements SerializerFactory {
|
||||||
private static final String STRING = "java.lang.String";
|
private static final String STRING = "java.lang.String";
|
||||||
|
|
@ -17,15 +20,15 @@ public class SynthSerializerFactory implements SerializerFactory {
|
||||||
private static final String SHORT = "java.lang.Short";
|
private static final String SHORT = "java.lang.Short";
|
||||||
private static final String INTEGER = "java.lang.Integer";
|
private static final String INTEGER = "java.lang.Integer";
|
||||||
|
|
||||||
private final static Set<String> wrappersAndString = new HashSet<String>(asList(BOOLEAN, CHARACTER, BYTE, DOUBLE, FLOAT, LONG, SHORT, INTEGER,
|
private final static Set<String> wrappersAndString = unmodifiableSet(new HashSet<String>(asList(BOOLEAN, CHARACTER, BYTE, DOUBLE, FLOAT, LONG, SHORT, INTEGER,
|
||||||
STRING));
|
STRING)));
|
||||||
|
|
||||||
private static final String COLLECTION = "java.util.Collection";
|
private static final String COLLECTION = "java.util.Collection";
|
||||||
private static final String LIST = "java.util.List";
|
private static final String LIST = "java.util.List";
|
||||||
private static final String SET = "java.util.Set";
|
private static final String SET = "java.util.Set";
|
||||||
private static final List<String> mapInterfaces = asList("java.util.Map", "java.util.concurrent.ConcurrentHashMap");
|
private static final List<String> mapInterfaces = Collections.unmodifiableList(asList("java.util.Map", "java.util.concurrent.ConcurrentHashMap"));
|
||||||
|
|
||||||
private static final Map<String, JSONSerializer<?>> serializers = new HashMap<>();
|
private static final ConcurrentMap<String, JSONSerializer<?>> serializers = new ConcurrentHashMap<>();
|
||||||
private static final String ROOT_PACKAGE = "serializer.";
|
private static final String ROOT_PACKAGE = "serializer.";
|
||||||
|
|
||||||
private final ClassPool pool = ClassPool.getDefault();
|
private final ClassPool pool = ClassPool.getDefault();
|
||||||
|
|
@ -47,43 +50,31 @@ public class SynthSerializerFactory implements SerializerFactory {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public <T> JSONSerializer<T> createSerializer(Class<T> beanjavaClass) {
|
|
||||||
try {
|
|
||||||
CtClass beanClass = pool.get(beanjavaClass.getName());
|
|
||||||
|
|
||||||
return createSerializer(beanClass);
|
|
||||||
} catch (NotFoundException e) {
|
|
||||||
throw new SerializerCreationException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
private <T> JSONSerializer<T> createSerializer(CtClass beanClass) {
|
public <T> JSONSerializer<T> createSerializer(Class<T> beanjavaClass) {
|
||||||
if (serializers.containsKey(createSerializerName(beanClass))) {
|
String serializerName = createSerializerName(beanjavaClass);
|
||||||
return (JSONSerializer<T>) serializers.get(createSerializerName(beanClass));
|
return (JSONSerializer<T>) serializers.computeIfAbsent(serializerName, key -> {
|
||||||
}
|
try {
|
||||||
try {
|
CtClass beanClass = pool.get(beanjavaClass.getName());
|
||||||
return tryCreateSerializer(beanClass);
|
CtClass serializerClass = pool.makeClass(serializerName, serializerBase);
|
||||||
} catch (NotFoundException | CannotCompileException | ReflectiveOperationException e) {
|
|
||||||
throw new SerializerCreationException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private <T> JSONSerializer<T> tryCreateSerializer(CtClass beanClass) throws NotFoundException, CannotCompileException, ReflectiveOperationException {
|
addToJsonStringMethod(beanClass, serializerClass);
|
||||||
CtClass serializerClass = pool.makeClass(createSerializerName(beanClass), serializerBase);
|
|
||||||
|
|
||||||
addToJsonStringMethod(beanClass, serializerClass);
|
return createSerializerInstance(serializerClass);
|
||||||
|
|
||||||
JSONSerializer<T> jsonSerializer = createSerializerInstance(serializerClass);
|
} catch (NotFoundException | CannotCompileException | ReflectiveOperationException e) {
|
||||||
|
e.printStackTrace();
|
||||||
serializers.put(createSerializerName(beanClass), jsonSerializer);
|
throw new SerializerCreationException(e);
|
||||||
return jsonSerializer;
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* create method source, compile it and add it to the class under construction
|
* create method source, compile it and add it to the class under construction
|
||||||
*/
|
*/
|
||||||
private void addToJsonStringMethod(CtClass beanClass, CtClass serializerClass) throws NotFoundException, CannotCompileException {
|
private void addToJsonStringMethod(CtClass beanClass, CtClass serializerClass) throws
|
||||||
|
NotFoundException, CannotCompileException {
|
||||||
String body = createToJSONStringMethodSource(beanClass);
|
String body = createToJSONStringMethodSource(beanClass);
|
||||||
serializerClass.addMethod(CtNewMethod.make(body, serializerClass));
|
serializerClass.addMethod(CtNewMethod.make(body, serializerClass));
|
||||||
}
|
}
|
||||||
|
|
@ -146,7 +137,8 @@ public class SynthSerializerFactory implements SerializerFactory {
|
||||||
/*
|
/*
|
||||||
* If the class contains fields for which public getters are available, then these will be called in the generated code.
|
* 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 {
|
private String addGetterCallers(CtClass beanClass, String source, List<CtMethod> getters) throws
|
||||||
|
NotFoundException {
|
||||||
int index = 0;
|
int index = 0;
|
||||||
source += "\treturn ";
|
source += "\treturn ";
|
||||||
source += "\"{";
|
source += "\"{";
|
||||||
|
|
@ -161,8 +153,9 @@ public class SynthSerializerFactory implements SerializerFactory {
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
private <T> JSONSerializer<T> createSerializerInstance(CtClass serializerClass) throws CannotCompileException, ReflectiveOperationException {
|
private <T> JSONSerializer<T> createSerializerInstance(CtClass serializerClass) throws
|
||||||
return (JSONSerializer<T>) serializerClass.toClass().getConstructor().newInstance();
|
CannotCompileException, ReflectiveOperationException {
|
||||||
|
return (JSONSerializer<T>) pool.toClass(serializerClass).getConstructor().newInstance();
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
@ -170,25 +163,30 @@ public class SynthSerializerFactory implements SerializerFactory {
|
||||||
*
|
*
|
||||||
* Array marks ( '[]' ) are replaced by the 'Array', Otherwise the SerializerClassName would be syntactically incorrect
|
* Array marks ( '[]' ) are replaced by the 'Array', Otherwise the SerializerClassName would be syntactically incorrect
|
||||||
*/
|
*/
|
||||||
public String createSerializerName(CtClass beanClass) {
|
public String createSerializerName(Class<?> beanClass) {
|
||||||
return createSerializerName(beanClass.getName());
|
return createSerializerName(beanClass.getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
public String createSerializerName(String name) {
|
public String createSerializerName(String name) {
|
||||||
return ROOT_PACKAGE + name.replaceAll("\\[\\]", "Array") + "Serializer";
|
return ROOT_PACKAGE + name.replaceAll("\\[]", "Array") + "Serializer";
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isCollection(CtClass beanClass) throws NotFoundException {
|
private boolean isCollection(CtClass beanClass) throws NotFoundException {
|
||||||
List<CtClass> interfaces = new ArrayList<>(asList(beanClass.getInterfaces()));
|
List<CtClass> interfaces = new ArrayList<>(asList(beanClass.getInterfaces()));
|
||||||
interfaces.add(beanClass);
|
interfaces.add(beanClass);
|
||||||
boolean is = interfaces.stream().anyMatch(interfaze -> interfaze.getName().equals(COLLECTION) || interfaze.getName().equals(LIST) || interfaze.getName().equals(SET));
|
return interfaces.stream()
|
||||||
return is;
|
.map(CtClass::getName)
|
||||||
|
.anyMatch(interfaze -> interfaze.equals(COLLECTION) || interfaze.equals(LIST) || interfaze.equals(SET));
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isMap(CtClass beanClass) throws NotFoundException {
|
private boolean isMap(CtClass beanClass) throws NotFoundException {
|
||||||
List<CtClass> interfaces = new ArrayList<>(asList(beanClass.getInterfaces()));
|
if (mapInterfaces.contains(beanClass.getName())) {
|
||||||
interfaces.add(beanClass);
|
return true;
|
||||||
return interfaces.stream().anyMatch(i -> mapInterfaces.contains(i.getName()));
|
} else {
|
||||||
|
return Arrays.stream(beanClass.getInterfaces())
|
||||||
|
.map(CtClass::getName)
|
||||||
|
.anyMatch(mapInterfaces::contains);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
@ -218,7 +216,8 @@ public class SynthSerializerFactory implements SerializerFactory {
|
||||||
return source;
|
return source;
|
||||||
}
|
}
|
||||||
|
|
||||||
private String createSubSerializerForReturnTypeAndAddInvocationToSource(CtClass classToSerialize, CtMethod getter, String source, CtClass returnType) {
|
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 */
|
/* 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(";
|
source += "\"+" + Serializer.class.getName() + ".toJSONString(";
|
||||||
|
|
|
||||||
87
src/main/resources/css/app.css
Normal file
87
src/main/resources/css/app.css
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
body {
|
||||||
|
background:white;
|
||||||
|
font: normal 12px/150% Arial, Helvetica, sans-serif;
|
||||||
|
padding:2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#callstack-view{
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid #006699;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#datagrid table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
text-align: left;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#datagrid {
|
||||||
|
padding: 10px;
|
||||||
|
font: normal 12px Arial, Helvetica, sans-serif;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #006699;
|
||||||
|
-webkit-border-radius: 3px;
|
||||||
|
-moz-border-radius: 3px;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#datagrid table thead th:first-child {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#datagrid table tbody tr {
|
||||||
|
color: #00496B;
|
||||||
|
border-left: 1px solid #E1EEF4;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
#datagrid table tbody .alt td {
|
||||||
|
background: #E1EEF4;
|
||||||
|
}
|
||||||
|
|
||||||
|
#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 tbody tr: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;
|
||||||
|
}
|
||||||
86
src/main/resources/css/tree.css
Normal file
86
src/main/resources/css/tree.css
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
.tree, .tree ul {
|
||||||
|
margin:0;
|
||||||
|
padding:0;
|
||||||
|
list-style:none
|
||||||
|
}
|
||||||
|
.tree ul {
|
||||||
|
margin-left:1em;
|
||||||
|
position:relative
|
||||||
|
}
|
||||||
|
.tree ul ul {
|
||||||
|
margin-left:.5em
|
||||||
|
}
|
||||||
|
.tree ul:before {
|
||||||
|
content:"";
|
||||||
|
display:block;
|
||||||
|
width:0;
|
||||||
|
position:absolute;
|
||||||
|
top:0;
|
||||||
|
bottom:0;
|
||||||
|
left:0;
|
||||||
|
border-left:1px solid
|
||||||
|
}
|
||||||
|
.tree li {
|
||||||
|
margin:0;
|
||||||
|
padding:0 1em;
|
||||||
|
line-height:2em;
|
||||||
|
color:#369;
|
||||||
|
position:relative
|
||||||
|
}
|
||||||
|
.tree ul li:before {
|
||||||
|
content:"";
|
||||||
|
display:block;
|
||||||
|
width:10px;
|
||||||
|
height:0;
|
||||||
|
border-top:1px solid;
|
||||||
|
margin-top:-1px;
|
||||||
|
position:absolute;
|
||||||
|
top:1em;
|
||||||
|
left:0
|
||||||
|
}
|
||||||
|
.tree ul li:last-child:before {
|
||||||
|
background:#fff;
|
||||||
|
height:auto;
|
||||||
|
top:1em;
|
||||||
|
bottom:0
|
||||||
|
}
|
||||||
|
.indicator {
|
||||||
|
margin-right:5px;
|
||||||
|
}
|
||||||
|
.tree li a {
|
||||||
|
text-decoration: none;
|
||||||
|
color:#369;
|
||||||
|
}
|
||||||
|
.tree li button, .tree li button:active, .tree li button:focus {
|
||||||
|
text-decoration: none;
|
||||||
|
color:#369;
|
||||||
|
border:none;
|
||||||
|
background:transparent;
|
||||||
|
margin:0;
|
||||||
|
padding:0;
|
||||||
|
outline: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.glyphicon {
|
||||||
|
position: relative;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.glyphicon-plus-sign::before {
|
||||||
|
content: "+";
|
||||||
|
}
|
||||||
|
|
||||||
|
.glyphicon-min-sign::before {
|
||||||
|
content: "-";
|
||||||
|
/*transform: translate(0,-5px);*/
|
||||||
|
}
|
||||||
|
|
||||||
|
.hidden{
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.visible{
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
25
src/main/resources/index.html
Normal file
25
src/main/resources/index.html
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Perfix</title>
|
||||||
|
<link rel="stylesheet" href="/css/app.css">
|
||||||
|
<link rel="stylesheet" href="/css/tree.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="main">
|
||||||
|
<div id="table-view">
|
||||||
|
<h1>List of invocations</h1>
|
||||||
|
<label for="name-filter">filter name </label><input id="name-filter" type="text" onkeyup="update_filter()">
|
||||||
|
<div id="datagrid"></div>
|
||||||
|
</div>
|
||||||
|
<div id="callstack-view">
|
||||||
|
<h1>Callstack</h1>
|
||||||
|
<div id="datatree" class="tree"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script type="application/ecmascript" src="/js/axios.min.js"></script>
|
||||||
|
<script type="application/ecmascript" src="/js/app.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
172
src/main/resources/js/app.js
Normal file
172
src/main/resources/js/app.js
Normal file
|
|
@ -0,0 +1,172 @@
|
||||||
|
let name_filter = "";
|
||||||
|
let datatable, callstack_data;
|
||||||
|
const datagrid = document.getElementById("datagrid");
|
||||||
|
const datatree = document.getElementById("datatree");
|
||||||
|
|
||||||
|
function refresh_data_grid() {
|
||||||
|
if (datagrid.firstChild) {
|
||||||
|
datagrid.removeChild(datagrid.firstChild);
|
||||||
|
}
|
||||||
|
|
||||||
|
const table = append_table(datagrid);
|
||||||
|
let row, name;
|
||||||
|
table.thead().tr()
|
||||||
|
.th("name")
|
||||||
|
.th("invocations")
|
||||||
|
.th("total duration")
|
||||||
|
.th("average");
|
||||||
|
const tbody = table.tbody();
|
||||||
|
for (let i = 0; i < datatable.data.length; i++) {
|
||||||
|
row = datatable.data[i];
|
||||||
|
name = row.name;
|
||||||
|
if (name_filter.length === 0 || name.includes(name_filter)) {
|
||||||
|
tbody.tr()
|
||||||
|
.td(name)
|
||||||
|
.td(row["invocations"])
|
||||||
|
.td(row["totalDuration"] / 1000000 + " ms")
|
||||||
|
.td(row["average"] / 1000000 + " ms");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function update_filter() {
|
||||||
|
name_filter = document.getElementById("name-filter").value;
|
||||||
|
if (datatable) {
|
||||||
|
refresh_data_grid();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function append_table(parent) {
|
||||||
|
const table = document.createElement("table");
|
||||||
|
parent.appendChild(table);
|
||||||
|
|
||||||
|
return {
|
||||||
|
thead: function () {
|
||||||
|
let thead = document.createElement("thead");
|
||||||
|
table.appendChild(thead);
|
||||||
|
return {
|
||||||
|
tr: function () {
|
||||||
|
let new_tr = document.createElement("tr");
|
||||||
|
thead.appendChild(new_tr);
|
||||||
|
|
||||||
|
let th = function (text) {
|
||||||
|
let new_td = document.createElement("th");
|
||||||
|
new_td.innerText = text;
|
||||||
|
new_tr.appendChild(new_td);
|
||||||
|
return {
|
||||||
|
th
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
th
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tbody: function () {
|
||||||
|
let tbody = document.createElement("tbody");
|
||||||
|
table.appendChild(tbody);
|
||||||
|
return {
|
||||||
|
tr: function () {
|
||||||
|
let new_tr = document.createElement("tr");
|
||||||
|
tbody.appendChild(new_tr);
|
||||||
|
|
||||||
|
let td = function (text) {
|
||||||
|
let new_td = document.createElement("td");
|
||||||
|
new_td.innerText = text;
|
||||||
|
new_tr.appendChild(new_td);
|
||||||
|
return {
|
||||||
|
td
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
td
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tr: function () {
|
||||||
|
let new_tr = document.createElement("tr");
|
||||||
|
table.appendChild(new_tr);
|
||||||
|
|
||||||
|
let td = function (text) {
|
||||||
|
let new_td = document.createElement("td");
|
||||||
|
new_td.innerText = text;
|
||||||
|
new_tr.appendChild(new_td);
|
||||||
|
return {
|
||||||
|
td
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
td
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// (function tabular_view() {
|
||||||
|
|
||||||
|
// }());
|
||||||
|
|
||||||
|
function appendChildren(parent, nodes) {
|
||||||
|
let node;
|
||||||
|
|
||||||
|
for (let i = 0; i < nodes.length; i++) {
|
||||||
|
node = nodes[i];
|
||||||
|
let li = document.createElement("li");
|
||||||
|
|
||||||
|
let branch = document.createElement("i");
|
||||||
|
if (node.children.length > 0) {
|
||||||
|
branch.setAttribute("class", "indicator glyphicon glyphicon-plus-sign");
|
||||||
|
branch.onclick = function (event) {
|
||||||
|
let icon = event.currentTarget.parentElement.firstChild;
|
||||||
|
let ul_to_toggle = icon.nextSibling.nextSibling;
|
||||||
|
|
||||||
|
if (ul_to_toggle.getAttribute("class") === "visible") {
|
||||||
|
icon.setAttribute("class", "indicator glyphicon glyphicon-plus-sign");
|
||||||
|
ul_to_toggle.setAttribute("class", "hidden");
|
||||||
|
} else {
|
||||||
|
icon.setAttribute("class", "indicator glyphicon glyphicon-min-sign");
|
||||||
|
ul_to_toggle.setAttribute("class", "visible");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
let label = document.createElement("a");
|
||||||
|
label.innerText = Math.floor(node["invocation"].duration / 1000) / 1000 + " ms " + node.name;
|
||||||
|
li.appendChild(branch);
|
||||||
|
li.appendChild(label);
|
||||||
|
|
||||||
|
let ul = document.createElement("ul");
|
||||||
|
ul.setAttribute("class", "hidden");
|
||||||
|
li.appendChild(ul);
|
||||||
|
|
||||||
|
appendChildren(ul, node.children);
|
||||||
|
|
||||||
|
parent.appendChild(li);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function refresh_data_tree() {
|
||||||
|
let datatree = document.getElementById("datatree");
|
||||||
|
let new_div = document.createElement("div");
|
||||||
|
new_div.setAttribute("class", "callstack-tree");
|
||||||
|
datatree.appendChild(new_div);
|
||||||
|
appendChildren(new_div, callstack_data);
|
||||||
|
}
|
||||||
|
|
||||||
|
(function main() {
|
||||||
|
update_filter();
|
||||||
|
|
||||||
|
axios.get('http://localhost:2048/report')
|
||||||
|
.then(response => {
|
||||||
|
datatable = response;
|
||||||
|
refresh_data_grid();
|
||||||
|
});
|
||||||
|
|
||||||
|
axios.get('http://localhost:2048/callstack')
|
||||||
|
.then(response => {
|
||||||
|
callstack_data = response.data;
|
||||||
|
refresh_data_tree();
|
||||||
|
});
|
||||||
|
}());
|
||||||
|
|
||||||
3
src/main/resources/js/axios.min.js
vendored
Normal file
3
src/main/resources/js/axios.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
src/main/resources/js/axios.min.map
Normal file
1
src/main/resources/js/axios.min.map
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -68,7 +68,7 @@ public class App {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (level < 1000) {
|
if (level < 10) {
|
||||||
someOtherMethod(level + 1);
|
someOtherMethod(level + 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue