enclose-io/compiler

View on GitHub
lts/deps/v8/tools/heap-stats/details-selection.js

Summary

Maintainability
F
2 wks
Test Coverage
// Copyright 2018 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';

const details_selection_template =
    document.currentScript.ownerDocument.querySelector(
        '#details-selection-template');

const VIEW_BY_INSTANCE_TYPE = 'by-instance-type';
const VIEW_BY_INSTANCE_CATEGORY = 'by-instance-category';
const VIEW_BY_FIELD_TYPE = 'by-field-type';

class DetailsSelection extends HTMLElement {
  constructor() {
    super();
    const shadowRoot = this.attachShadow({mode: 'open'});
    shadowRoot.appendChild(details_selection_template.content.cloneNode(true));
    this.isolateSelect.addEventListener(
        'change', e => this.handleIsolateChange(e));
    this.dataViewSelect.addEventListener(
        'change', e => this.notifySelectionChanged(e));
    this.datasetSelect.addEventListener(
        'change', e => this.notifySelectionChanged(e));
    this.gcSelect.addEventListener(
      'change', e => this.notifySelectionChanged(e));
    this.$('#csv-export-btn')
        .addEventListener('click', e => this.exportCurrentSelection(e));
    this.$('#category-filter-btn')
        .addEventListener('click', e => this.filterCurrentSelection(e));
    this.$('#category-auto-filter-btn')
        .addEventListener('click', e => this.filterTop20Categories(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');
    return this.selectedIsolate.gcs[this.selection.gc][this.selection.data_set];
  }

  $(id) {
    return this.shadowRoot.querySelector(id);
  }

  querySelectorAll(query) {
    return this.shadowRoot.querySelectorAll(query);
  }

  get dataViewSelect() {
    return this.$('#data-view-select');
  }

  get datasetSelect() {
    return this.$('#dataset-select');
  }

  get isolateSelect() {
    return this.$('#isolate-select');
  }

  get gcSelect() {
    return this.$('#gc-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: {}};
    this.resetUI(true);
    this.populateIsolateSelect();
    this.handleIsolateChange();
    this.$('#dataSelectionSection').style.display = 'block';
  }

  populateIsolateSelect() {
    let isolates = Object.entries(this.data);
    // Sorty by peak heap memory consumption.
    isolates.sort((a, b) => b[1].peakMemory - a[1].peakMemory);
    this.populateSelect(
        '#isolate-select', isolates, (key, isolate) => isolate.getLabel());
  }

  resetUI(resetIsolateSelect) {
    if (resetIsolateSelect) removeAllChildren(this.isolateSelect);

    removeAllChildren(this.dataViewSelect);
    removeAllChildren(this.datasetSelect);
    removeAllChildren(this.gcSelect);
    this.clearCategories();
    this.setButtonState('disabled');
  }

  setButtonState(disabled) {
    this.$('#csv-export-btn').disabled = disabled;
    this.$('#category-filter').disabled = disabled;
    this.$('#category-filter-btn').disabled = disabled;
    this.$('#category-auto-filter-btn').disabled = disabled;
  }

  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_BY_INSTANCE_TYPE, 'Selected instance types'],
          [VIEW_BY_INSTANCE_CATEGORY, 'Selected type categories'],
          [VIEW_BY_FIELD_TYPE, 'Field type statistics']
        ],
        (key, label) => label, VIEW_BY_INSTANCE_TYPE);
    this.populateSelect(
        '#dataset-select', this.selectedIsolate.data_sets.entries(), null,
        'live');
    this.populateSelect(
        '#gc-select',
        Object.keys(this.selectedIsolate.gcs)
            .map(id => [id, this.selectedIsolate.gcs[id].time]),
        (key, time, index) => {
          return (index + ': ').padStart(4, '0') +
              formatSeconds(time).padStart(6, '0') + ' ' +
              formatBytes(this.selectedIsolate.gcs[key].live.overall)
                  .padStart(9, '0');
        });
    this.populateCategories();
    this.notifySelectionChanged();
  }

  notifySelectionChanged(e) {
    if (!this.selection.isolate) return;

    this.selection.data_view = this.dataViewSelect.value;
    this.selection.categories = {};
    if (this.selection.data_view === VIEW_BY_FIELD_TYPE) {
      this.$('#categories').style.display = 'none';
    } else {
      for (let category of CATEGORIES.keys()) {
        const selected = this.selectedInCategory(category);
        if (selected.length > 0) this.selection.categories[category] = selected;
      }
      this.$('#categories').style.display = 'block';
    }
    this.selection.category_names = CATEGORY_NAMES;
    this.selection.data_set = this.datasetSelect.value;
    this.selection.gc = this.gcSelect.value;
    this.setButtonState(false);
    this.updatePercentagesInCategory();
    this.updatePercentagesInInstanceTypes();
    this.dispatchEvent(new CustomEvent(
        'change', {bubbles: true, composed: true, detail: this.selection}));
  }

  filterCurrentSelection(e) {
    const minSize = this.$('#category-filter').value * KB;
    this.filterCurrentSelectionWithThresold(minSize);
  }

  filterTop20Categories(e) {
    // Limit to show top 20 categories only.
    let minSize = 0;
    let count = 0;
    let sizes = this.selectedIsolate.instanceTypePeakMemory;
    for (let key in sizes) {
      if (count == 20) break;
      minSize = sizes[key];
      count++;
    }
    this.filterCurrentSelectionWithThresold(minSize);
  }

  filterCurrentSelectionWithThresold(minSize) {
    if (minSize === 0) return;

    this.selection.category_names.forEach((_, category) => {
      for (let checkbox of this.querySelectorAll(
               'input[name=' + category + 'Checkbox]')) {
        checkbox.checked =
            this.selectedData.instance_type_data[checkbox.instance_type]
                .overall > minSize;
        console.log(
            checkbox.instance_type, checkbox.checked,
            this.selectedData.instance_type_data[checkbox.instance_type]
                .overall);
      }
    });
    this.notifySelectionChanged();
  }

  updatePercentagesInCategory() {
    const overalls = {};
    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) =>
                  accu + this.selectedData.instance_type_data[current].overall,
              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 + '%';
    });
  }

  updatePercentagesInInstanceTypes() {
    const instanceTypeData = this.selectedData.instance_type_data;
    const maxInstanceType = this.selectedData.singleInstancePeakMemory;
    this.querySelectorAll('.instanceTypeSelectBox  input').forEach(checkbox => {
      let instanceType = checkbox.value;
      let instanceTypeSize = instanceTypeData[instanceType].overall;
      let percents = instanceTypeSize / maxInstanceType;
      let percentDiv = checkbox.parentNode.querySelector('.percentBackground');
      percentDiv.style.left = (percents * 100) + '%';

    });
  }

  selectedInCategory(category) {
    let tmp = [];
    this.querySelectorAll('input[name=' + category + 'Checkbox]:checked')
        .forEach(checkbox => tmp.push(checkbox.value));
    return tmp;
  }

  categoryForType(instance_type) {
    for (let [key, value] of CATEGORIES.entries()) {
      if (value.has(instance_type)) return key;
    }
    return 'unclassified';
  }

  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 = {};
    for (let cat of CATEGORIES.keys()) {
      categories[cat] = [];
    }

    for (let instance_type of this.selectedIsolate.non_empty_instance_types) {
      const category = this.categoryForType(instance_type);
      categories[category].push(instance_type);
    }
    for (let category of Object.keys(categories)) {
      categories[category].sort();
      for (let instance_type of categories[category]) {
        this.$('#' + category + 'Content')
            .appendChild(this.createCheckBox(instance_type, 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('instanceTypeSelectBox');
    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;
  }

  exportCurrentSelection(e) {
    const data = [];
    const selected_data =
        this.selectedIsolate.gcs[this.selection.gc][this.selection.data_set]
            .instance_type_data;
    Object.values(this.selection.categories).forEach(instance_types => {
      instance_types.forEach(instance_type => {
        data.push([instance_type, selected_data[instance_type].overall / KB]);
      });
    });
    const createInlineContent = arrayOfRows => {
      const content = arrayOfRows.reduce(
          (accu, rowAsArray) => {return accu + `${rowAsArray.join(',')}\n`},
          '');
      return `data:text/csv;charset=utf-8,${content}`;
    };
    const encodedUri = encodeURI(createInlineContent(data));
    const link = document.createElement('a');
    link.setAttribute('href', encodedUri);
    link.setAttribute(
        'download',
        `heap_objects_data_${this.selection.isolate}_${this.selection.gc}.csv`);
    this.shadowRoot.appendChild(link);
    link.click();
    this.shadowRoot.removeChild(link);
  }
}

customElements.define('details-selection', DetailsSelection);