* removed React for the UI

* removed TinyHttpd for the backend service
* fixed errors
* reduced footprint
* improved performance and responsiveness
This commit is contained in:
Sander Hautvast 2020-12-29 18:06:19 +01:00
parent b814c44a0c
commit 29e71c0f96
12 changed files with 520 additions and 82 deletions

View file

@ -33,4 +33,14 @@ Pretty basic profiling tool for JVM's
# DISCLAIMER:
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.

View file

@ -19,24 +19,17 @@
<artifactId>javassist</artifactId>
<version>3.26.0-GA</version>
</dependency>
<dependency>
<groupId>org.nanohttpd</groupId>
<artifactId>nanohttpd</artifactId>
<version>2.3.1</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.1</version>
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-dbcp2 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-dbcp2</artifactId>
<version>2.4.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.h2database</groupId>

View file

@ -1,76 +1,138 @@
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.server.json.Serializer;
import java.io.ByteArrayInputStream;
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.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) {
super(port);
this.port = port;
}
public void start() {
try {
start(NanoHTTPD.SOCKET_READ_TIMEOUT, false);
System.out.println(" --- Perfix http server running. Point your browser to http://localhost:" + getListeningPort() + "/");
HttpServer server = HttpServer.create(new InetSocketAddress("localhost", port), 0);
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) {
System.err.println(" --- Couldn't start Perfix http server:\n" + ioe);
}
}
@Override
public Response serve(IHTTPSession session) {
String uri = session.getUri();
public void handle(HttpExchange exchange) throws IOException {
String uri = exchange.getRequestURI().toString();
InputStream response = null;
switch (uri) {
case "/report":
return perfixMetrics();
setContentTypeJson(exchange);
response = toStream(perfixMetrics());
break;
case "/callstack":
return perfixCallstack();
setContentTypeJson(exchange);
response = toStream(perfixCallstack());
break;
case "/clear":
return clear();
setContentTypeJson(exchange);
response = toStream(clear());
break;
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 {
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) {
e.printStackTrace();
return newFixedLengthResponse(e.toString());
log.severe(e.toString());
return e.toString();
}
}
private Response addCors(Response response) {
response.addHeader("Access-Control-Allow-Origin", "*");
response.addHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
return response;
private InputStream toStream(String text) {
return new ByteArrayInputStream(text.getBytes(StandardCharsets.UTF_8));
}
private Response perfixCallstack() {
private String perfixCallstack() {
try {
return addCors(newFixedLengthResponse(Response.Status.OK, "application/json", Serializer.toJSONString(Registry.getCallStack())));
return Serializer.toJSONString(Registry.getCallStack());
} catch (Exception e) {
e.printStackTrace();
return newFixedLengthResponse(e.toString());
log.severe(e.toString());
return e.toString();
}
}
private Response clear() {
private String clear() {
Registry.clear();
try {
return addCors(newFixedLengthResponse(Response.Status.OK, "application/json", Serializer.toJSONString(Registry.getCallStack())));
return Serializer.toJSONString(Registry.getCallStack());
} catch (Exception e) {
e.printStackTrace();
return newFixedLengthResponse(e.toString());
log.severe(e.toString());
return e.toString();
}
}
}

View file

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

View file

@ -3,8 +3,11 @@ package perfix.server.json;
import javassist.*;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import static java.util.Arrays.asList;
import static java.util.Collections.unmodifiableSet;
public class SynthSerializerFactory implements SerializerFactory {
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 INTEGER = "java.lang.Integer";
private final static Set<String> wrappersAndString = new HashSet<String>(asList(BOOLEAN, CHARACTER, BYTE, DOUBLE, FLOAT, LONG, SHORT, INTEGER,
STRING));
private final static Set<String> wrappersAndString = unmodifiableSet(new HashSet<String>(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 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 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")
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 | ReflectiveOperationException e) {
throw new SerializerCreationException(e);
}
}
public <T> JSONSerializer<T> createSerializer(Class<T> beanjavaClass) {
String serializerName = createSerializerName(beanjavaClass);
return (JSONSerializer<T>) serializers.computeIfAbsent(serializerName, key -> {
try {
CtClass beanClass = pool.get(beanjavaClass.getName());
CtClass serializerClass = pool.makeClass(serializerName, serializerBase);
private <T> JSONSerializer<T> tryCreateSerializer(CtClass beanClass) throws NotFoundException, CannotCompileException, ReflectiveOperationException {
CtClass serializerClass = pool.makeClass(createSerializerName(beanClass), serializerBase);
addToJsonStringMethod(beanClass, serializerClass);
addToJsonStringMethod(beanClass, serializerClass);
return createSerializerInstance(serializerClass);
JSONSerializer<T> jsonSerializer = createSerializerInstance(serializerClass);
serializers.put(createSerializerName(beanClass), jsonSerializer);
return jsonSerializer;
} catch (NotFoundException | CannotCompileException | ReflectiveOperationException e) {
e.printStackTrace();
throw new SerializerCreationException(e);
}
});
}
/*
* 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);
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.
*/
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;
source += "\treturn ";
source += "\"{";
@ -161,8 +153,9 @@ public class SynthSerializerFactory implements SerializerFactory {
}
@SuppressWarnings("unchecked")
private <T> JSONSerializer<T> createSerializerInstance(CtClass serializerClass) throws CannotCompileException, ReflectiveOperationException {
return (JSONSerializer<T>) serializerClass.toClass().getConstructor().newInstance();
private <T> JSONSerializer<T> createSerializerInstance(CtClass serializerClass) throws
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
*/
public String createSerializerName(CtClass beanClass) {
public String createSerializerName(Class<?> beanClass) {
return createSerializerName(beanClass.getName());
}
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 {
List<CtClass> interfaces = new ArrayList<>(asList(beanClass.getInterfaces()));
interfaces.add(beanClass);
boolean is = interfaces.stream().anyMatch(interfaze -> interfaze.getName().equals(COLLECTION) || interfaze.getName().equals(LIST) || interfaze.getName().equals(SET));
return is;
return interfaces.stream()
.map(CtClass::getName)
.anyMatch(interfaze -> interfaze.equals(COLLECTION) || interfaze.equals(LIST) || interfaze.equals(SET));
}
private boolean isMap(CtClass beanClass) throws NotFoundException {
List<CtClass> interfaces = new ArrayList<>(asList(beanClass.getInterfaces()));
interfaces.add(beanClass);
return interfaces.stream().anyMatch(i -> mapInterfaces.contains(i.getName()));
if (mapInterfaces.contains(beanClass.getName())) {
return true;
} else {
return Arrays.stream(beanClass.getInterfaces())
.map(CtClass::getName)
.anyMatch(mapInterfaces::contains);
}
}
/*
@ -218,7 +216,8 @@ public class SynthSerializerFactory implements SerializerFactory {
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 */
source += "\"+" + Serializer.class.getName() + ".toJSONString(";

View 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;
}

View 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;
}

View 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>

View 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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -68,7 +68,7 @@ public class App {
e.printStackTrace();
}
if (level < 1000) {
if (level < 10) {
someOtherMethod(level + 1);
}
}