Home | History | Annotate | Download | only in heap-stats
      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 
      7 const details_selection_template =
      8     document.currentScript.ownerDocument.querySelector(
      9         '#details-selection-template');
     10 
     11 const VIEW_BY_INSTANCE_TYPE = 'by-instance-type';
     12 const VIEW_BY_INSTANCE_CATEGORY = 'by-instance-category';
     13 const VIEW_BY_FIELD_TYPE = 'by-field-type';
     14 
     15 class 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 
    408 customElements.define('details-selection', DetailsSelection);
    409