1// Copyright 2020 the V8 project authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5'use strict'; 6 7import {CATEGORIES, CATEGORY_NAMES, categoryByZoneName} from './categories.js'; 8 9export const VIEW_TOTALS = 'by-totals'; 10export const VIEW_BY_ZONE_NAME = 'by-zone-name'; 11export const VIEW_BY_ZONE_CATEGORY = 'by-zone-category'; 12 13export const KIND_ALLOCATED_MEMORY = 'kind-detailed-allocated'; 14export const KIND_USED_MEMORY = 'kind-detailed-used'; 15export const KIND_FREED_MEMORY = 'kind-detailed-freed'; 16 17defineCustomElement('details-selection', (templateText) => 18 class DetailsSelection extends HTMLElement { 19 constructor() { 20 super(); 21 const shadowRoot = this.attachShadow({mode: 'open'}); 22 shadowRoot.innerHTML = templateText; 23 this.isolateSelect.addEventListener( 24 'change', e => this.handleIsolateChange(e)); 25 this.dataViewSelect.addEventListener( 26 'change', e => this.notifySelectionChanged(e)); 27 this.dataKindSelect.addEventListener( 28 'change', e => this.notifySelectionChanged(e)); 29 this.showTotalsSelect.addEventListener( 30 'change', e => this.notifySelectionChanged(e)); 31 this.memoryUsageSampleSelect.addEventListener( 32 'change', e => this.notifySelectionChanged(e)); 33 this.timeStartSelect.addEventListener( 34 'change', e => this.notifySelectionChanged(e)); 35 this.timeEndSelect.addEventListener( 36 'change', e => this.notifySelectionChanged(e)); 37 } 38 39 connectedCallback() { 40 for (let category of CATEGORIES.keys()) { 41 this.$('#categories').appendChild(this.buildCategory(category)); 42 } 43 } 44 45 set data(value) { 46 this._data = value; 47 this.dataChanged(); 48 } 49 50 get data() { 51 return this._data; 52 } 53 54 get selectedIsolate() { 55 return this._data[this.selection.isolate]; 56 } 57 58 get selectedData() { 59 console.assert(this.data, 'invalid data'); 60 console.assert(this.selection, 'invalid selection'); 61 const time = this.selection.time; 62 return this.selectedIsolate.samples.get(time); 63 } 64 65 $(id) { 66 return this.shadowRoot.querySelector(id); 67 } 68 69 querySelectorAll(query) { 70 return this.shadowRoot.querySelectorAll(query); 71 } 72 73 get dataViewSelect() { 74 return this.$('#data-view-select'); 75 } 76 77 get dataKindSelect() { 78 return this.$('#data-kind-select'); 79 } 80 81 get isolateSelect() { 82 return this.$('#isolate-select'); 83 } 84 85 get memoryUsageSampleSelect() { 86 return this.$('#memory-usage-sample-select'); 87 } 88 89 get showTotalsSelect() { 90 return this.$('#show-totals-select'); 91 } 92 93 get timeStartSelect() { 94 return this.$('#time-start-select'); 95 } 96 97 get timeEndSelect() { 98 return this.$('#time-end-select'); 99 } 100 101 buildCategory(name) { 102 const div = document.createElement('div'); 103 div.id = name; 104 div.classList.add('box'); 105 const ul = document.createElement('ul'); 106 div.appendChild(ul); 107 const name_li = document.createElement('li'); 108 ul.appendChild(name_li); 109 name_li.innerHTML = CATEGORY_NAMES.get(name); 110 const percent_li = document.createElement('li'); 111 ul.appendChild(percent_li); 112 percent_li.innerHTML = '0%'; 113 percent_li.id = name + 'PercentContent'; 114 const all_li = document.createElement('li'); 115 ul.appendChild(all_li); 116 const all_button = document.createElement('button'); 117 all_li.appendChild(all_button); 118 all_button.innerHTML = 'All'; 119 all_button.addEventListener('click', e => this.selectCategory(name)); 120 const none_li = document.createElement('li'); 121 ul.appendChild(none_li); 122 const none_button = document.createElement('button'); 123 none_li.appendChild(none_button); 124 none_button.innerHTML = 'None'; 125 none_button.addEventListener('click', e => this.unselectCategory(name)); 126 const innerDiv = document.createElement('div'); 127 div.appendChild(innerDiv); 128 innerDiv.id = name + 'Content'; 129 const percentDiv = document.createElement('div'); 130 div.appendChild(percentDiv); 131 percentDiv.className = 'percentBackground'; 132 percentDiv.id = name + 'PercentBackground'; 133 return div; 134 } 135 136 dataChanged() { 137 this.selection = {categories: {}, zones: new Map()}; 138 this.resetUI(true); 139 this.populateIsolateSelect(); 140 this.handleIsolateChange(); 141 this.$('#dataSelectionSection').style.display = 'block'; 142 } 143 144 populateIsolateSelect() { 145 let isolates = Object.entries(this.data); 146 // Sort by peak heap memory consumption. 147 isolates.sort((a, b) => b[1].peakAllocatedMemory - a[1].peakAllocatedMemory); 148 this.populateSelect( 149 '#isolate-select', isolates, (key, isolate) => isolate.getLabel()); 150 } 151 152 resetUI(resetIsolateSelect) { 153 if (resetIsolateSelect) removeAllChildren(this.isolateSelect); 154 155 removeAllChildren(this.dataViewSelect); 156 removeAllChildren(this.dataKindSelect); 157 removeAllChildren(this.memoryUsageSampleSelect); 158 this.clearCategories(); 159 } 160 161 handleIsolateChange(e) { 162 this.selection.isolate = this.isolateSelect.value; 163 if (this.selection.isolate.length === 0) { 164 this.selection.isolate = null; 165 return; 166 } 167 this.resetUI(false); 168 this.populateSelect( 169 '#data-view-select', [ 170 [VIEW_TOTALS, 'Total memory usage'], 171 [VIEW_BY_ZONE_NAME, 'Selected zones types'], 172 [VIEW_BY_ZONE_CATEGORY, 'Selected zone categories'], 173 ], 174 (key, label) => label, VIEW_TOTALS); 175 this.populateSelect( 176 '#data-kind-select', [ 177 [KIND_ALLOCATED_MEMORY, 'Allocated memory per zone'], 178 [KIND_USED_MEMORY, 'Used memory per zone'], 179 [KIND_FREED_MEMORY, 'Freed memory per zone'], 180 ], 181 (key, label) => label, KIND_ALLOCATED_MEMORY); 182 183 this.populateSelect( 184 '#memory-usage-sample-select', 185 [...this.selectedIsolate.samples.entries()].filter(([time, sample]) => { 186 // Remove samples that does not have detailed per-zone data. 187 return sample.zones !== undefined; 188 }), 189 (time, sample, index) => { 190 return ((index + ': ').padStart(6, '\u00A0') + 191 formatSeconds(time).padStart(8, '\u00A0') + ' ' + 192 formatBytes(sample.allocated).padStart(12, '\u00A0')); 193 }, 194 this.selectedIsolate.peakUsageTime); 195 196 this.timeStartSelect.value = this.selectedIsolate.start; 197 this.timeEndSelect.value = this.selectedIsolate.end; 198 199 this.populateCategories(); 200 this.notifySelectionChanged(); 201 } 202 203 notifySelectionChanged(e) { 204 if (!this.selection.isolate) return; 205 206 this.selection.data_view = this.dataViewSelect.value; 207 this.selection.data_kind = this.dataKindSelect.value; 208 this.selection.categories = Object.create(null); 209 this.selection.zones = new Map(); 210 this.$('#categories').style.display = 'none'; 211 for (let category of CATEGORIES.keys()) { 212 const selected = this.selectedInCategory(category); 213 if (selected.length > 0) this.selection.categories[category] = selected; 214 for (const zone_name of selected) { 215 this.selection.zones.set(zone_name, category); 216 } 217 } 218 this.$('#categories').style.display = 'block'; 219 this.selection.category_names = CATEGORY_NAMES; 220 this.selection.show_totals = this.showTotalsSelect.checked; 221 this.selection.time = Number(this.memoryUsageSampleSelect.value); 222 this.selection.timeStart = Number(this.timeStartSelect.value); 223 this.selection.timeEnd = Number(this.timeEndSelect.value); 224 this.updatePercentagesInCategory(); 225 this.updatePercentagesInZones(); 226 this.dispatchEvent(new CustomEvent( 227 'change', {bubbles: true, composed: true, detail: this.selection})); 228 } 229 230 updatePercentagesInCategory() { 231 const overalls = Object.create(null); 232 let overall = 0; 233 // Reset all categories. 234 this.selection.category_names.forEach((_, category) => { 235 overalls[category] = 0; 236 }); 237 // Only update categories that have selections. 238 Object.entries(this.selection.categories).forEach(([category, value]) => { 239 overalls[category] = 240 Object.values(value).reduce( 241 (accu, current) => { 242 const zone_data = this.selectedData.zones.get(current); 243 return zone_data === undefined ? accu 244 : accu + zone_data.allocated; 245 }, 0) / 246 KB; 247 overall += overalls[category]; 248 }); 249 Object.entries(overalls).forEach(([category, category_overall]) => { 250 let percents = category_overall / overall * 100; 251 this.$(`#${category}PercentContent`).innerHTML = 252 `${percents.toFixed(1)}%`; 253 this.$('#' + category + 'PercentBackground').style.left = percents + '%'; 254 }); 255 } 256 257 updatePercentagesInZones() { 258 const selected_data = this.selectedData; 259 const zones_data = selected_data.zones; 260 const total_allocated = selected_data.allocated; 261 this.querySelectorAll('.zonesSelectBox input').forEach(checkbox => { 262 const zone_name = checkbox.value; 263 const zone_data = zones_data.get(zone_name); 264 const zone_allocated = zone_data === undefined ? 0 : zone_data.allocated; 265 const percents = zone_allocated / total_allocated; 266 const percent_div = checkbox.parentNode.querySelector('.percentBackground'); 267 percent_div.style.left = (percents * 100) + '%'; 268 checkbox.parentNode.style.display = 'block'; 269 }); 270 } 271 272 selectedInCategory(category) { 273 let tmp = []; 274 this.querySelectorAll('input[name=' + category + 'Checkbox]:checked') 275 .forEach(checkbox => tmp.push(checkbox.value)); 276 return tmp; 277 } 278 279 createOption(value, text) { 280 const option = document.createElement('option'); 281 option.value = value; 282 option.text = text; 283 return option; 284 } 285 286 populateSelect(id, iterable, labelFn = null, autoselect = null) { 287 if (labelFn == null) labelFn = e => e; 288 let index = 0; 289 for (let [key, value] of iterable) { 290 index++; 291 const label = labelFn(key, value, index); 292 const option = this.createOption(key, label); 293 if (autoselect === key) { 294 option.selected = 'selected'; 295 } 296 this.$(id).appendChild(option); 297 } 298 } 299 300 clearCategories() { 301 for (const category of CATEGORIES.keys()) { 302 let f = this.$('#' + category + 'Content'); 303 while (f.firstChild) { 304 f.removeChild(f.firstChild); 305 } 306 } 307 } 308 309 populateCategories() { 310 this.clearCategories(); 311 const categories = Object.create(null); 312 for (let cat of CATEGORIES.keys()) { 313 categories[cat] = []; 314 } 315 316 for (const [zone_name, zone_stats] of this.selectedIsolate.zones) { 317 const category = categoryByZoneName(zone_name); 318 categories[category].push(zone_name); 319 } 320 for (let category of Object.keys(categories)) { 321 categories[category].sort(); 322 for (let zone_name of categories[category]) { 323 this.$('#' + category + 'Content') 324 .appendChild(this.createCheckBox(zone_name, category)); 325 } 326 } 327 } 328 329 unselectCategory(category) { 330 this.querySelectorAll('input[name=' + category + 'Checkbox]') 331 .forEach(checkbox => checkbox.checked = false); 332 this.notifySelectionChanged(); 333 } 334 335 selectCategory(category) { 336 this.querySelectorAll('input[name=' + category + 'Checkbox]') 337 .forEach(checkbox => checkbox.checked = true); 338 this.notifySelectionChanged(); 339 } 340 341 createCheckBox(instance_type, category) { 342 const div = document.createElement('div'); 343 div.classList.add('zonesSelectBox'); 344 div.style.width = "200px"; 345 const input = document.createElement('input'); 346 div.appendChild(input); 347 input.type = 'checkbox'; 348 input.name = category + 'Checkbox'; 349 input.checked = 'checked'; 350 input.id = instance_type + 'Checkbox'; 351 input.instance_type = instance_type; 352 input.value = instance_type; 353 input.addEventListener('change', e => this.notifySelectionChanged(e)); 354 const label = document.createElement('label'); 355 div.appendChild(label); 356 label.innerText = instance_type; 357 label.htmlFor = instance_type + 'Checkbox'; 358 const percentDiv = document.createElement('div'); 359 percentDiv.className = 'percentBackground'; 360 div.appendChild(percentDiv); 361 return div; 362 } 363}); 364