Skip to content

Commit

Permalink
Deploy JFR analyzer
Browse files Browse the repository at this point in the history
  • Loading branch information
ci-build committed Mar 13, 2024
1 parent d5f0838 commit 881c8be
Show file tree
Hide file tree
Showing 2 changed files with 1,851 additions and 0 deletions.
366 changes: 366 additions & 0 deletions flightrecording-analyzer/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,366 @@
<!--
Copyright 2017 Activeviam
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<html>
<head>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
<script src="https://code.jquery.com/jquery-3.2.1.js"></script>
<style>
body { color : #333; }
table {
margin: 10px auto;
border-collapse: separate;
border-spacing: 5px 5px;
}
th { border-bottom: 1px solid #333 }
.stacks {
display : none;
width : 100%;
background : #333;
color : #eee;
height : 300px;
overflow-y: scroll;
position: absolute;
top: 5px;
z-index: 1;
}
td {
position : relative;
}
tr:hover .stacks {
display : block;
}
.stacks th {
border-bottom: 1px solid #eee;
}

form {
margin: auto;
}
.uploadcsv {
margin:20px auto;
width:80%;
}
.uploadcsv textarea {
margin: auto;
width: 100%;
height: 100px;
}
.uploadcsv button {
margin: 10px 45%;
}

.loadbar {
margin: 0 auto;
width: 80%;
height: 30px;
background: #ddd;
position : relative;
}

.loadbar .percent {
color: #333;
position : absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-weight: bold;
}

.loadbar .progress {
height: 30px;
width: 0%;
background: #00aeef;
}

</style>
<script>

let MAX_STACK_DEPTH = 100; /* Lower => faster, but we loose more info*/
let FILTERS = [/ForkJoinTask/g, /ForkJoinPool/g, /ForkJoinWorkerThread/g];

let filterOut = function(name) {
let res = false;
FILTERS.forEach(function(filter) {
if(name.match(filter)) {
res = true;
}
});
return res;
}

function numberOfSpacesInString(text) {
var count = 0;
var index = 0;
while (text.charAt(index++) === " ") {
count++;
}
return count;
}

let stacksTable = function(stacksNode, depth = 0) {
if(!stacksNode || depth > 0) {
// Note that some updates need to be made to support depth > 0
return "";
}
let firstLevelCallers = [];
for(name in stacksNode.children) {
firstLevelCallers.push(stacksNode.children[name]);
}
firstLevelCallers.sort(function(caller1, caller2) {
return caller2.samples - caller1.samples;
});

let stacksHTML = `
<table class="stacks">
<tr>
<th>Method <font size="-1">(Caller if using Call Tree, Callee if using Hot Methods)</font> </th>
<th>Sample Count</th>
</tr>`;
firstLevelCallers.forEach(function(caller) {
if(caller) {
if(filterOut(caller.name)) {
return;
}
stacksHTML += "<tr><td><div>"+caller.name+"</div>"
+ stacksTable(stacksNode.children[caller.name], depth+1)
+ "</td><td>" + caller.samples + "</td></tr>";
}
});
stacksHTML += "</table>"
return stacksHTML;
}

/** Stacks is a tree
stacks.root is the node
Node {
String name,
Map<String, Node> children,
int samples
}
stack is and array of methods: [
{
String name,
int samples
}
]
*/
let mergeStackTraces = function(stacks, stack, samples) {
if(!stacks.root) {
stacks.root = { children: {}, samples: 0};
}
stacks.root.samples += samples;
let currentNode = stacks.root;
let depth = 0;
stack.forEach(method => {
if(depth > MAX_STACK_DEPTH) {
// We avoid going too deep to save memory.
return;
}
if(!currentNode.children[method.name]) {
currentNode.children[method.name] = { children: {}, name: method.name, samples: 0};
}
currentNode = currentNode.children[method.name];
currentNode.samples += method.samples;
depth++;
});

}

let parseCsv = function(content, onParsed) {
let profileData = {};
let methods = [];
let lines = content.split("\n");
let stackSize = lines.length;
let currentCallers = [];
let i=0;
let start = new Date().getTime();
let prevDepth = 0;
$(".analyze").prepend(`
<div class="loadbar">
<div class="progress"></div>
<p class="percent">0%</p>
</div>`);

let GROUP_SIZE = 40;
let group = [];
let processGroup = function() {
let localI = i;
let localGroup = group.slice(0);
setTimeout(function() {
for(let splitLine of localGroup) {
let columns = splitLine.columns;
let format = splitLine.format;

let methodName = format(columns[0]).trim();
let depth = numberOfSpacesInString(format(columns[0])) / 3;
let samples = format(columns[1]).replace(",","") * 1;
let percent = format(columns[2]);

if(!(methodName in profileData)) {
profileData[methodName] = {
name: methodName,
samples: 0,
totalDepth: 0, // The cumulated depth at which the method was found, for all samples
count: 0, // The number of times a method was found
stacks: {}
};
}
profileData[methodName].samples += samples;
profileData[methodName].totalDepth += depth;
profileData[methodName].count += 1;
for(let j = 0; j < prevDepth - depth ; ++j) {
currentCallers.pop();
}
mergeStackTraces(profileData[methodName].stacks, currentCallers.slice(0).reverse(), samples);
currentCallers.push({
name: methodName,
depth: depth,
samples: samples
});

if(new Date().getTime() - start > 100 ) {
let percent = (100.0*localI/(stackSize*1.0));
$('.percent').html(percent.toFixed(0)+"%");
$('.progress').css({"width": percent+"%"});
}

prevDepth = depth;
}
}, 0);
}

for(let line of lines) {
if(!line) { // The line is empty
continue;
}
let columns = line.split(";");
let format = col => col.slice(1,-1);
if(columns.length <= 1) {
columns = line.split("\t");
format = col => col; // Raw format, simply return the column
}

let methodName = format(columns[0]).trim();
if(methodName === "Stack Trace") { // This is the header line (May or may not be present)
continue;
}
group.push({columns: columns, format: format});
if(group.length > GROUP_SIZE) {
processGroup();
group = [];
}
i++;
}

processGroup();
setTimeout(function() {
$('.progress').css({"width": "100%"});
$('.percent').html("100%");

for (let methodName in profileData) {
methods.push(profileData[methodName]);
}
methods.sort(function(method1, method2) {
return method2.samples - method1.samples;
});

onParsed(methods);
}, 0);
}

/**
* Retrieve the maximum average depth at which a method was found.
* This average can then be used to color the methods.
*/
let getMaxAverageDepth = function(methods) {
let max = 0;
methods.forEach(function(method) {
let averageDepth = ((1.0 * method.totalDepth) / method.count);
if(averageDepth > max) {
max = averageDepth;
}
});
return max;
}

let createPage = function(methods) {
let html = `
<table class="methods">
<tr>
<th>Method <font size="-1">(Colored by depth: the redder, the deeper)</font></th>
<th>Sample Count</th>
</tr>`;
let maxAverageDepth = getMaxAverageDepth(methods);
methods.forEach(function(method) {
if(filterOut(method.name)) {
return;
}
let averageDepth = ((1.0 * method.totalDepth) / method.count);
// We color the deepest methods in red, as they are probably more interesting
let red = Math.round(255.0 * averageDepth / (maxAverageDepth));
html += "<tr><td><div style=\"color:rgb("+red+",0,0)\">"+method.name+"</div>"
+ stacksTable(method.stacks.root)
+ "</td><td>"+method.samples+"</td></tr>";
});
html += "</table>";

$(".analyze").append(html);
$("tr").on('click auxclick contextmenu', function(e) {
// On right click or middle click, hide the element.
if(e.type === "contextmenu" || e.which === 2 || e.which === 3) {
e.preventDefault();
e.target.parentElement.parentElement.style.display = "none";
}
});
};

let parseNewCsv = function() {
parseCsv(document.getElementById("csv").value, function(methods) {
// Parsing has been done. Lets update the HTML
$(".analyze").empty();
document.getElementById("csv").value = "";
createPage(methods);
});

};

$(document).ready(function() {
$("#submitcsv").on("click", parseNewCsv);
});
</script>

</head>
<body>
<div class="uploadcsv">
<h1>Online Java Flight Recording Analyzer</h1>
<p>
In your Flight Recording, go in the Call Tree or Hot Methods tab, expand
some methods fully, and copy and paste the stacks in this text area.<br>
<font size="-2">Note that we support both CSV and Raw export.</font>
</p>
<textarea id="csv" name="CSVFlightRecording" form="csv"></textarea>
<br>
<button type="submit" id="submitcsv">Analyze</button>
<p><font size="-1">This page is client-side only, and no data will leave your computer when you click Analyze.</font></p>
</div>
<div class="analyze">

</div>
</body>




Loading

0 comments on commit 881c8be

Please sign in to comment.