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