-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
ci-build
committed
Mar 13, 2024
1 parent
d5f0838
commit 881c8be
Showing
2 changed files
with
1,851 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
|
||
|
||
|
||
|
Oops, something went wrong.