<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>SPX Report</title>
<link rel="stylesheet" href="?SPX_UI_URI=/css/main.css">
</head>
<body>
<div id="init-report">
<div>
<h2>Initializing...</h2>
<p></p>
<div class="progress">
<div style="width: 0%"></div>
</div>
<p></p>
</div>
</div>
<div style="display: flex; flex-direction: column; height: 100%">
<div style="flex: 0 0 24px; display: flex; flex-direction: row;">
<div id="metric-selector" class="widget" style="flex: 0 0 120px;">
<select></select>
</div>
<div id="colorscheme-manager" class="widget" style="flex: 0 0 220px;"></div>
<div id="category-legend" class="widget"></div>
<div id="color-scale" class="widget" style="flex: 1 1 150px;"></div>
</div>
<div id="overview" class="widget visualization" style="flex: 0 0 80px"></div>
<div style="flex:1; min-height: 0; display: flex; flex-direction: column;">
<div id="timeline" class="widget visualization" style="flex: 0 0 auto; height: 300px"></div>
<div
style="flex: 0 0 3px; width: 100%; background-color: #444; cursor: row-resize;"
data-layout-splitter-target="timeline"
data-layout-splitter-axis="y"
data-layout-splitter-dir="-1"
data-layout-splitter-min="30"
></div>
<div style="flex: 1; min-height: 0; display: flex; flex-direction: row;">
<div id="flatprofile" class="widget" style="flex: 0 0 auto; width: 900px"></div>
<div
style="flex: 0 0 3px; height: 100%; background-color: #444; cursor: col-resize;"
data-layout-splitter-target="flatprofile"
data-layout-splitter-axis="x"
data-layout-splitter-dir="-1"
data-layout-splitter-min="500"
></div>
<div id="flamegraph" class="widget visualization" style="flex: 1 1 auto"></div>
</div>
</div>
</div>
<script
src="https://code.jquery.com/jquery-3.2.1.min.js"
integrity="sha256-hwg4gsxgFZhOsEEamdOYGBf13FyQuiTwlAQgxVSNgt4="
crossorigin="anonymous"
></script>
<script
src="https://cdnjs.cloudflare.com/ajax/libs/jscolor/2.0.4/jscolor.min.js"
integrity="sha256-CJWfUCeP3jLdUMVNUll6yQx37gh9AKmXTRxvRf7jzro="
crossorigin="anonymous"
></script>
<!-- Required workaround for Firefox to fix a "no credentials" issue -->
<script type="module" crossorigin src="?SPX_UI_URI=/js/fmt.js"></script>
<script type="module" crossorigin src="?SPX_UI_URI=/js/profileData.js"></script>
<script type="module" crossorigin src="?SPX_UI_URI=/js/widget.js"></script>
<script type="module" crossorigin src="?SPX_UI_URI=/js/layoutSplitter.js"></script>
<script type="module" crossorigin>
import * as fmt from './?SPX_UI_URI=/js/fmt.js';
import {ProfileDataBuilder} from './?SPX_UI_URI=/js/profileData.js';
import * as widget from './?SPX_UI_URI=/js/widget.js';
import * as layoutSplitter from './?SPX_UI_URI=/js/layoutSplitter.js';
$(() => {
const m = window.location.href.match(/\bkey=([^&]+)/);
if (!m) {
return;
}
const key = m[1];
let profileDataBuilder = null;
const initDialog = {
container: $('#init-report'),
title: document.querySelector('#init-report p:nth-of-type(1)'),
progressInfo: document.querySelector('#init-report p:nth-of-type(2)'),
progress: document.querySelector('#init-report .progress'),
progressBar: document.querySelector('#init-report .progress > div'),
data: {}
};
function setProgress(title, current, total) {
initDialog.title.innerHTML = title;
if (!total) {
initDialog.progress.style.display = 'none';
return;
}
if (!(title in initDialog.data)) {
initDialog.data[title] = {
startCount: current,
startTime: new Date().getTime() / 1000,
};
}
initDialog.progress.style.display = 'block';
initDialog.progressBar.style.width = (
Math.round(100 * current / total) + '%'
);
const currentTime = new Date().getTime() / 1000;
const rate = (current - initDialog.data[title].startCount) / (currentTime - initDialog.data[title].startTime);
const remainingTime = Math.round(
(total - current) / rate
);
initDialog.progressInfo.innerText = (
`${fmt.pct(current / total)} (${fmt.quantity(current)}, ${fmt.quantity(rate)}/s) ETA: ${remainingTime}s`
);
}
fetch('?SPX_UI_URI=/data/metrics', {credentials: "same-origin"})
.then(response => response.json())
.then(response => {
profileDataBuilder = new ProfileDataBuilder(response.results);
return fetch('?SPX_UI_URI=/data/reports/metadata/' + key, {credentials: "same-origin"});
})
.then(response => {
if (!response.ok) {
throw new Error('Cannot load report metadata');
}
return response.json();
})
.then(metadata => {
profileDataBuilder.setMetadata(metadata);
return fetch('?SPX_UI_URI=/data/reports/get/' + key, {credentials: "same-origin"});
})
.then(response => {
if (!response.ok) {
throw new Error('Cannot load report');
}
let type = null;
let currentFunctionIdx = 0;
let lastTruncatedLine = '';
const decoder = new TextDecoder('ascii');
function readBuffer(buffer) {
if (buffer.length == 0) {
return;
}
const lines = decoder.decode(buffer).split("\n");
lines[0] = lastTruncatedLine + lines[0];
lastTruncatedLine = lines.pop();
for (const line of lines) {
if (line == '[events]') {
type = 'event';
continue;
}
if (line == '[functions]') {
type = 'function';
continue;
}
if (type == 'event') {
profileDataBuilder.addEvent(
line.split(' ').map(e => parseFloat(e))
);
continue;
}
if (type == 'function') {
profileDataBuilder.setFunctionName(
currentFunctionIdx++,
line
);
continue;
}
}
setProgress(
'Building call list... (1/2)<br>'
+ `${fmt.quantity(profileDataBuilder.getTotalCallCount())} function calls`
,
profileDataBuilder.getCurrentCallCount(),
profileDataBuilder.getTotalCallCount()
);
}
if (!response.body) {
return response.arrayBuffer().then(buffer => {
const blockSize = 256 * 1024;
const readBlocks = (offset, resolve) => {
if (buffer.byteLength - offset <= blockSize) {
readBuffer(new Uint8Array(buffer, offset));
return new Promise(() => resolve());
}
readBuffer(new Uint8Array(buffer, offset, blockSize));
setTimeout(() => readBlocks(offset + blockSize, resolve), 0);
}
return new Promise(resolve => {
setTimeout(() => readBlocks(0, resolve), 0);
});
});
}
const reader = response.body.getReader();
const streamHandler = status => {
if (status.value) {
readBuffer(status.value);
}
if (!status.done) {
return reader.read().then(streamHandler);
}
return new Promise(resolve => resolve());
}
return reader.read().then(streamHandler);
})
.then(() => profileDataBuilder.buildCallRangeTree(
(current, total) => setProgress(
`Building call range tree... (2/2)<br>${fmt.quantity(total)} function calls`,
current,
total
)
))
.then(() => {
initDialog.title.innerText = 'Initializing widgets...';
return new Promise(resolve => resolve(profileDataBuilder.getProfileData()));
})
.then(profileData => {
const metricSelector = $('#metric-selector select');
for (const metric of profileData.getMetadata().enabled_metrics) {
metricSelector.append(
`<option value="${metric}">${profileData.getMetricInfo(metric).name}</option>`
);
}
const widgets = [
new widget.ColorScale($('#color-scale'), profileData),
new widget.CategoryLegend($('#category-legend'), profileData),
new widget.ColorSchemeManager($('#colorscheme-manager'), profileData),
new widget.OverView($('#overview'), profileData),
new widget.TimeLine($('#timeline'), profileData),
new widget.FlatProfile($('#flatprofile'), profileData),
new widget.FlameGraph($('#flamegraph'), profileData),
];
widgets.forEach(widget => widget.render());
metricSelector.on('change', () => {
widgets.forEach(widget => widget.setCurrentMetric(metricSelector.val()));
widgets.forEach(widget => widget.repaint());
});
layoutSplitter.init();
layoutSplitter.change(() => {
widgets.forEach(widget => {
if (!["timeline", "flatprofile", "flamegraph"].includes(widget.container.attr("id"))) {
return;
}
widget.handleResize();
});
});
return new Promise(resolve => resolve());
})
.then(() => initDialog.container.fadeOut(500))
.catch(e => {
initDialog.progressInfo.innerText = e;
console.error(e)
})
;
});
</script>
</body>
</html>