diff options
author | Allan Sandfeld Jensen <allan.jensen@qt.io> | 2020-10-12 14:27:29 +0200 |
---|---|---|
committer | Allan Sandfeld Jensen <allan.jensen@qt.io> | 2020-10-13 09:35:20 +0000 |
commit | c30a6232df03e1efbd9f3b226777b07e087a1122 (patch) | |
tree | e992f45784689f373bcc38d1b79a239ebe17ee23 /chromium/v8/tools/zone-stats | |
parent | 7b5b123ac58f58ffde0f4f6e488bcd09aa4decd3 (diff) | |
download | qtwebengine-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.js | 129 | ||||
-rw-r--r-- | chromium/v8/tools/zone-stats/details-selection-template.html | 146 | ||||
-rw-r--r-- | chromium/v8/tools/zone-stats/details-selection.js | 365 | ||||
-rw-r--r-- | chromium/v8/tools/zone-stats/global-timeline-template.html | 16 | ||||
-rw-r--r-- | chromium/v8/tools/zone-stats/global-timeline.js | 323 | ||||
-rw-r--r-- | chromium/v8/tools/zone-stats/helper.js | 30 | ||||
-rw-r--r-- | chromium/v8/tools/zone-stats/index.html | 93 | ||||
-rw-r--r-- | chromium/v8/tools/zone-stats/model.js | 92 | ||||
-rw-r--r-- | chromium/v8/tools/zone-stats/trace-file-reader-template.html | 81 | ||||
-rw-r--r-- | chromium/v8/tools/zone-stats/trace-file-reader.js | 294 |
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; + } +}); |