summaryrefslogtreecommitdiff
path: root/chromium/v8/tools/zone-stats
diff options
context:
space:
mode:
authorAllan Sandfeld Jensen <allan.jensen@qt.io>2020-10-12 14:27:29 +0200
committerAllan Sandfeld Jensen <allan.jensen@qt.io>2020-10-13 09:35:20 +0000
commitc30a6232df03e1efbd9f3b226777b07e087a1122 (patch)
treee992f45784689f373bcc38d1b79a239ebe17ee23 /chromium/v8/tools/zone-stats
parent7b5b123ac58f58ffde0f4f6e488bcd09aa4decd3 (diff)
downloadqtwebengine-chromium-85-based.tar.gz
BASELINE: Update Chromium to 85.0.4183.14085-based
Change-Id: Iaa42f4680837c57725b1344f108c0196741f6057 Reviewed-by: Allan Sandfeld Jensen <allan.jensen@qt.io>
Diffstat (limited to 'chromium/v8/tools/zone-stats')
-rw-r--r--chromium/v8/tools/zone-stats/categories.js129
-rw-r--r--chromium/v8/tools/zone-stats/details-selection-template.html146
-rw-r--r--chromium/v8/tools/zone-stats/details-selection.js365
-rw-r--r--chromium/v8/tools/zone-stats/global-timeline-template.html16
-rw-r--r--chromium/v8/tools/zone-stats/global-timeline.js323
-rw-r--r--chromium/v8/tools/zone-stats/helper.js30
-rw-r--r--chromium/v8/tools/zone-stats/index.html93
-rw-r--r--chromium/v8/tools/zone-stats/model.js92
-rw-r--r--chromium/v8/tools/zone-stats/trace-file-reader-template.html81
-rw-r--r--chromium/v8/tools/zone-stats/trace-file-reader.js294
10 files changed, 1569 insertions, 0 deletions
diff --git a/chromium/v8/tools/zone-stats/categories.js b/chromium/v8/tools/zone-stats/categories.js
new file mode 100644
index 00000000000..96c4e32eaf7
--- /dev/null
+++ b/chromium/v8/tools/zone-stats/categories.js
@@ -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;
+}
diff --git a/chromium/v8/tools/zone-stats/details-selection-template.html b/chromium/v8/tools/zone-stats/details-selection-template.html
new file mode 100644
index 00000000000..ef1e2f68b9b
--- /dev/null
+++ b/chromium/v8/tools/zone-stats/details-selection-template.html
@@ -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>
diff --git a/chromium/v8/tools/zone-stats/details-selection.js b/chromium/v8/tools/zone-stats/details-selection.js
new file mode 100644
index 00000000000..b25a11337a3
--- /dev/null
+++ b/chromium/v8/tools/zone-stats/details-selection.js
@@ -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;
+ }
+});
diff --git a/chromium/v8/tools/zone-stats/global-timeline-template.html b/chromium/v8/tools/zone-stats/global-timeline-template.html
new file mode 100644
index 00000000000..49e75646f16
--- /dev/null
+++ b/chromium/v8/tools/zone-stats/global-timeline-template.html
@@ -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>
diff --git a/chromium/v8/tools/zone-stats/global-timeline.js b/chromium/v8/tools/zone-stats/global-timeline.js
new file mode 100644
index 00000000000..ea1793101b4
--- /dev/null
+++ b/chromium/v8/tools/zone-stats/global-timeline.js
@@ -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));
+ }
+});
diff --git a/chromium/v8/tools/zone-stats/helper.js b/chromium/v8/tools/zone-stats/helper.js
new file mode 100644
index 00000000000..a0d04859d1c
--- /dev/null
+++ b/chromium/v8/tools/zone-stats/helper.js
@@ -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)));
+}
diff --git a/chromium/v8/tools/zone-stats/index.html b/chromium/v8/tools/zone-stats/index.html
new file mode 100644
index 00000000000..5997aab8831
--- /dev/null
+++ b/chromium/v8/tools/zone-stats/index.html
@@ -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>
diff --git a/chromium/v8/tools/zone-stats/model.js b/chromium/v8/tools/zone-stats/model.js
new file mode 100644
index 00000000000..80f45237631
--- /dev/null
+++ b/chromium/v8/tools/zone-stats/model.js
@@ -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];
+ }
+}
diff --git a/chromium/v8/tools/zone-stats/trace-file-reader-template.html b/chromium/v8/tools/zone-stats/trace-file-reader-template.html
new file mode 100644
index 00000000000..ede7ee9a75b
--- /dev/null
+++ b/chromium/v8/tools/zone-stats/trace-file-reader-template.html
@@ -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>
diff --git a/chromium/v8/tools/zone-stats/trace-file-reader.js b/chromium/v8/tools/zone-stats/trace-file-reader.js
new file mode 100644
index 00000000000..6decfa8f273
--- /dev/null
+++ b/chromium/v8/tools/zone-stats/trace-file-reader.js
@@ -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;
+ }
+});