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