Commit 262a1078 authored by Igor Sheludko's avatar Igor Sheludko Committed by Commit Bot

[zone-stats] Add a UI for exploring zone memory usage stats

... collected via --trace-zone-stats flag or v8.zone_stats trace
category.

This is an initial version inspired by heap-stats UI.

Bug: v8:10572
Change-Id: Ib87cf0b4e120bc99683227eef02668a2a5c3d594
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/2226855Reviewed-by: 's avatarCamillo Bruni <cbruni@chromium.org>
Commit-Queue: Igor Sheludko <ishell@chromium.org>
Cr-Commit-Position: refs/heads/master@{#68133}
parent 70eb0898
// Copyright 2020 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
const UNCLASSIFIED_CATEGORY = 'unclassified';
const UNCLASSIFIED_CATEGORY_NAME = 'Unclassified';
// Categories for zones.
export const CATEGORIES = new Map([
[
'parser', new Set([
'AstStringConstants',
'ParseInfo',
'Parser',
])
],
[
'misc', new Set([
'Run',
'CanonicalHandleScope',
'Temporary scoped zone',
'UpdateFieldType',
])
],
[
'interpreter', new Set([
'InterpreterCompilationJob',
])
],
[
'regexp', new Set([
'CompileIrregexp',
])
],
[
'compiler-huge', new Set([
'graph-zone',
'instruction-zone',
'pipeline-compilation-job-zone',
'register-allocation-zone',
'register-allocator-verifier-zone',
])
],
[
'compiler-other', new Set([
'Compile',
'V8.TFAllocateFPRegisters',
'V8.TFAllocateGeneralRegisters',
'V8.TFAssembleCode',
'V8.TFAssignSpillSlots',
'V8.TFBuildLiveRangeBundles',
'V8.TFBuildLiveRanges',
'V8.TFBytecodeGraphBuilder',
'V8.TFCommitAssignment',
'V8.TFConnectRanges',
'V8.TFControlFlowOptimization',
'V8.TFDecideSpillingMode',
'V8.TFDecompressionOptimization',
'V8.TFEarlyOptimization',
'V8.TFEarlyTrimming',
'V8.TFEffectLinearization',
'V8.TFEscapeAnalysis',
'V8.TFFinalizeCode',
'V8.TFFrameElision',
'V8.TFGenericLowering',
'V8.TFHeapBrokerInitialization',
'V8.TFInlining',
'V8.TFJumpThreading',
'V8.TFLateGraphTrimming',
'V8.TFLateOptimization',
'V8.TFLoadElimination',
'V8.TFLocateSpillSlots',
'V8.TFLoopPeeling',
'V8.TFMachineOperatorOptimization',
'V8.TFMeetRegisterConstraints',
'V8.TFMemoryOptimization',
'V8.TFOptimizeMoves',
'V8.TFPopulatePointerMaps',
'V8.TFResolveControlFlow',
'V8.TFResolvePhis',
'V8.TFScheduling',
'V8.TFSelectInstructions',
'V8.TFSerializeMetadata',
'V8.TFSimplifiedLowering',
'V8.TFStoreStoreElimination',
'V8.TFTypedLowering',
'V8.TFTyper',
'V8.TFUntyper',
'V8.TFVerifyGraph',
'ValidatePendingAssessment',
'codegen-zone',
])
],
[UNCLASSIFIED_CATEGORY, new Set()],
]);
// Maps category to description text that is shown in html.
export const CATEGORY_NAMES = new Map([
['parser', 'Parser'],
['misc', 'Misc'],
['interpreter', 'Ignition'],
['regexp', 'Regexp compiler'],
['compiler-huge', 'TurboFan (huge zones)'],
['compiler-other', 'TurboFan (other zones)'],
[UNCLASSIFIED_CATEGORY, UNCLASSIFIED_CATEGORY_NAME],
]);
function buildZoneToCategoryMap() {
const map = new Map();
for (let [category, zone_names] of CATEGORIES.entries()) {
for (let zone_name of zone_names) {
if (map.has(zone_name)) {
console.error("Zone belongs to multiple categories: " + zone_name);
} else {
map.set(zone_name, category);
}
}
}
return map;
}
const CATEGORY_BY_ZONE = buildZoneToCategoryMap();
// Maps zone name to category.
export function categoryByZoneName(zone_name) {
const category = CATEGORY_BY_ZONE.get(zone_name);
if (category !== undefined) return category;
return UNCLASSIFIED_CATEGORY;
}
<!-- Copyright 2020 the V8 project authors. All rights reserved.
Use of this source code is governed by a BSD-style license that can be
found in the LICENSE file. -->
<style>
#dataSelectionSection {
display: none;
}
.box {
border-left: dashed 1px #666666;
border-right: dashed 1px #666666;
border-bottom: dashed 1px #666666;
padding: 10px;
overflow: hidden;
position: relative;
}
.box:nth-of-type(1) {
border-top: dashed 1px #666666;
border-radius: 5px 5px 0px 0px;
}
.box:last-of-type {
border-radius: 0px 0px 5px 5px;
}
.box > ul {
margin: 0px;
padding: 0px;
}
.box > ul > li {
display: inline-block;
}
.box > ul > li:not(:first-child) {
margin-left: 10px;
}
.box > ul > li:first-child {
font-weight: bold;
}
.zonesSelectBox {
position: relative;
overflow: hidden;
float: left;
padding: 0px 5px 2px 0px;
margin: 3px;
border-radius: 3px;
}
.zonesSelectBox > label {
font-size: xx-small;
}
.zonesSelectBox > input {
vertical-align: middle;
}
.percentBackground {
position: absolute;
width: 200%;
height: 100%;
left: 0%;
top: 0px;
margin-left: -100%;
transition: all 1s ease-in-out;
}
.zonesSelectBox > .percentBackground {
background: linear-gradient(90deg, #68b0f7 50%, #b3d9ff 50%);
z-index: -1;
}
.box > .percentBackground {
background: linear-gradient(90deg, #e0edfe 50%, #fff 50%);
z-index: -2;
}
#categories {
margin-top: 10px;
}
#category-filter {
text-align: right;
width: 50px;
}
</style>
<section id="dataSelectionSection">
<h2>Data selection</h2>
<ul>
<li>
<label for="isolate-select">
Isolate
</label>
<select id="isolate-select">
<option>No data</option>
</select>
</li>
<li>
<label for="data-view-select">
Data view
</label>
<select id="data-view-select">
<option>No data</option>
</select>
</li>
<li>
<label for="show-totals-select">
Show total allocated/used zone memory
</label>
<input type="checkbox" id="show-totals-select" checked>
</li>
<li>
<label for="data-kind-select">
Data kind
</label>
<select id="data-kind-select">
<option>No data</option>
</select>
</li>
<li>
<label for="time-start-select">
Time start
</label>
<input type="number" id="time-start-select" value="0">ms</input>
</li>
<li>
<label for="time-end-select">
Time end
</label>
<input type="number" id="time-end-select" value="0">ms</input>
</li>
<li>
<label for="memory-usage-sample-select">
Memory usage sample (at a specific time in ms)
</label>
<select id="memory-usage-sample-select">
<option>No data</option>
</select>
</li>
</ul>
<div id="categories"></div>
</section>
This diff is collapsed.
<!-- Copyright 2020 the V8 project authors. All rights reserved.
Use of this source code is governed by a BSD-style license that can be
found in the LICENSE file. -->
<style>
#chart {
width: 100%;
height: 500px;
}
</style>
<div id="container" style="display: none;">
<h2>Stats</h2>
<p>Peak allocated zone memory <span id="peak-memory-label"></span></p>
<h2>Timeline</h2>
<div id="chart"></div>
</div>
// Copyright 2020 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
'use strict';
import {categoryByZoneName} from './categories.js';
import {
VIEW_TOTALS,
VIEW_BY_ZONE_NAME,
VIEW_BY_ZONE_CATEGORY,
KIND_ALLOCATED_MEMORY,
KIND_USED_MEMORY,
} from './details-selection.js';
defineCustomElement('global-timeline', (templateText) =>
class GlobalTimeline extends HTMLElement {
constructor() {
super();
const shadowRoot = this.attachShadow({mode: 'open'});
shadowRoot.innerHTML = templateText;
}
$(id) {
return this.shadowRoot.querySelector(id);
}
set data(value) {
this._data = value;
this.stateChanged();
}
get data() {
return this._data;
}
set selection(value) {
this._selection = value;
this.stateChanged();
}
get selection() {
return this._selection;
}
isValid() {
return this.data && this.selection;
}
hide() {
this.$('#container').style.display = 'none';
}
show() {
this.$('#container').style.display = 'block';
}
stateChanged() {
if (this.isValid()) {
const isolate_data = this.data[this.selection.isolate];
const peakAllocatedMemory = isolate_data.peakAllocatedMemory;
this.$('#peak-memory-label').innerText = formatBytes(peakAllocatedMemory);
this.drawChart();
} else {
this.hide();
}
}
getZoneLabels(zone_names) {
switch (this.selection.data_kind) {
case KIND_ALLOCATED_MEMORY:
return zone_names.map(name => {
return {label: name + " (allocated)", type: 'number'};
});
case KIND_USED_MEMORY:
return zone_names.map(name => {
return {label: name + " (used)", type: 'number'};
});
default:
// Don't show detailed per-zone information.
return [];
}
}
getTotalsData() {
const isolate_data = this.data[this.selection.isolate];
const labels = [
{ label: "Time", type: "number" },
{ label: "Total allocated", type: "number" },
{ label: "Total used", type: "number" },
];
const chart_data = [labels];
const timeStart = this.selection.timeStart;
const timeEnd = this.selection.timeEnd;
const filter_entries = timeStart > 0 || timeEnd > 0;
for (const [time, zone_data] of isolate_data.samples) {
if (filter_entries && (time < timeStart || time > timeEnd)) continue;
const data = [];
data.push(time * kMillis2Seconds);
data.push(zone_data.allocated / KB);
data.push(zone_data.used / KB);
chart_data.push(data);
}
return chart_data;
}
getZoneData() {
const isolate_data = this.data[this.selection.isolate];
const zone_names = isolate_data.sorted_zone_names;
const selected_zones = this.selection.zones;
const data_kind = this.selection.data_kind;
const show_totals = this.selection.show_totals;
const zones_labels = this.getZoneLabels(zone_names);
const totals_labels = show_totals
? [
{ label: "Total allocated", type: "number" },
{ label: "Total used", type: "number" },
]
: [];
const labels = [
{ label: "Time", type: "number" },
...totals_labels,
...zones_labels,
];
const chart_data = [labels];
const timeStart = this.selection.timeStart;
const timeEnd = this.selection.timeEnd;
const filter_entries = timeStart > 0 || timeEnd > 0;
for (const [time, zone_data] of isolate_data.samples) {
if (filter_entries && (time < timeStart || time > timeEnd)) continue;
const active_zone_stats = Object.create(null);
if (zone_data.zones !== undefined) {
for (const [zone_name, zone_stats] of zone_data.zones) {
if (!selected_zones.has(zone_name)) continue; // Not selected, skip.
const current_stats = active_zone_stats[zone_name];
if (current_stats === undefined) {
active_zone_stats[zone_name] =
{ allocated: zone_stats.allocated, used: zone_stats.used };
} else {
// We've got two zones with the same name.
console.log("=== Duplicate zone names: " + zone_name);
// Sum stats.
current_stats.allocated += zone_stats.allocated;
current_stats.used += zone_stats.used;
}
}
}
const data = [];
data.push(time * kMillis2Seconds);
if (show_totals) {
data.push(zone_data.allocated / KB);
data.push(zone_data.used / KB);
}
if (zone_data.used > 30 * MB) {
console.log("BOOOM!!!! Zone usage in a sample is too big: " +
(zone_data.used / MB) + " MB");
}
zone_names.forEach(zone => {
const sample = active_zone_stats[zone];
let used = null;
let allocated = null;
if (sample !== undefined) {
used = sample.used / KB;
allocated = sample.allocated / KB;
}
if (data_kind == KIND_ALLOCATED_MEMORY) {
data.push(allocated);
} else {
// KIND_USED_MEMORY
data.push(used);
}
});
chart_data.push(data);
}
return chart_data;
}
getCategoryData() {
const isolate_data = this.data[this.selection.isolate];
const categories = Object.keys(this.selection.categories);
const categories_names =
categories.map(k => this.selection.category_names.get(k));
const selected_zones = this.selection.zones;
const data_kind = this.selection.data_kind;
const show_totals = this.selection.show_totals;
const categories_labels = this.getZoneLabels(categories_names);
const totals_labels = show_totals
? [
{ label: "Total allocated", type: "number" },
{ label: "Total used", type: "number" },
]
: [];
const labels = [
{ label: "Time", type: "number" },
...totals_labels,
...categories_labels,
];
const chart_data = [labels];
const timeStart = this.selection.timeStart;
const timeEnd = this.selection.timeEnd;
const filter_entries = timeStart > 0 || timeEnd > 0;
for (const [time, zone_data] of isolate_data.samples) {
if (filter_entries && (time < timeStart || time > timeEnd)) continue;
const active_category_stats = Object.create(null);
if (zone_data.zones !== undefined) {
for (const [zone_name, zone_stats] of zone_data.zones) {
const category = selected_zones.get(zone_name);
if (category === undefined) continue; // Zone was not selected.
const current_stats = active_category_stats[category];
if (current_stats === undefined) {
active_category_stats[category] =
{ allocated: zone_stats.allocated, used: zone_stats.used };
} else {
// Sum stats.
current_stats.allocated += zone_stats.allocated;
current_stats.used += zone_stats.used;
}
}
}
const data = [];
data.push(time * kMillis2Seconds);
if (show_totals) {
data.push(zone_data.allocated / KB);
data.push(zone_data.used / KB);
}
categories.forEach(category => {
const sample = active_category_stats[category];
let used = null;
let allocated = null;
if (sample !== undefined) {
used = sample.used / KB;
allocated = sample.allocated / KB;
}
if (data_kind == KIND_ALLOCATED_MEMORY) {
data.push(allocated);
} else {
// KIND_USED_MEMORY
data.push(used);
}
});
chart_data.push(data);
}
return chart_data;
}
getChartData() {
switch (this.selection.data_view) {
case VIEW_BY_ZONE_NAME:
return this.getZoneData();
case VIEW_BY_ZONE_CATEGORY:
return this.getCategoryData();
case VIEW_TOTALS:
default:
return this.getTotalsData();
}
}
getChartOptions() {
const options = {
isStacked: true,
interpolateNulls: true,
hAxis: {
format: '###.##s',
title: 'Time [s]',
},
vAxis: {
format: '#,###KB',
title: 'Memory consumption [KBytes]'
},
chartArea: {left:100, width: '85%', height: '70%'},
legend: {position: 'top', maxLines: '1'},
pointsVisible: true,
pointSize: 3,
explorer: {},
};
// Overlay total allocated/used points on top of the graph.
const series = {}
if (this.selection.data_view == VIEW_TOTALS) {
series[0] = {type: 'line', color: "red"};
series[1] = {type: 'line', color: "blue"};
} else if (this.selection.show_totals) {
series[0] = {type: 'line', color: "red", lineDashStyle: [13, 13]};
series[1] = {type: 'line', color: "blue", lineDashStyle: [13, 13]};
}
return Object.assign(options, {series: series});
}
drawChart() {
console.assert(this.data, 'invalid data');
console.assert(this.selection, 'invalid selection');
const chart_data = this.getChartData();
const data = google.visualization.arrayToDataTable(chart_data);
const options = this.getChartOptions();
const chart = new google.visualization.AreaChart(this.$('#chart'));
this.show();
chart.draw(data, google.charts.Line.convertOptions(options));
}
});
// Copyright 2020 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
const KB = 1024;
const MB = KB * KB;
const GB = MB * KB;
const kMillis2Seconds = 1 / 1000;
function formatBytes(bytes) {
const units = [' B', ' KB', ' MB', ' GB'];
const divisor = 1024;
let index = 0;
while (index < units.length && bytes >= divisor) {
index++;
bytes /= divisor;
}
return bytes.toFixed(2) + units[index];
}
function formatSeconds(millis) {
return (millis * kMillis2Seconds).toFixed(2) + 's';
}
function defineCustomElement(name, generator) {
let htmlTemplatePath = name + '-template.html';
fetch(htmlTemplatePath)
.then(stream => stream.text())
.then(templateText => customElements.define(name, generator(templateText)));
}
<!DOCTYPE html>
<!-- Copyright 2020 the V8 project authors. All rights reserved.
Use of this source code is governed by a BSD-style license that can be
found in the LICENSE file. -->
<html lang="en">
<head>
<meta charset="UTF-8">
<title>V8 Heap Statistics</title>
<link href='https://fonts.googleapis.com/css?family=Roboto' rel='stylesheet'>
<script
src="https://www.gstatic.com/charts/loader.js"></script>
<script
src="https://cdnjs.cloudflare.com/ajax/libs/pako/1.0.6/pako_inflate.js"
integrity1="sha256-N1z6ddQzX83fjw8v7uSNe7/MgOmMKdwFUv1+AJMDqNM="
crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/oboe.js/2.1.5/oboe-browser.js"
crossorigin="anonymous"></script>
<script src="helper.js"></script>
<script type="module" src="details-selection.js"></script>
<script type="module" src="global-timeline.js"></script>
<script type="module" src="trace-file-reader.js"></script>
<style>
body {
font-family: 'Roboto', sans-serif;
margin-left: 5%;
margin-right: 5%;
}
</style>
<script>
'use strict';
google.charts.load('current', {'packages':['line', 'corechart', 'bar']});
function $(id) { return document.querySelector(id); }
function removeAllChildren(node) {
while (node.firstChild) {
node.removeChild(node.firstChild);
}
}
let state = Object.create(null);
function globalDataChanged(e) {
state.data = e.detail;
// Emit one entry with the whole model for debugging purposes.
console.log(state.data);
state.selection = null;
$('#global-timeline').selection = state.selection;
$('#global-timeline').data = state.data;
$('#details-selection').data = state.data;
}
function globalSelectionChangedA(e) {
state.selection = e.detail;
console.log(state.selection);
$('#global-timeline').selection = state.selection;
}
</script>
</head>
<body>
<h1>V8 Zone memory usage Statistics</h1>
<trace-file-reader onchange="globalDataChanged(event)"></trace-file-reader>
<details-selection id="details-selection" onchange="globalSelectionChangedA(event)"></details-selection>
<global-timeline id="global-timeline"></global-timeline>
<p>Visualize zone usage profile and statistics that have been gathered using</p>
<ul>
<li><code>--trace-zone-stats</code> on V8</li>
<li>
<a
href="https://www.chromium.org/developers/how-tos/trace-event-profiling-tool">Chrome's
tracing infrastructure</a> collecting data for the category
<code>v8.zone_stats</code>.
</li>
</ul>
<p>
Note that the visualizer needs to run on a web server due to HTML imports
requiring <a
href="https://en.wikipedia.org/wiki/Cross-origin_resource_sharing">CORS</a>.
</p>
</body>
</html>
// Copyright 2020 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
'use strict';
export class Isolate {
constructor(address) {
this.address = address;
this.start = null;
this.end = null;
this.peakUsageTime = null;
// Maps zone name to per-zone statistics.
this.zones = new Map();
// Zone names sorted by memory usage (from low to high).
this.sorted_zone_names = [];
// Maps time to total and per-zone memory usages.
this.samples = new Map();
this.peakAllocatedMemory = 0;
// Maps zone name to their max memory consumption.
this.zonePeakMemory = Object.create(null);
// Peak memory consumed by a single zone.
this.singleZonePeakMemory = 0;
}
finalize() {
this.samples.forEach(sample => this.finalizeSample(sample));
this.start = Math.floor(this.start);
this.end = Math.ceil(this.end);
this.sortZoneNamesByPeakMemory();
}
getLabel() {
let label = `${this.address}: `;
label += ` peak=${formatBytes(this.peakAllocatedMemory)}`;
label += ` time=[${this.start}, ${this.end}] ms`;
return label;
}
finalizeSample(sample) {
const time = sample.time;
if (this.start == null) {
this.start = time;
this.end = time;
} else {
this.end = Math.max(this.end, time);
}
const allocated = sample.allocated;
if (allocated > this.peakAllocatedMemory) {
this.peakUsageTime = time;
this.peakAllocatedMemory = allocated;
}
const sample_zones = sample.zones;
if (sample_zones !== undefined) {
sample.zones.forEach((zone_sample, zone_name) => {
let zone_stats = this.zones.get(zone_name);
if (zone_stats === undefined) {
zone_stats = {max_allocated: 0, max_used: 0};
this.zones.set(zone_name, zone_stats);
}
zone_stats.max_allocated =
Math.max(zone_stats.max_allocated, zone_sample.allocated);
zone_stats.max_used = Math.max(zone_stats.max_used, zone_sample.used);
});
}
}
sortZoneNamesByPeakMemory() {
let entries = [...this.zones.keys()];
entries.sort((a, b) =>
this.zones.get(a).max_allocated - this.zones.get(b).max_allocated
);
this.sorted_zone_names = entries;
let max = 0;
for (let [key, value] of entries) {
this.zonePeakMemory[key] = value;
max = Math.max(max, value);
}
this.singleZonePeakMemory = max;
}
getInstanceTypePeakMemory(type) {
if (!(type in this.zonePeakMemory)) return 0;
return this.zonePeakMemory[type];
}
}
<!-- Copyright 2020 the V8 project authors. All rights reserved.
Use of this source code is governed by a BSD-style license that can be
found in the LICENSE file. -->
<style>
#fileReader {
width: 100%;
height: 100px;
line-height: 100px;
text-align: center;
border: solid 1px #000000;
border-radius: 5px;
cursor: pointer;
transition: all 0.5s ease-in-out;
}
#fileReader.done {
height: 20px;
line-height: 20px;
}
#fileReader:hover {
background-color: #e0edfe ;
}
.loading #fileReader {
cursor: wait;
}
#fileReader > input {
display: none;
}
#loader {
display: none;
}
.loading #loader {
display: block;
position: fixed;
top: 0px;
left: 0px;
width: 100%;
height: 100%;
background-color: rgba(255, 255, 255, 0.5);
}
#spinner {
position: absolute;
width: 100px;
height: 100px;
top: 40%;
left: 50%;
margin-left: -50px;
border: 30px solid #000;
border-top: 30px solid #36E;
border-radius: 50%;
animation: spin 1s ease-in-out infinite;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
</style>
<section id="fileReaderSection">
<div id="fileReader" tabindex=1 >
<span id="label">
Drag and drop a trace file into this area, or click to choose from disk.
</span>
<input id="file" type="file" name="file" />
</div>
<div id="loader">
<div id="spinner"></div>
</div>
</section>
// Copyright 2020 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
'use strict';
import {Isolate} from './model.js';
defineCustomElement('trace-file-reader', (templateText) =>
class TraceFileReader extends HTMLElement {
constructor() {
super();
const shadowRoot = this.attachShadow({mode: 'open'});
shadowRoot.innerHTML = templateText;
this.addEventListener('click', e => this.handleClick(e));
this.addEventListener('dragover', e => this.handleDragOver(e));
this.addEventListener('drop', e => this.handleChange(e));
this.$('#file').addEventListener('change', e => this.handleChange(e));
this.$('#fileReader').addEventListener('keydown', e => this.handleKeyEvent(e));
}
$(id) {
return this.shadowRoot.querySelector(id);
}
get section() {
return this.$('#fileReaderSection');
}
updateLabel(text) {
this.$('#label').innerText = text;
}
handleKeyEvent(event) {
if (event.key == "Enter") this.handleClick(event);
}
handleClick(event) {
this.$('#file').click();
}
handleChange(event) {
// Used for drop and file change.
event.preventDefault();
var host = event.dataTransfer ? event.dataTransfer : event.target;
this.readFile(host.files[0]);
}
handleDragOver(event) {
event.preventDefault();
}
connectedCallback() {
this.$('#fileReader').focus();
}
readFile(file) {
if (!file) {
this.updateLabel('Failed to load file.');
return;
}
this.$('#fileReader').blur();
this.section.className = 'loading';
const reader = new FileReader();
if (['application/gzip', 'application/x-gzip'].includes(file.type)) {
reader.onload = (e) => {
try {
// Decode data as strings of 64Kb chunks. Bigger chunks may cause
// parsing failures in Oboe.js.
const chunkedInflate = new pako.Inflate(
{to: 'string', chunkSize: 65536}
);
let processingState = undefined;
chunkedInflate.onData = (chunk) => {
if (processingState === undefined) {
processingState = this.startProcessing(file, chunk);
} else {
processingState.processChunk(chunk);
}
};
chunkedInflate.onEnd = () => {
if (processingState !== undefined) {
const result_data = processingState.endProcessing();
this.processLoadedData(file, result_data);
}
};
console.log("======");
const textResult = chunkedInflate.push(e.target.result);
this.section.className = 'success';
this.$('#fileReader').classList.add('done');
} catch (err) {
console.error(err);
this.section.className = 'failure';
}
};
// Delay the loading a bit to allow for CSS animations to happen.
setTimeout(() => reader.readAsArrayBuffer(file), 0);
} else {
reader.onload = (e) => {
try {
// Process the whole file in at once.
const processingState = this.startProcessing(file, e.target.result);
const dataModel = processingState.endProcessing();
this.processLoadedData(file, dataModel);
this.section.className = 'success';
this.$('#fileReader').classList.add('done');
} catch (err) {
console.error(err);
this.section.className = 'failure';
}
};
// Delay the loading a bit to allow for CSS animations to happen.
setTimeout(() => reader.readAsText(file), 0);
}
}
processLoadedData(file, dataModel) {
console.log("Trace file parsed successfully.");
this.extendAndSanitizeModel(dataModel);
this.updateLabel('Finished loading \'' + file.name + '\'.');
this.dispatchEvent(new CustomEvent(
'change', {bubbles: true, composed: true, detail: dataModel}));
}
createOrUpdateEntryIfNeeded(data, entry) {
console.assert(entry.isolate, 'entry should have an isolate');
if (!(entry.isolate in data)) {
data[entry.isolate] = new Isolate(entry.isolate);
}
}
extendAndSanitizeModel(data) {
const checkNonNegativeProperty = (obj, property) => {
console.assert(obj[property] >= 0, 'negative property', obj, property);
};
Object.values(data).forEach(isolate => isolate.finalize());
}
processOneZoneStatsEntry(data, entry_stats) {
this.createOrUpdateEntryIfNeeded(data, entry_stats);
const isolate_data = data[entry_stats.isolate];
let zones = undefined;
const entry_zones = entry_stats.zones;
if (entry_zones !== undefined) {
zones = new Map();
entry_zones.forEach(zone => {
// There might be multiple occurrences of the same zone in the set,
// combine numbers in this case.
const existing_zone_stats = zones.get(zone.name);
if (existing_zone_stats !== undefined) {
existing_zone_stats.allocated += zone.allocated;
existing_zone_stats.used += zone.used;
} else {
zones.set(zone.name, {allocated: zone.allocated, used: zone.used});
}
});
}
const time = entry_stats.time;
const sample = {
time: time,
allocated: entry_stats.allocated,
used: entry_stats.used,
zones: zones
};
isolate_data.samples.set(time, sample);
}
startProcessing(file, chunk) {
const isV8TraceFile = chunk.includes('v8-zone-trace');
const processingState =
isV8TraceFile ? this.startProcessingAsV8TraceFile(file)
: this.startProcessingAsChromeTraceFile(file);
processingState.processChunk(chunk);
return processingState;
}
startProcessingAsChromeTraceFile(file) {
console.log(`Processing log as chrome trace file.`);
const data = Object.create(null); // Final data container.
const parseOneZoneEvent = (actual_data) => {
if ('stats' in actual_data) {
try {
const entry_stats = JSON.parse(actual_data.stats);
this.processOneZoneStatsEntry(data, entry_stats);
} catch (e) {
console.error('Unable to parse data set entry', e);
}
}
};
const zone_events_filter = (event) => {
if (event.name == 'V8.Zone_Stats') {
parseOneZoneEvent(event.args);
}
return oboe.drop;
};
const oboe_stream = oboe();
// Trace files support two formats.
oboe_stream
// 1) {traceEvents: [ data ]}
.node('traceEvents.*', zone_events_filter)
// 2) [ data ]
.node('!.*', zone_events_filter)
.fail((errorReport) => {
throw new Error("Trace data parse failed: " + errorReport.thrown);
});
let failed = false;
const processingState = {
file: file,
processChunk(chunk) {
if (failed) return false;
try {
oboe_stream.emit('data', chunk);
return true;
} catch (e) {
console.error('Unable to parse chrome trace file.', e);
failed = true;
return false;
}
},
endProcessing() {
if (failed) return null;
oboe_stream.emit('end');
return data;
},
};
return processingState;
}
startProcessingAsV8TraceFile(file) {
console.log('Processing log as V8 trace file.');
const data = Object.create(null); // Final data container.
const processOneLine = (line) => {
try {
// Strip away a potentially present adb logcat prefix.
line = line.replace(/^I\/v8\s*\(\d+\):\s+/g, '');
const entry = JSON.parse(line);
if (entry === null || entry.type === undefined) return;
if ((entry.type === 'v8-zone-trace') && ('stats' in entry)) {
const entry_stats = entry.stats;
this.processOneZoneStatsEntry(data, entry_stats);
} else {
console.log('Unknown entry type: ' + entry.type);
}
} catch (e) {
console.log('Unable to parse line: \'' + line + '\' (' + e + ')');
}
};
let prev_chunk_leftover = "";
const processingState = {
file: file,
processChunk(chunk) {
const contents = chunk.split('\n');
const last_line = contents.pop();
const linesCount = contents.length;
if (linesCount == 0) {
// There was only one line in the chunk, it may still be unfinished.
prev_chunk_leftover += last_line;
} else {
contents[0] = prev_chunk_leftover + contents[0];
prev_chunk_leftover = last_line;
for (let line of contents) {
processOneLine(line);
}
}
return true;
},
endProcessing() {
if (prev_chunk_leftover.length > 0) {
processOneLine(prev_chunk_leftover);
prev_chunk_leftover = "";
}
return data;
},
};
return processingState;
}
});
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment