diff options
Diffstat (limited to 'chromium/v8/tools/heap-stats')
-rw-r--r-- | chromium/v8/tools/heap-stats/README.md | 15 | ||||
-rw-r--r-- | chromium/v8/tools/heap-stats/categories.js | 167 | ||||
-rw-r--r-- | chromium/v8/tools/heap-stats/details-selection.html | 72 | ||||
-rw-r--r-- | chromium/v8/tools/heap-stats/details-selection.js | 211 | ||||
-rw-r--r-- | chromium/v8/tools/heap-stats/global-timeline.html | 16 | ||||
-rw-r--r-- | chromium/v8/tools/heap-stats/global-timeline.js | 135 | ||||
-rw-r--r-- | chromium/v8/tools/heap-stats/index.html | 88 | ||||
-rw-r--r-- | chromium/v8/tools/heap-stats/trace-file-reader.html | 26 | ||||
-rw-r--r-- | chromium/v8/tools/heap-stats/trace-file-reader.js | 300 |
9 files changed, 1030 insertions, 0 deletions
diff --git a/chromium/v8/tools/heap-stats/README.md b/chromium/v8/tools/heap-stats/README.md new file mode 100644 index 00000000000..70083fe2574 --- /dev/null +++ b/chromium/v8/tools/heap-stats/README.md @@ -0,0 +1,15 @@ +# Heap Stats + +Heap stats is a HTML-based tool for visualizing V8-internal object statistics. +For example, the tool can be used to visualize how much heap memory is used for +maintaining internal state versus actually allocated by the user. + +The tool consumes log files produced by d8 (or Chromium) by passing +`--trace-gc-object-stats` or a trace captured using Chrome's tracing +infrastructure. Chrome trace files need to be unpacked before they can +be used though. + +Hosting requires a web server, e.g.: + + cd tools/heap-stats + python -m SimpleHTTPServer 8000 diff --git a/chromium/v8/tools/heap-stats/categories.js b/chromium/v8/tools/heap-stats/categories.js new file mode 100644 index 00000000000..0a836d5f6c0 --- /dev/null +++ b/chromium/v8/tools/heap-stats/categories.js @@ -0,0 +1,167 @@ +// Copyright 2018 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. + +// Categories for instance types. +const CATEGORIES = new Map([ + [ + 'user', new Set([ + '*FIXED_ARRAY_CONTEXT_SUB_TYPE', + '*FIXED_ARRAY_COPY_ON_WRITE_SUB_TYPE', + '*FIXED_ARRAY_DICTIONARY_PROPERTIES_SUB_TYPE', + '*FIXED_ARRAY_JS_COLLECTION_SUB_TYPE', + '*FIXED_ARRAY_JS_WEAK_COLLECTION_SUB_TYPE', + '*FIXED_ARRAY_PACKED_ELEMENTS_SUB_TYPE', + 'CONS_ONE_BYTE_STRING_TYPE', + 'CONS_STRING_TYPE', + 'DESCRIPTOR_ARRAY_TYPE', + 'EXTERNAL_INTERNALIZED_STRING_TYPE', + 'EXTERNAL_ONE_BYTE_INTERNALIZED_STRING_TYPE', + 'EXTERNAL_ONE_BYTE_STRING_TYPE', + 'EXTERNAL_STRING_TYPE', + 'EXTERNAL_STRING_WITH_ONE_BYTE_DATA_TYPE', + 'FIXED_DOUBLE_ARRAY_TYPE', + 'FIXED_FLOAT32_ARRAY_TYPE', + 'FIXED_FLOAT64_ARRAY_TYPE', + 'FIXED_INT16_ARRAY_TYPE', + 'FIXED_INT32_ARRAY_TYPE', + 'FIXED_INT8_ARRAY_TYPE', + 'FIXED_UINT16_ARRAY_TYPE', + 'FIXED_UINT32_ARRAY_TYPE', + 'FIXED_UINT8_ARRAY_TYPE', + 'FIXED_UINT8_CLAMPED_ARRAY_TYPE', + 'HEAP_NUMBER_TYPE', + 'INTERNALIZED_STRING_TYPE', + 'JS_ARGUMENTS_TYPE', + 'JS_ARRAY_BUFFER_TYPE', + 'JS_ARRAY_TYPE', + 'JS_BOUND_FUNCTION_TYPE', + 'JS_DATE_TYPE', + 'JS_ERROR_TYPE', + 'JS_FAST_ARRAY_KEY_ITERATOR_TYPE', + 'JS_FAST_ARRAY_VALUE_ITERATOR_TYPE', + 'JS_FAST_HOLEY_ARRAY_VALUE_ITERATOR_TYPE', + 'JS_FAST_HOLEY_SMI_ARRAY_VALUE_ITERATOR_TYPE', + 'JS_FAST_SMI_ARRAY_KEY_VALUE_ITERATOR_TYPE', + 'JS_FAST_SMI_ARRAY_VALUE_ITERATOR_TYPE', + 'JS_FUNCTION_TYPE', + 'JS_GENERATOR_OBJECT_TYPE', + 'JS_GENERIC_ARRAY_VALUE_ITERATOR_TYPE', + 'JS_GLOBAL_OBJECT_TYPE', + 'JS_GLOBAL_PROXY_TYPE', + 'JS_MAP_KEY_VALUE_ITERATOR_TYPE', + 'JS_MAP_TYPE', + 'JS_MESSAGE_OBJECT_TYPE', + 'JS_OBJECT_TYPE', + 'JS_PROMISE_TYPE', + 'JS_REGEXP_TYPE', + 'JS_SET_TYPE', + 'JS_STRING_ITERATOR_TYPE', + 'JS_TYPED_ARRAY_TYPE', + 'JS_VALUE_TYPE', + 'JS_WEAK_MAP_TYPE', + 'MUTABLE_HEAP_NUMBER_TYPE', + 'ONE_BYTE_INTERNALIZED_STRING_TYPE', + 'ONE_BYTE_STRING_TYPE', + 'PROPERTY_ARRAY_TYPE', + 'SHORT_EXTERNAL_INTERNALIZED_STRING_TYPE', + 'SHORT_EXTERNAL_ONE_BYTE_INTERNALIZED_STRING_TYPE', + 'SHORT_EXTERNAL_ONE_BYTE_STRING_TYPE', + 'SHORT_EXTERNAL_STRING_TYPE', + 'SLICED_ONE_BYTE_STRING_TYPE', + 'SLICED_STRING_TYPE', + 'STRING_TYPE', + 'SYMBOL_TYPE', + 'THIN_ONE_BYTE_STRING_TYPE', + 'THIN_STRING_TYPE', + ]) + ], + [ + 'system', new Set([ + 'ACCESS_CHECK_INFO_TYPE', + 'ACCESSOR_INFO_TYPE', + 'ACCESSOR_PAIR_TYPE', + 'ALLOCATION_MEMENTO_TYPE', + 'ALLOCATION_SITE_TYPE', + 'BOILERPLATE_ELEMENTS_TYPE', + 'BOILERPLATE_NAME_DICTIONARY_TYPE', + 'BOILERPLATE_PROPERTY_ARRAY_TYPE', + 'BYTE_ARRAY_TYPE', + 'CELL_TYPE', + 'CONTEXT_EXTENSION_TYPE', + '*FIXED_ARRAY_DEPENDENT_CODE_SUB_TYPE', + '*FIXED_ARRAY_ENUM_CACHE_SUB_TYPE', + '*FIXED_ARRAY_ENUM_INDICES_CACHE_SUB_TYPE', + '*FIXED_ARRAY_FAST_TEMPLATE_INSTANTIATIONS_CACHE_SUB_TYPE', + '*FIXED_ARRAY_NUMBER_STRING_CACHE_SUB_TYPE', + '*FIXED_ARRAY_PROTOTYPE_USERS_SUB_TYPE', + '*FIXED_ARRAY_REGEXP_MULTIPLE_CACHE_SUB_TYPE', + '*FIXED_ARRAY_RETAINED_MAPS_SUB_TYPE', + '*FIXED_ARRAY_SCOPE_INFO_SUB_TYPE', + '*FIXED_ARRAY_SCRIPT_LIST_SUB_TYPE', + '*FIXED_ARRAY_SINGLE_CHARACTER_STRING_CACHE_SUB_TYPE', + '*FIXED_ARRAY_STRING_SPLIT_CACHE_SUB_TYPE', + '*FIXED_ARRAY_TEMPLATE_INFO_SUB_TYPE', + '*FIXED_ARRAY_WEAK_NEW_SPACE_OBJECT_TO_CODE_SUB_TYPE', + 'FOREIGN_TYPE', + 'FUNCTION_TEMPLATE_INFO_TYPE', + 'INTERCEPTOR_INFO_TYPE', + 'JS_API_OBJECT_TYPE', + 'JS_ARRAY_BOILERPLATE_TYPE', + 'JS_OBJECT_BOILERPLATE_TYPE', + 'JS_SPECIAL_API_OBJECT_TYPE', + 'MAP_TYPE', + 'OBJECT_TEMPLATE_INFO_TYPE', + 'ODDBALL_TYPE', + 'PROMISE_REACTION_JOB_INFO_TYPE', + 'PROMISE_RESOLVE_THENABLE_JOB_INFO_TYPE', + 'PROPERTY_CELL_TYPE', + 'PROTOTYPE_INFO_TYPE', + 'STACK_FRAME_INFO_TYPE', + 'TRANSITION_ARRAY_TYPE', + 'WEAK_CELL_TYPE', + ]) + ], + [ + 'code', new Set([ + '*CODE_BUILTIN', + '*CODE_BYTECODE_HANDLER', + '*CODE_OPTIMIZED_FUNCTION', + '*CODE_REGEXP', + '*CODE_STUB', + '*FIXED_ARRAY_BYTECODE_ARRAY_CONSTANT_POOL_SUB_TYPE', + '*FIXED_ARRAY_BYTECODE_ARRAY_HANDLER_TABLE_SUB_TYPE', + '*FIXED_ARRAY_CODE_STUBS_TABLE_SUB_TYPE', + '*FIXED_ARRAY_COMPILATION_CACHE_TABLE_SUB_TYPE', + '*FIXED_ARRAY_DEOPTIMIZATION_DATA_SUB_TYPE', + '*FIXED_ARRAY_EMBEDDED_OBJECT_SUB_TYPE', + '*FIXED_ARRAY_HANDLER_TABLE_SUB_TYPE', + '*FIXED_ARRAY_NOSCRIPT_SHARED_FUNCTION_INFOS_SUB_TYPE', + '*FIXED_ARRAY_OPTIMIZED_CODE_LITERALS_SUB_TYPE', + '*FIXED_ARRAY_SHARED_FUNCTION_INFOS_SUB_TYPE', + 'BYTECODE_ARRAY_TYPE', + 'CODE_DATA_CONTAINER_TYPE', + 'FEEDBACK_VECTOR_TYPE', + 'LOAD_HANDLER_TYPE', + 'SCRIPT_TYPE', + 'SHARED_FUNCTION_INFO_TYPE', + 'STORE_HANDLER_TYPE', + ]) + ], + ['unclassified', new Set()], +]); + +// Maps category to description text that is shown in html. +const CATEGORY_NAMES = new Map([ + ['user', 'JS'], + ['system', 'Metadata'], + ['code', 'Code'], + ['unclassified', 'Unclassified'], +]); + +// Instance types that are constructed from their sub types and +// should thus be hidden. +const IGNORED_INSTANCE_TYPES = new Set([ + 'FIXED_ARRAY_TYPE', + 'CODE_TYPE', +]); diff --git a/chromium/v8/tools/heap-stats/details-selection.html b/chromium/v8/tools/heap-stats/details-selection.html new file mode 100644 index 00000000000..d60aef96694 --- /dev/null +++ b/chromium/v8/tools/heap-stats/details-selection.html @@ -0,0 +1,72 @@ +<!-- Copyright 2018 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. --> +<template id="details-selection-template"> +<style> +.box { + border-left: dashed 1px #666666; + border-right: dashed 1px #666666; + border-bottom: dashed 1px #666666; + padding: 10px; + overflow: hidden; +} + +.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; +} + +span { + display: block; + padding: 5px; + font-weight: bold; +} + +.boxDiv { + padding: 3px; + float: left; +} + +.boxDiv > label { + font-size: xx-small; +} + +#categories { + margin-top: 10px; +} +</style> +<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="dataset-select"> + Data set + </label> + <select id="dataset-select"> + <option>No data</option> + </select> + </li> + <li> + <input type="checkbox" id="merge-categories" checked=checked /> + <label for="merge-categories"> + Merge categories + </label> + </li> +</ul> + + +<div id="categories"></div> +</template> +<script type="text/javascript" src="categories.js"></script> +<script type="text/javascript" src="details-selection.js"></script>
\ No newline at end of file diff --git a/chromium/v8/tools/heap-stats/details-selection.js b/chromium/v8/tools/heap-stats/details-selection.js new file mode 100644 index 00000000000..43c000d3f45 --- /dev/null +++ b/chromium/v8/tools/heap-stats/details-selection.js @@ -0,0 +1,211 @@ +// Copyright 2018 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'; + +const details_selection_template = + document.currentScript.ownerDocument.querySelector( + '#details-selection-template'); + +class DetailsSelection extends HTMLElement { + constructor() { + super(); + const shadowRoot = this.attachShadow({mode: 'open'}); + shadowRoot.appendChild(details_selection_template.content.cloneNode(true)); + this.isolateSelect.addEventListener( + 'change', e => this.handleIsolateChange(e)); + this.datasetSelect.addEventListener( + 'change', e => this.notifySelectionChanged(e)); + this.$('#merge-categories') + .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; + } + + buildCategory(name) { + const div = document.createElement('div'); + div.id = name; + div.classList.add('box'); + const span = document.createElement('span'); + div.appendChild(span); + span.innerHTML = CATEGORY_NAMES.get(name) + ' '; + const all_button = document.createElement('button'); + span.appendChild(all_button); + all_button.innerHTML = 'All'; + all_button.addEventListener('click', e => this.selectCategory(name)); + const none_button = document.createElement('button'); + span.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'; + return div; + } + + $(id) { + return this.shadowRoot.querySelector(id); + } + + get datasetSelect() { + return this.$('#dataset-select'); + } + + get isolateSelect() { + return this.$('#isolate-select'); + } + + dataChanged() { + this.clearUI(); + this.populateSelect('#isolate-select', Object.keys(this.data)); + this.handleIsolateChange(); + } + + clearUI() { + this.selection = {categories: {}}; + removeAllChildren(this.isolateSelect); + removeAllChildren(this.datasetSelect); + this.clearCategories(); + } + + handleIsolateChange(e) { + this.selection.isolate = this.isolateSelect.value; + if (this.selection.isolate.length === 0) { + this.selection.isolate = null; + return; + } + + this.populateSelect( + '#dataset-select', this.data[this.selection.isolate].data_sets, 'live'); + this.populateCategories(); + this.notifySelectionChanged(); + } + + notifySelectionChanged(e) { + if (!this.selection.isolate) return; + + this.selection.categories = {}; + for (let category of CATEGORIES.keys()) { + const selected = this.selectedInCategory(category); + if (selected.length > 0) this.selection.categories[category] = selected; + } + this.selection.category_names = CATEGORY_NAMES; + this.selection.data_set = this.datasetSelect.value; + this.selection.merge_categories = this.$('#merge-categories').checked; + this.dispatchEvent(new CustomEvent( + 'change', {bubbles: true, composed: true, detail: this.selection})); + } + + selectedInCategory(category) { + const selected = this.shadowRoot.querySelectorAll( + 'input[name=' + category + 'Checkbox]:checked'); + var tmp = []; + for (var val of selected.values()) tmp.push(val.value); + return tmp; + } + + categoryForType(instance_type) { + for (let [key, value] of CATEGORIES.entries()) { + if (value.has(instance_type)) return key; + } + return 'unclassified'; + } + + createOption(text) { + const option = document.createElement('option'); + option.value = text; + option.text = text; + return option; + } + + populateSelect(id, iterable, autoselect = null) { + for (let option_value of iterable) { + const option = this.createOption(option_value); + if (autoselect === option_value) { + 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 = {}; + for (let cat of CATEGORIES.keys()) { + categories[cat] = []; + } + + for (let instance_type of this.data[this.selection.isolate] + .non_empty_instance_types) { + if (IGNORED_INSTANCE_TYPES.has(instance_type)) continue; + const category = this.categoryForType(instance_type); + categories[category].push(instance_type); + } + for (let category of Object.keys(categories)) { + categories[category].sort(); + for (let instance_type of categories[category]) { + this.$('#' + category + 'Content') + .appendChild(this.createCheckBox(instance_type, category)); + } + } + } + + unselectCategory(category) { + for (let checkbox of this.shadowRoot.querySelectorAll( + 'input[name=' + category + 'Checkbox]')) { + checkbox.checked = false; + } + this.notifySelectionChanged(); + } + + selectCategory(category) { + for (let checkbox of this.shadowRoot.querySelectorAll( + 'input[name=' + category + 'Checkbox]')) { + checkbox.checked = true; + } + this.notifySelectionChanged(); + } + + createCheckBox(instance_type, category) { + const div = document.createElement('div'); + div.classList.add('boxDiv'); + const input = document.createElement('input'); + div.appendChild(input); + input.type = 'checkbox'; + input.name = category + 'Checkbox'; + input.checked = 'checked'; + input.id = instance_type + 'Checkbox'; + 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'; + return div; + } +} + +customElements.define('details-selection', DetailsSelection); diff --git a/chromium/v8/tools/heap-stats/global-timeline.html b/chromium/v8/tools/heap-stats/global-timeline.html new file mode 100644 index 00000000000..788f966735f --- /dev/null +++ b/chromium/v8/tools/heap-stats/global-timeline.html @@ -0,0 +1,16 @@ +<!-- Copyright 2018 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. --> +<template id="global-timeline-template"> +<style> +#chart { + width: 100%; + height: 500px; +} +</style> +<div id="container" style="display: none;"> + <h2>Timeline</h2> + <div id="chart"></div> +</div> +</template> +<script type="text/javascript" src="global-timeline.js"></script>
\ No newline at end of file diff --git a/chromium/v8/tools/heap-stats/global-timeline.js b/chromium/v8/tools/heap-stats/global-timeline.js new file mode 100644 index 00000000000..0533f214322 --- /dev/null +++ b/chromium/v8/tools/heap-stats/global-timeline.js @@ -0,0 +1,135 @@ +// Copyright 2018 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'; + +const KB = 1024; +const MB = KB * KB; + +const global_timeline_template = + document.currentScript.ownerDocument.querySelector( + '#global-timeline-template'); + +class GlobalTimeline extends HTMLElement { + constructor() { + super(); + const shadowRoot = this.attachShadow({mode: 'open'}); + shadowRoot.appendChild(global_timeline_template.content.cloneNode(true)); + } + + $(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()) { + this.drawChart(); + } else { + this.hide(); + } + } + + getCategoryData() { + const categories = Object.keys(this.selection.categories) + .map(k => this.selection.category_names.get(k)); + const labels = ['Time', ...categories]; + const chart_data = [labels]; + const isolate_data = this.data[this.selection.isolate]; + Object.keys(isolate_data.gcs).forEach(gc_key => { + const gc_data = isolate_data.gcs[gc_key]; + const data_set = gc_data[this.selection.data_set].instance_type_data; + const data = []; + data.push(gc_data.time); + Object.values(this.selection.categories).forEach(instance_types => { + data.push( + instance_types + .map(instance_type => { + return data_set[instance_type].overall; + }) + .reduce((accu, current) => accu + current, 0) / + KB); + }); + chart_data.push(data); + }); + return chart_data; + } + + getInstanceTypeData() { + const categories = Object.keys(this.selection.categories); + const instance_types = + Object.values(this.selection.categories) + .reduce((accu, current) => accu.concat(current), []); + const labels = ['Time', ...instance_types]; + const chart_data = [labels]; + const isolate_data = this.data[this.selection.isolate]; + Object.keys(isolate_data.gcs).forEach(gc_key => { + const gc_data = isolate_data.gcs[gc_key]; + const data_set = gc_data[this.selection.data_set].instance_type_data; + const data = []; + data.push(gc_data.time); + instance_types.forEach(instance_type => { + data.push(data_set[instance_type].overall / KB); + }); + chart_data.push(data); + }); + return chart_data; + } + + drawChart() { + console.assert(this.data, 'invalid data'); + console.assert(this.selection, 'invalid selection'); + + const chart_data = (this.selection.merge_categories) ? + this.getCategoryData() : + this.getInstanceTypeData(); + const data = google.visualization.arrayToDataTable(chart_data); + const options = { + isStacked: true, + hAxis: { + title: 'Time [ms]', + }, + vAxis: {title: 'Memory consumption [KBytes]'}, + chartArea: {width: '85%', height: '70%'}, + legend: {position: 'top', maxLines: '1'}, + pointsVisible: true, + pointSize: 5, + explorer: {}, + }; + const chart = new google.visualization.AreaChart(this.$('#chart')); + this.show(); + chart.draw(data, google.charts.Line.convertOptions(options)); + } +} + +customElements.define('global-timeline', GlobalTimeline); diff --git a/chromium/v8/tools/heap-stats/index.html b/chromium/v8/tools/heap-stats/index.html new file mode 100644 index 00000000000..3c2e62b6d0b --- /dev/null +++ b/chromium/v8/tools/heap-stats/index.html @@ -0,0 +1,88 @@ +<!DOCTYPE html> +<!-- Copyright 2018 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' type='text/css'> + <script type="text/javascript" src="https://www.gstatic.com/charts/loader.js"></script> + + <link rel="import" href="details-selection.html"> + <link rel="import" href="global-timeline.html"> + <link rel="import" href="trace-file-reader.html"> + + <style type="text/css"> + +body { + font-family: 'Roboto', sans-serif; + margin-left: 5%; + margin-right: 5%; +} + + </style> + <script type="text/javascript"> + +'use strict'; + +google.charts.load('current', {'packages':['line', 'corechart']}); + +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; + $('#type-details').selection = state.selection; + $('#type-details').data = state.data; + $('#details-selection').data = state.data; +} + +function globalSelectionChangedA(e) { + state.selection = e.detail; + $('#global-timeline').selection = state.selection; + $('#type-details').selection = state.selection; +} + + </script> +</head> + +<body> + <trace-file-reader onchange="globalDataChanged(event)"></trace-file-reader> + <h1>V8 Heap Statistics</h1> + <p>Visualize object statistics that have been gathered using</p> + <ul> + <li><code>--trace-gc-object-stats on V8</code></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.gc_stats</code>. The trace file needs to be unpacked (e.g. using + <code>gunzip</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> + <details-selection id="details-selection" onchange="globalSelectionChangedA(event)"></details-selection> + <global-timeline id="global-timeline"></global-timeline> + <type-details id="type-details"></type-details> +</body> + +</html> diff --git a/chromium/v8/tools/heap-stats/trace-file-reader.html b/chromium/v8/tools/heap-stats/trace-file-reader.html new file mode 100644 index 00000000000..98c2ef0c606 --- /dev/null +++ b/chromium/v8/tools/heap-stats/trace-file-reader.html @@ -0,0 +1,26 @@ +<!-- Copyright 2018 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. --> +<template id="trace-file-reader-template"> +<style> +#fileReader { + width: 100%; + height: 100px; + line-height: 100px; + text-align: center; + border: solid 1px #000000; + border-radius: 5px; +} + +#fileReader > input { + display: none; +} +</style> +<div id="fileReader"> + <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> +</template> +<script type="text/javascript" src="trace-file-reader.js"></script> diff --git a/chromium/v8/tools/heap-stats/trace-file-reader.js b/chromium/v8/tools/heap-stats/trace-file-reader.js new file mode 100644 index 00000000000..59825fe514e --- /dev/null +++ b/chromium/v8/tools/heap-stats/trace-file-reader.js @@ -0,0 +1,300 @@ +// Copyright 2018 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'; + +const trace_file_reader_template = + document.currentScript.ownerDocument.querySelector( + '#trace-file-reader-template'); + +class TraceFileReader extends HTMLElement { + constructor() { + super(); + const shadowRoot = this.attachShadow({mode: 'open'}); + shadowRoot.appendChild(trace_file_reader_template.content.cloneNode(true)); + 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)); + } + + $(id) { + return this.shadowRoot.querySelector(id); + } + + updateLabel(text) { + this.$('#label').innerText = text; + } + + 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() {} + + readFile(file) { + if (!file) { + this.updateLabel('Failed to load file.'); + return; + } + + const result = new FileReader(); + result.onload = (e) => { + let contents = e.target.result.split('\n'); + const return_data = (e.target.result.includes('V8.GC_Objects_Stats')) ? + this.createModelFromChromeTraceFile(contents) : + this.createModelFromV8TraceFile(contents); + this.updateLabel('Finished loading \'' + file.name + '\'.'); + this.dispatchEvent(new CustomEvent( + 'change', {bubbles: true, composed: true, detail: return_data})); + }; + result.readAsText(file); + } + + createOrUpdateEntryIfNeeded(data, keys, entry) { + console.assert(entry.isolate, 'entry should have an isolate'); + if (!(entry.isolate in keys)) { + keys[entry.isolate] = new Set(); + } + if (!(entry.isolate in data)) { + data[entry.isolate] = { + non_empty_instance_types: new Set(), + gcs: {}, + zonetags: [], + samples: {zone: {}}, + start: null, + end: null, + data_sets: new Set() + }; + } + const data_object = data[entry.isolate]; + if (('id' in entry) && !(entry.id in data_object.gcs)) { + data_object.gcs[entry.id] = {non_empty_instance_types: new Set()}; + } + if ('time' in entry) { + if (data_object.end === null || data_object.end < entry.time) + data_object.end = entry.time; + if (data_object.start === null || data_object.start > entry.time) + data_object.start = entry.time; + } + } + + createDatasetIfNeeded(data, keys, entry, data_set) { + if (!(data_set in data[entry.isolate].gcs[entry.id])) { + data[entry.isolate].gcs[entry.id][data_set] = { + instance_type_data: {}, + non_empty_instance_types: new Set(), + overall: 0 + }; + data[entry.isolate].data_sets.add(data_set); + } + } + + addInstanceTypeData( + data, keys, isolate, gc_id, data_set, instance_type, entry) { + keys[isolate].add(data_set); + data[isolate].gcs[gc_id][data_set].instance_type_data[instance_type] = { + overall: entry.overall, + count: entry.count, + histogram: entry.histogram, + over_allocated: entry.over_allocated, + over_allocated_histogram: entry.over_allocated_histogram + }; + data[isolate].gcs[gc_id][data_set].overall += entry.overall; + if (entry.overall !== 0) { + data[isolate].gcs[gc_id][data_set].non_empty_instance_types.add( + instance_type); + data[isolate].gcs[gc_id].non_empty_instance_types.add(instance_type); + data[isolate].non_empty_instance_types.add(instance_type); + } + } + + extendAndSanitizeModel(data, keys) { + const checkNonNegativeProperty = (obj, property) => { + console.assert(obj[property] >= 0, 'negative property', obj, property); + }; + + for (const isolate of Object.keys(data)) { + for (const gc of Object.keys(data[isolate].gcs)) { + for (const data_set_key of keys[isolate]) { + const data_set = data[isolate].gcs[gc][data_set_key]; + // 1. Create a ranked instance type array that sorts instance + // types by memory size (overall). + data_set.ranked_instance_types = + [...data_set.non_empty_instance_types].sort(function(a, b) { + if (data_set.instance_type_data[a].overall > + data_set.instance_type_data[b].overall) { + return 1; + } else if ( + data_set.instance_type_data[a].overall < + data_set.instance_type_data[b].overall) { + return -1; + } + return 0; + }); + + let known_count = 0; + let known_overall = 0; + let known_histogram = + Array( + data_set.instance_type_data.FIXED_ARRAY_TYPE.histogram.length) + .fill(0); + for (const instance_type in data_set.instance_type_data) { + if (!instance_type.startsWith('*FIXED_ARRAY')) continue; + const subtype = data_set.instance_type_data[instance_type]; + known_count += subtype.count; + known_overall += subtype.count; + for (let i = 0; i < subtype.histogram.length; i++) { + known_histogram[i] += subtype.histogram[i]; + } + } + + const fixed_array_data = data_set.instance_type_data.FIXED_ARRAY_TYPE; + const unknown_entry = { + count: fixed_array_data.count - known_count, + overall: fixed_array_data.overall - known_overall, + histogram: fixed_array_data.histogram.map( + (value, index) => value - known_histogram[index]) + }; + + // Check for non-negative values. + checkNonNegativeProperty(unknown_entry, 'count'); + checkNonNegativeProperty(unknown_entry, 'overall'); + for (let i = 0; i < unknown_entry.histogram.length; i++) { + checkNonNegativeProperty(unknown_entry.histogram, i); + } + + data_set.instance_type_data['*FIXED_ARRAY_UNKNOWN_SUB_TYPE'] = + unknown_entry; + data_set.non_empty_instance_types.add( + '*FIXED_ARRAY_UNKNOWN_SUB_TYPE'); + } + } + } + } + + createModelFromChromeTraceFile(contents) { + console.log('Processing log as chrome trace file.'); + const data = Object.create(null); // Final data container. + const keys = Object.create(null); // Collecting 'keys' per isolate. + + // Pop last line in log as it might be broken. + contents.pop(); + // Remove trailing comma. + contents[contents.length - 1] = contents[contents.length - 1].slice(0, -1); + // Terminate JSON. + const sanitized_contents = [...contents, ']}'].join(''); + try { + const raw_data = JSON.parse(sanitized_contents); + const objects_stats_data = + raw_data.traceEvents.filter(e => e.name == 'V8.GC_Objects_Stats'); + objects_stats_data.forEach(trace_data => { + const actual_data = trace_data.args; + const data_sets = new Set(Object.keys(actual_data)); + Object.keys(actual_data).forEach(data_set => { + const string_entry = actual_data[data_set]; + try { + const entry = JSON.parse(string_entry); + this.createOrUpdateEntryIfNeeded(data, keys, entry); + this.createDatasetIfNeeded(data, keys, entry, data_set); + const isolate = entry.isolate; + const time = entry.time; + const gc_id = entry.id; + data[isolate].gcs[gc_id].time = time; + data[isolate].gcs[gc_id][data_set].bucket_sizes = + entry.bucket_sizes; + for (let [instance_type, value] of Object.entries( + entry.type_data)) { + // Trace file format uses markers that do not have actual + // properties. + if (!('overall' in value)) continue; + this.addInstanceTypeData( + data, keys, isolate, gc_id, data_set, instance_type, value); + } + } catch (e) { + console.log('Unable to parse data set entry', e); + } + }); + }); + } catch (e) { + console.log('Unable to parse chrome trace file.', e); + } + this.extendAndSanitizeModel(data, keys); + return data; + } + + createModelFromV8TraceFile(contents) { + console.log('Processing log as V8 trace file.'); + contents = contents.map(function(line) { + try { + // Strip away a potentially present adb logcat prefix. + line = line.replace(/^I\/v8\s*\(\d+\):\s+/g, ''); + return JSON.parse(line); + } catch (e) { + console.log('Unable to parse line: \'' + line + '\'\' (' + e + ')'); + } + return null; + }); + + const data = Object.create(null); // Final data container. + const keys = Object.create(null); // Collecting 'keys' per isolate. + + for (var entry of contents) { + if (entry === null || entry.type === undefined) { + continue; + } + if (entry.type === 'zone') { + this.createOrUpdateEntryIfNeeded(data, keys, entry); + const stacktrace = ('stacktrace' in entry) ? entry.stacktrace : []; + data[entry.isolate].samples.zone[entry.time] = { + allocated: entry.allocated, + pooled: entry.pooled, + stacktrace: stacktrace + }; + } else if ( + entry.type === 'zonecreation' || entry.type === 'zonedestruction') { + this.createOrUpdateEntryIfNeeded(data, keys, entry); + data[entry.isolate].zonetags.push( + Object.assign({opening: entry.type === 'zonecreation'}, entry)); + } else if (entry.type === 'gc_descriptor') { + this.createOrUpdateEntryIfNeeded(data, keys, entry); + data[entry.isolate].gcs[entry.id].time = entry.time; + if ('zone' in entry) + data[entry.isolate].gcs[entry.id].malloced = entry.zone; + } else if (entry.type === 'instance_type_data') { + if (entry.id in data[entry.isolate].gcs) { + this.createOrUpdateEntryIfNeeded(data, keys, entry); + this.createDatasetIfNeeded(data, keys, entry, entry.key); + this.addInstanceTypeData( + data, keys, entry.isolate, entry.id, entry.key, + entry.instance_type_name, entry); + } + } else if (entry.type === 'bucket_sizes') { + if (entry.id in data[entry.isolate].gcs) { + this.createOrUpdateEntryIfNeeded(data, keys, entry); + this.createDatasetIfNeeded(data, keys, entry, entry.key); + data[entry.isolate].gcs[entry.id][entry.key].bucket_sizes = + entry.sizes; + } + } else { + console.log('Unknown entry type: ' + entry.type); + } + } + this.extendAndSanitizeModel(data, keys); + return data; + } +} + +customElements.define('trace-file-reader', TraceFileReader); |