[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/+/2226855 Reviewed-by: Camillo Bruni <cbruni@chromium.org> Commit-Queue: Igor Sheludko <ishell@chromium.org> Cr-Commit-Position: refs/heads/master@{#68133}
This commit is contained in:
parent
70eb08982c
commit
262a1078d5
129
tools/zone-stats/categories.js
Normal file
129
tools/zone-stats/categories.js
Normal file
@ -0,0 +1,129 @@
|
||||
// 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;
|
||||
}
|
146
tools/zone-stats/details-selection-template.html
Normal file
146
tools/zone-stats/details-selection-template.html
Normal file
@ -0,0 +1,146 @@
|
||||
<!-- 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>
|
365
tools/zone-stats/details-selection.js
Normal file
365
tools/zone-stats/details-selection.js
Normal file
@ -0,0 +1,365 @@
|
||||
// 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 {CATEGORIES, CATEGORY_NAMES, categoryByZoneName} from './categories.js';
|
||||
|
||||
export const VIEW_TOTALS = 'by-totals';
|
||||
export const VIEW_BY_ZONE_NAME = 'by-zone-name';
|
||||
export const VIEW_BY_ZONE_CATEGORY = 'by-zone-category';
|
||||
|
||||
export const KIND_ALLOCATED_MEMORY = 'kind-detailed-allocated';
|
||||
export const KIND_USED_MEMORY = 'kind-detailed-used';
|
||||
|
||||
defineCustomElement('details-selection', (templateText) =>
|
||||
class DetailsSelection extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
const shadowRoot = this.attachShadow({mode: 'open'});
|
||||
shadowRoot.innerHTML = templateText;
|
||||
this.isolateSelect.addEventListener(
|
||||
'change', e => this.handleIsolateChange(e));
|
||||
this.dataViewSelect.addEventListener(
|
||||
'change', e => this.notifySelectionChanged(e));
|
||||
this.dataKindSelect.addEventListener(
|
||||
'change', e => this.notifySelectionChanged(e));
|
||||
this.showTotalsSelect.addEventListener(
|
||||
'change', e => this.notifySelectionChanged(e));
|
||||
this.memoryUsageSampleSelect.addEventListener(
|
||||
'change', e => this.notifySelectionChanged(e));
|
||||
this.timeStartSelect.addEventListener(
|
||||
'change', e => this.notifySelectionChanged(e));
|
||||
this.timeEndSelect.addEventListener(
|
||||
'change', e => this.notifySelectionChanged(e));
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
for (let category of CATEGORIES.keys()) {
|
||||
this.$('#categories').appendChild(this.buildCategory(category));
|
||||
}
|
||||
}
|
||||
|
||||
set data(value) {
|
||||
this._data = value;
|
||||
this.dataChanged();
|
||||
}
|
||||
|
||||
get data() {
|
||||
return this._data;
|
||||
}
|
||||
|
||||
get selectedIsolate() {
|
||||
return this._data[this.selection.isolate];
|
||||
}
|
||||
|
||||
get selectedData() {
|
||||
console.assert(this.data, 'invalid data');
|
||||
console.assert(this.selection, 'invalid selection');
|
||||
const time = this.selection.time;
|
||||
return this.selectedIsolate.samples.get(time);
|
||||
}
|
||||
|
||||
$(id) {
|
||||
return this.shadowRoot.querySelector(id);
|
||||
}
|
||||
|
||||
querySelectorAll(query) {
|
||||
return this.shadowRoot.querySelectorAll(query);
|
||||
}
|
||||
|
||||
get dataViewSelect() {
|
||||
return this.$('#data-view-select');
|
||||
}
|
||||
|
||||
get dataKindSelect() {
|
||||
return this.$('#data-kind-select');
|
||||
}
|
||||
|
||||
get isolateSelect() {
|
||||
return this.$('#isolate-select');
|
||||
}
|
||||
|
||||
get memoryUsageSampleSelect() {
|
||||
return this.$('#memory-usage-sample-select');
|
||||
}
|
||||
|
||||
get showTotalsSelect() {
|
||||
return this.$('#show-totals-select');
|
||||
}
|
||||
|
||||
get timeStartSelect() {
|
||||
return this.$('#time-start-select');
|
||||
}
|
||||
|
||||
get timeEndSelect() {
|
||||
return this.$('#time-end-select');
|
||||
}
|
||||
|
||||
buildCategory(name) {
|
||||
const div = document.createElement('div');
|
||||
div.id = name;
|
||||
div.classList.add('box');
|
||||
const ul = document.createElement('ul');
|
||||
div.appendChild(ul);
|
||||
const name_li = document.createElement('li');
|
||||
ul.appendChild(name_li);
|
||||
name_li.innerHTML = CATEGORY_NAMES.get(name);
|
||||
const percent_li = document.createElement('li');
|
||||
ul.appendChild(percent_li);
|
||||
percent_li.innerHTML = '0%';
|
||||
percent_li.id = name + 'PercentContent';
|
||||
const all_li = document.createElement('li');
|
||||
ul.appendChild(all_li);
|
||||
const all_button = document.createElement('button');
|
||||
all_li.appendChild(all_button);
|
||||
all_button.innerHTML = 'All';
|
||||
all_button.addEventListener('click', e => this.selectCategory(name));
|
||||
const none_li = document.createElement('li');
|
||||
ul.appendChild(none_li);
|
||||
const none_button = document.createElement('button');
|
||||
none_li.appendChild(none_button);
|
||||
none_button.innerHTML = 'None';
|
||||
none_button.addEventListener('click', e => this.unselectCategory(name));
|
||||
const innerDiv = document.createElement('div');
|
||||
div.appendChild(innerDiv);
|
||||
innerDiv.id = name + 'Content';
|
||||
const percentDiv = document.createElement('div');
|
||||
div.appendChild(percentDiv);
|
||||
percentDiv.className = 'percentBackground';
|
||||
percentDiv.id = name + 'PercentBackground';
|
||||
return div;
|
||||
}
|
||||
|
||||
dataChanged() {
|
||||
this.selection = {categories: {}, zones: new Map()};
|
||||
this.resetUI(true);
|
||||
this.populateIsolateSelect();
|
||||
this.handleIsolateChange();
|
||||
this.$('#dataSelectionSection').style.display = 'block';
|
||||
}
|
||||
|
||||
populateIsolateSelect() {
|
||||
let isolates = Object.entries(this.data);
|
||||
// Sort by peak heap memory consumption.
|
||||
isolates.sort((a, b) => b[1].peakAllocatedMemory - a[1].peakAllocatedMemory);
|
||||
this.populateSelect(
|
||||
'#isolate-select', isolates, (key, isolate) => isolate.getLabel());
|
||||
}
|
||||
|
||||
resetUI(resetIsolateSelect) {
|
||||
if (resetIsolateSelect) removeAllChildren(this.isolateSelect);
|
||||
|
||||
removeAllChildren(this.dataViewSelect);
|
||||
removeAllChildren(this.dataKindSelect);
|
||||
removeAllChildren(this.memoryUsageSampleSelect);
|
||||
this.clearCategories();
|
||||
}
|
||||
|
||||
handleIsolateChange(e) {
|
||||
this.selection.isolate = this.isolateSelect.value;
|
||||
if (this.selection.isolate.length === 0) {
|
||||
this.selection.isolate = null;
|
||||
return;
|
||||
}
|
||||
this.resetUI(false);
|
||||
this.populateSelect(
|
||||
'#data-view-select', [
|
||||
[VIEW_TOTALS, 'Total memory usage'],
|
||||
[VIEW_BY_ZONE_NAME, 'Selected zones types'],
|
||||
[VIEW_BY_ZONE_CATEGORY, 'Selected zone categories'],
|
||||
],
|
||||
(key, label) => label, VIEW_TOTALS);
|
||||
this.populateSelect(
|
||||
'#data-kind-select', [
|
||||
[KIND_ALLOCATED_MEMORY, 'Allocated memory per zone'],
|
||||
[KIND_USED_MEMORY, 'Used memory per zone'],
|
||||
],
|
||||
(key, label) => label, KIND_ALLOCATED_MEMORY);
|
||||
|
||||
this.populateSelect(
|
||||
'#memory-usage-sample-select',
|
||||
[...this.selectedIsolate.samples.entries()].filter(([time, sample]) => {
|
||||
// Remove samples that does not have detailed per-zone data.
|
||||
return sample.zones !== undefined;
|
||||
}),
|
||||
(time, sample, index) => {
|
||||
return ((index + ': ').padStart(6, '\u00A0') +
|
||||
formatSeconds(time).padStart(8, '\u00A0') + ' ' +
|
||||
formatBytes(sample.allocated).padStart(12, '\u00A0'));
|
||||
},
|
||||
this.selectedIsolate.peakUsageTime);
|
||||
|
||||
this.timeStartSelect.value = this.selectedIsolate.start;
|
||||
this.timeEndSelect.value = this.selectedIsolate.end;
|
||||
|
||||
this.populateCategories();
|
||||
this.notifySelectionChanged();
|
||||
}
|
||||
|
||||
notifySelectionChanged(e) {
|
||||
if (!this.selection.isolate) return;
|
||||
|
||||
this.selection.data_view = this.dataViewSelect.value;
|
||||
this.selection.data_kind = this.dataKindSelect.value;
|
||||
this.selection.categories = Object.create(null);
|
||||
this.selection.zones = new Map();
|
||||
this.$('#categories').style.display = 'none';
|
||||
for (let category of CATEGORIES.keys()) {
|
||||
const selected = this.selectedInCategory(category);
|
||||
if (selected.length > 0) this.selection.categories[category] = selected;
|
||||
for (const zone_name of selected) {
|
||||
this.selection.zones.set(zone_name, category);
|
||||
}
|
||||
}
|
||||
this.$('#categories').style.display = 'block';
|
||||
this.selection.category_names = CATEGORY_NAMES;
|
||||
this.selection.show_totals = this.showTotalsSelect.checked;
|
||||
this.selection.time = Number(this.memoryUsageSampleSelect.value);
|
||||
this.selection.timeStart = Number(this.timeStartSelect.value);
|
||||
this.selection.timeEnd = Number(this.timeEndSelect.value);
|
||||
this.updatePercentagesInCategory();
|
||||
this.updatePercentagesInZones();
|
||||
this.dispatchEvent(new CustomEvent(
|
||||
'change', {bubbles: true, composed: true, detail: this.selection}));
|
||||
}
|
||||
|
||||
updatePercentagesInCategory() {
|
||||
const overalls = Object.create(null);
|
||||
let overall = 0;
|
||||
// Reset all categories.
|
||||
this.selection.category_names.forEach((_, category) => {
|
||||
overalls[category] = 0;
|
||||
});
|
||||
// Only update categories that have selections.
|
||||
Object.entries(this.selection.categories).forEach(([category, value]) => {
|
||||
overalls[category] =
|
||||
Object.values(value).reduce(
|
||||
(accu, current) => {
|
||||
const zone_data = this.selectedData.zones.get(current);
|
||||
return zone_data === undefined ? accu
|
||||
: accu + zone_data.allocated;
|
||||
}, 0) /
|
||||
KB;
|
||||
overall += overalls[category];
|
||||
});
|
||||
Object.entries(overalls).forEach(([category, category_overall]) => {
|
||||
let percents = category_overall / overall * 100;
|
||||
this.$(`#${category}PercentContent`).innerHTML =
|
||||
`${percents.toFixed(1)}%`;
|
||||
this.$('#' + category + 'PercentBackground').style.left = percents + '%';
|
||||
});
|
||||
}
|
||||
|
||||
updatePercentagesInZones() {
|
||||
const selected_data = this.selectedData;
|
||||
const zones_data = selected_data.zones;
|
||||
const total_allocated = selected_data.allocated;
|
||||
this.querySelectorAll('.zonesSelectBox input').forEach(checkbox => {
|
||||
const zone_name = checkbox.value;
|
||||
const zone_data = zones_data.get(zone_name);
|
||||
const zone_allocated = zone_data === undefined ? 0 : zone_data.allocated;
|
||||
if (zone_allocated == 0) {
|
||||
checkbox.parentNode.style.display = 'none';
|
||||
} else {
|
||||
const percents = zone_allocated / total_allocated;
|
||||
const percent_div = checkbox.parentNode.querySelector('.percentBackground');
|
||||
percent_div.style.left = (percents * 100) + '%';
|
||||
checkbox.parentNode.style.display = 'block';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
selectedInCategory(category) {
|
||||
let tmp = [];
|
||||
this.querySelectorAll('input[name=' + category + 'Checkbox]:checked')
|
||||
.forEach(checkbox => tmp.push(checkbox.value));
|
||||
return tmp;
|
||||
}
|
||||
|
||||
createOption(value, text) {
|
||||
const option = document.createElement('option');
|
||||
option.value = value;
|
||||
option.text = text;
|
||||
return option;
|
||||
}
|
||||
|
||||
populateSelect(id, iterable, labelFn = null, autoselect = null) {
|
||||
if (labelFn == null) labelFn = e => e;
|
||||
let index = 0;
|
||||
for (let [key, value] of iterable) {
|
||||
index++;
|
||||
const label = labelFn(key, value, index);
|
||||
const option = this.createOption(key, label);
|
||||
if (autoselect === key) {
|
||||
option.selected = 'selected';
|
||||
}
|
||||
this.$(id).appendChild(option);
|
||||
}
|
||||
}
|
||||
|
||||
clearCategories() {
|
||||
for (const category of CATEGORIES.keys()) {
|
||||
let f = this.$('#' + category + 'Content');
|
||||
while (f.firstChild) {
|
||||
f.removeChild(f.firstChild);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
populateCategories() {
|
||||
this.clearCategories();
|
||||
const categories = Object.create(null);
|
||||
for (let cat of CATEGORIES.keys()) {
|
||||
categories[cat] = [];
|
||||
}
|
||||
|
||||
for (const [zone_name, zone_stats] of this.selectedIsolate.zones) {
|
||||
const category = categoryByZoneName(zone_name);
|
||||
categories[category].push(zone_name);
|
||||
}
|
||||
for (let category of Object.keys(categories)) {
|
||||
categories[category].sort();
|
||||
for (let zone_name of categories[category]) {
|
||||
this.$('#' + category + 'Content')
|
||||
.appendChild(this.createCheckBox(zone_name, category));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unselectCategory(category) {
|
||||
this.querySelectorAll('input[name=' + category + 'Checkbox]')
|
||||
.forEach(checkbox => checkbox.checked = false);
|
||||
this.notifySelectionChanged();
|
||||
}
|
||||
|
||||
selectCategory(category) {
|
||||
this.querySelectorAll('input[name=' + category + 'Checkbox]')
|
||||
.forEach(checkbox => checkbox.checked = true);
|
||||
this.notifySelectionChanged();
|
||||
}
|
||||
|
||||
createCheckBox(instance_type, category) {
|
||||
const div = document.createElement('div');
|
||||
div.classList.add('zonesSelectBox');
|
||||
div.style.width = "200px";
|
||||
const input = document.createElement('input');
|
||||
div.appendChild(input);
|
||||
input.type = 'checkbox';
|
||||
input.name = category + 'Checkbox';
|
||||
input.checked = 'checked';
|
||||
input.id = instance_type + 'Checkbox';
|
||||
input.instance_type = instance_type;
|
||||
input.value = instance_type;
|
||||
input.addEventListener('change', e => this.notifySelectionChanged(e));
|
||||
const label = document.createElement('label');
|
||||
div.appendChild(label);
|
||||
label.innerText = instance_type;
|
||||
label.htmlFor = instance_type + 'Checkbox';
|
||||
const percentDiv = document.createElement('div');
|
||||
percentDiv.className = 'percentBackground';
|
||||
div.appendChild(percentDiv);
|
||||
return div;
|
||||
}
|
||||
});
|
16
tools/zone-stats/global-timeline-template.html
Normal file
16
tools/zone-stats/global-timeline-template.html
Normal file
@ -0,0 +1,16 @@
|
||||
<!-- 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>
|
323
tools/zone-stats/global-timeline.js
Normal file
323
tools/zone-stats/global-timeline.js
Normal file
@ -0,0 +1,323 @@
|
||||
// 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));
|
||||
}
|
||||
});
|
30
tools/zone-stats/helper.js
Normal file
30
tools/zone-stats/helper.js
Normal file
@ -0,0 +1,30 @@
|
||||
// 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)));
|
||||
}
|
93
tools/zone-stats/index.html
Normal file
93
tools/zone-stats/index.html
Normal file
@ -0,0 +1,93 @@
|
||||
<!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>
|
92
tools/zone-stats/model.js
Normal file
92
tools/zone-stats/model.js
Normal file
@ -0,0 +1,92 @@
|
||||
// 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];
|
||||
}
|
||||
}
|
81
tools/zone-stats/trace-file-reader-template.html
Normal file
81
tools/zone-stats/trace-file-reader-template.html
Normal file
@ -0,0 +1,81 @@
|
||||
<!-- 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>
|
294
tools/zone-stats/trace-file-reader.js
Normal file
294
tools/zone-stats/trace-file-reader.js
Normal file
@ -0,0 +1,294 @@
|
||||
// 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;
|
||||
}
|
||||
});
|
Loading…
Reference in New Issue
Block a user