1// Copyright 2018 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} from './categories.js'; 8 9export const VIEW_BY_INSTANCE_TYPE = 'by-instance-type'; 10export const VIEW_BY_INSTANCE_CATEGORY = 'by-instance-category'; 11export const VIEW_BY_FIELD_TYPE = 'by-field-type'; 12 13defineCustomElement('details-selection', (templateText) => 14 class DetailsSelection extends HTMLElement { 15 constructor() { 16 super(); 17 const shadowRoot = this.attachShadow({mode: 'open'}); 18 shadowRoot.innerHTML = templateText; 19 this.isolateSelect.addEventListener( 20 'change', e => this.handleIsolateChange(e)); 21 this.dataViewSelect.addEventListener( 22 'change', e => this.notifySelectionChanged(e)); 23 this.datasetSelect.addEventListener( 24 'change', e => this.notifySelectionChanged(e)); 25 this.gcSelect.addEventListener( 26 'change', e => this.notifySelectionChanged(e)); 27 this.$('#csv-export-btn') 28 .addEventListener('click', e => this.exportCurrentSelection(e)); 29 this.$('#category-filter-btn') 30 .addEventListener('click', e => this.filterCurrentSelection(e)); 31 this.$('#category-auto-filter-btn') 32 .addEventListener('click', e => this.filterTop20Categories(e)); 33 this._data = undefined; 34 this.selection = undefined; 35 } 36 37 connectedCallback() { 38 for (let category of CATEGORIES.keys()) { 39 this.$('#categories').appendChild(this.buildCategory(category)); 40 } 41 } 42 43 dataChanged() { 44 this.selection = {categories: {}}; 45 this.resetUI(true); 46 this.populateIsolateSelect(); 47 this.handleIsolateChange(); 48 this.$('#dataSelectionSection').style.display = 'block'; 49 } 50 51 set data(value) { 52 this._data = value; 53 this.dataChanged(); 54 } 55 56 get data() { 57 return this._data; 58 } 59 60 get selectedIsolate() { 61 return this._data[this.selection.isolate]; 62 } 63 64 get selectedData() { 65 console.assert(this.data, 'invalid data'); 66 console.assert(this.selection, 'invalid selection'); 67 return this.selectedIsolate.gcs[this.selection.gc][this.selection.data_set]; 68 } 69 70 $(id) { 71 return this.shadowRoot.querySelector(id); 72 } 73 74 querySelectorAll(query) { 75 return this.shadowRoot.querySelectorAll(query); 76 } 77 78 get dataViewSelect() { 79 return this.$('#data-view-select'); 80 } 81 82 get datasetSelect() { 83 return this.$('#dataset-select'); 84 } 85 86 get isolateSelect() { 87 return this.$('#isolate-select'); 88 } 89 90 get gcSelect() { 91 return this.$('#gc-select'); 92 } 93 94 buildCategory(name) { 95 const div = document.createElement('div'); 96 div.id = name; 97 div.classList.add('box'); 98 99 let ul = document.createElement('ul'); 100 ul.className = 'categoryLabels' 101 { 102 const name_li = document.createElement('li'); 103 name_li.textContent = CATEGORY_NAMES.get(name); 104 ul.appendChild(name_li); 105 106 const percent_li = document.createElement('li'); 107 percent_li.textContent = '0%'; 108 percent_li.id = name + 'PercentContent'; 109 ul.appendChild(percent_li); 110 } 111 div.appendChild(ul); 112 113 ul = document.createElement('ul'); 114 ul.className = 'categorySelectionButtons' 115 { 116 const all_li = document.createElement('li'); 117 const all_button = document.createElement('button'); 118 all_button.textContent = 'All'; 119 all_button.addEventListener('click', e => this.selectCategory(name)); 120 all_li.appendChild(all_button); 121 ul.appendChild(all_li); 122 123 const top_li = document.createElement('li'); 124 const top_button = document.createElement('button'); 125 top_button.textContent = 'Top 10'; 126 top_button.addEventListener( 127 'click', e => this.selectCategoryTopEntries(name)); 128 top_li.appendChild(top_button); 129 ul.appendChild(top_li); 130 131 const none_li = document.createElement('li'); 132 const none_button = document.createElement('button'); 133 none_button.textContent = 'None'; 134 none_button.addEventListener('click', e => this.unselectCategory(name)); 135 none_li.appendChild(none_button); 136 ul.appendChild(none_li); 137 } 138 div.appendChild(ul); 139 140 const innerDiv = document.createElement('div'); 141 innerDiv.id = name + 'Content'; 142 innerDiv.className = 'categoryContent'; 143 div.appendChild(innerDiv); 144 145 const percentDiv = document.createElement('div'); 146 percentDiv.className = 'percentBackground'; 147 percentDiv.id = name + 'PercentBackground'; 148 div.appendChild(percentDiv); 149 return div; 150 } 151 152 populateIsolateSelect() { 153 let isolates = Object.entries(this.data); 154 // Sorty by peak heap memory consumption. 155 isolates.sort((a, b) => b[1].peakMemory - a[1].peakMemory); 156 this.populateSelect( 157 '#isolate-select', isolates, (key, isolate) => isolate.getLabel()); 158 } 159 160 resetUI(resetIsolateSelect) { 161 if (resetIsolateSelect) removeAllChildren(this.isolateSelect); 162 163 removeAllChildren(this.dataViewSelect); 164 removeAllChildren(this.datasetSelect); 165 removeAllChildren(this.gcSelect); 166 this.clearCategories(); 167 this.setButtonState('disabled'); 168 } 169 170 setButtonState(disabled) { 171 this.$('#csv-export-btn').disabled = disabled; 172 this.$('#category-filter').disabled = disabled; 173 this.$('#category-filter-btn').disabled = disabled; 174 this.$('#category-auto-filter-btn').disabled = disabled; 175 } 176 177 handleIsolateChange(e) { 178 this.selection.isolate = this.isolateSelect.value; 179 if (this.selection.isolate.length === 0) { 180 this.selection.isolate = null; 181 return; 182 } 183 this.resetUI(false); 184 this.populateSelect( 185 '#data-view-select', [ 186 [VIEW_BY_INSTANCE_TYPE, 'Selected instance types'], 187 [VIEW_BY_INSTANCE_CATEGORY, 'Selected type categories'], 188 [VIEW_BY_FIELD_TYPE, 'Field type statistics'] 189 ], 190 (key, label) => label, VIEW_BY_INSTANCE_TYPE); 191 this.populateSelect( 192 '#dataset-select', this.selectedIsolate.data_sets.entries(), null, 193 'live'); 194 this.populateSelect( 195 '#gc-select', 196 Object.keys(this.selectedIsolate.gcs) 197 .map(id => [id, this.selectedIsolate.gcs[id].time]), 198 (key, time, index) => { 199 return (index + ': ').padStart(4, '0') + 200 formatSeconds(time).padStart(6, '0') + ' ' + 201 formatBytes(this.selectedIsolate.gcs[key].live.overall) 202 .padStart(9, '0'); 203 }); 204 this.populateCategories(); 205 this.notifySelectionChanged(); 206 } 207 208 notifySelectionChanged(e) { 209 if (!this.selection.isolate) return; 210 211 this.selection.data_view = this.dataViewSelect.value; 212 this.selection.categories = {}; 213 if (this.selection.data_view === VIEW_BY_FIELD_TYPE) { 214 this.$('#categories').style.display = 'none'; 215 } else { 216 for (let category of CATEGORIES.keys()) { 217 const selected = this.selectedInCategory(category); 218 if (selected.length > 0) this.selection.categories[category] = selected; 219 } 220 this.$('#categories').style.display = 'block'; 221 } 222 this.selection.category_names = CATEGORY_NAMES; 223 this.selection.data_set = this.datasetSelect.value; 224 this.selection.gc = this.gcSelect.value; 225 this.setButtonState(false); 226 this.updatePercentagesInCategory(); 227 this.updatePercentagesInInstanceTypes(); 228 this.dispatchEvent(new CustomEvent( 229 'change', {bubbles: true, composed: true, detail: this.selection})); 230 } 231 232 filterCurrentSelection(e) { 233 const minSize = this.$('#category-filter').value * KB; 234 this.filterCurrentSelectionWithThresold(minSize); 235 } 236 237 filterTop20Categories(e) { 238 // Limit to show top 20 categories only. 239 let minSize = 0; 240 let count = 0; 241 let sizes = this.selectedIsolate.instanceTypePeakMemory; 242 for (let key in sizes) { 243 if (count == 20) break; 244 minSize = sizes[key]; 245 count++; 246 } 247 this.filterCurrentSelectionWithThresold(minSize); 248 } 249 250 filterCurrentSelectionWithThresold(minSize) { 251 if (minSize === 0) return; 252 253 this.selection.category_names.forEach((_, category) => { 254 for (let checkbox of this.querySelectorAll( 255 'input[name=' + category + 'Checkbox]')) { 256 checkbox.checked = 257 this.selectedData.instance_type_data[checkbox.instance_type] 258 .overall > minSize; 259 console.log( 260 checkbox.instance_type, checkbox.checked, 261 this.selectedData.instance_type_data[checkbox.instance_type] 262 .overall); 263 } 264 }); 265 this.notifySelectionChanged(); 266 } 267 268 updatePercentagesInCategory() { 269 const overalls = {}; 270 let overall = 0; 271 // Reset all categories. 272 this.selection.category_names.forEach((_, category) => { 273 overalls[category] = 0; 274 }); 275 // Only update categories that have selections. 276 Object.entries(this.selection.categories).forEach(([category, value]) => { 277 overalls[category] = 278 Object.values(value).reduce( 279 (accu, current) => 280 accu + this.selectedData.instance_type_data[current].overall, 281 0) / 282 KB; 283 overall += overalls[category]; 284 }); 285 Object.entries(overalls).forEach(([category, category_overall]) => { 286 let percents = category_overall / overall * 100; 287 this.$(`#${category}PercentContent`).textContent = 288 `${percents.toFixed(1)}%`; 289 this.$('#' + category + 'PercentBackground').style.left = percents + '%'; 290 }); 291 } 292 293 updatePercentagesInInstanceTypes() { 294 const instanceTypeData = this.selectedData.instance_type_data; 295 const maxInstanceType = this.selectedData.singleInstancePeakMemory; 296 this.querySelectorAll('.instanceTypeSelectBox input').forEach(checkbox => { 297 let instanceType = checkbox.value; 298 let instanceTypeSize = instanceTypeData[instanceType].overall; 299 let percents = instanceTypeSize / maxInstanceType; 300 let percentDiv = checkbox.parentNode.querySelector('.percentBackground'); 301 percentDiv.style.left = (percents * 100) + '%'; 302 303 }); 304 } 305 306 selectedInCategory(category) { 307 let tmp = []; 308 this.querySelectorAll('input[name=' + category + 'Checkbox]:checked') 309 .forEach(checkbox => tmp.push(checkbox.value)); 310 return tmp; 311 } 312 313 categoryForType(instance_type) { 314 for (let [key, value] of CATEGORIES.entries()) { 315 if (value.has(instance_type)) return key; 316 } 317 return 'unclassified'; 318 } 319 320 createOption(value, text) { 321 const option = document.createElement('option'); 322 option.value = value; 323 option.text = text; 324 return option; 325 } 326 327 populateSelect(id, iterable, labelFn = null, autoselect = null) { 328 if (labelFn == null) labelFn = e => e; 329 let index = 0; 330 for (let [key, value] of iterable) { 331 index++; 332 const label = labelFn(key, value, index); 333 const option = this.createOption(key, label); 334 if (autoselect === key) { 335 option.selected = 'selected'; 336 } 337 this.$(id).appendChild(option); 338 } 339 } 340 341 clearCategories() { 342 for (const category of CATEGORIES.keys()) { 343 let f = this.$('#' + category + 'Content'); 344 while (f.firstChild) { 345 f.removeChild(f.firstChild); 346 } 347 } 348 } 349 350 populateCategories() { 351 this.clearCategories(); 352 const categories = {__proto__:null}; 353 for (let cat of CATEGORIES.keys()) { 354 categories[cat] = []; 355 } 356 357 for (let instance_type of this.selectedIsolate.non_empty_instance_types) { 358 const category = this.categoryForType(instance_type); 359 categories[category].push(instance_type); 360 } 361 for (let category of Object.keys(categories)) { 362 categories[category].sort(); 363 for (let instance_type of categories[category]) { 364 this.$('#' + category + 'Content') 365 .appendChild(this.createCheckBox(instance_type, category)); 366 } 367 } 368 } 369 370 unselectCategory(category) { 371 this.querySelectorAll('input[name=' + category + 'Checkbox]') 372 .forEach(checkbox => checkbox.checked = false); 373 this.notifySelectionChanged(); 374 } 375 376 selectCategory(category) { 377 this.querySelectorAll('input[name=' + category + 'Checkbox]') 378 .forEach(checkbox => checkbox.checked = true); 379 this.notifySelectionChanged(); 380 } 381 382 selectCategoryTopEntries(category) { 383 // unselect all checkboxes in this category. 384 this.querySelectorAll('input[name=' + category + 'Checkbox]') 385 .forEach(checkbox => checkbox.checked = false); 386 const data = this.selectedData.instance_type_data; 387 388 // Get the max values for instance_types in this category 389 const categoryInstanceTypes = Array.from(CATEGORIES.get(category)); 390 categoryInstanceTypes.filter(each => each in data) 391 .sort((a,b) => { 392 return data[b].overall - data[a].overall; 393 }).slice(0, 10).forEach((category) => { 394 this.$('#' + category + 'Checkbox').checked = true; 395 }); 396 this.notifySelectionChanged(); 397 } 398 399 createCheckBox(instance_type, category) { 400 const div = document.createElement('div'); 401 div.classList.add('instanceTypeSelectBox'); 402 const input = document.createElement('input'); 403 div.appendChild(input); 404 input.type = 'checkbox'; 405 input.name = category + 'Checkbox'; 406 input.checked = 'checked'; 407 input.id = instance_type + 'Checkbox'; 408 input.instance_type = instance_type; 409 input.value = instance_type; 410 input.addEventListener('change', e => this.notifySelectionChanged(e)); 411 const label = document.createElement('label'); 412 div.appendChild(label); 413 label.innerText = instance_type; 414 label.htmlFor = instance_type + 'Checkbox'; 415 const percentDiv = document.createElement('div'); 416 percentDiv.className = 'percentBackground'; 417 div.appendChild(percentDiv); 418 return div; 419 } 420 421 exportCurrentSelection(e) { 422 const data = []; 423 const selected_data = 424 this.selectedIsolate.gcs[this.selection.gc][this.selection.data_set] 425 .instance_type_data; 426 Object.values(this.selection.categories).forEach(instance_types => { 427 instance_types.forEach(instance_type => { 428 data.push([instance_type, selected_data[instance_type].overall / KB]); 429 }); 430 }); 431 const createInlineContent = arrayOfRows => { 432 const content = arrayOfRows.reduce( 433 (accu, rowAsArray) => {return accu + `${rowAsArray.join(',')}\n`}, 434 ''); 435 return `data:text/csv;charset=utf-8,${content}`; 436 }; 437 const encodedUri = encodeURI(createInlineContent(data)); 438 const link = document.createElement('a'); 439 link.setAttribute('href', encodedUri); 440 link.setAttribute( 441 'download', 442 `heap_objects_data_${this.selection.isolate}_${this.selection.gc}.csv`); 443 this.shadowRoot.appendChild(link); 444 link.click(); 445 this.shadowRoot.removeChild(link); 446 } 447}); 448