// 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'; export const KIND_FREED_MEMORY = 'kind-detailed-freed'; 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'], [KIND_FREED_MEMORY, 'Freed 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; 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; } });