• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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