• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright 2020 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, categoryByZoneName} from './categories.js';
8
9export const VIEW_TOTALS = 'by-totals';
10export const VIEW_BY_ZONE_NAME = 'by-zone-name';
11export const VIEW_BY_ZONE_CATEGORY = 'by-zone-category';
12
13export const KIND_ALLOCATED_MEMORY = 'kind-detailed-allocated';
14export const KIND_USED_MEMORY = 'kind-detailed-used';
15export const KIND_FREED_MEMORY = 'kind-detailed-freed';
16
17defineCustomElement('details-selection', (templateText) =>
18 class DetailsSelection extends HTMLElement {
19  constructor() {
20    super();
21    const shadowRoot = this.attachShadow({mode: 'open'});
22    shadowRoot.innerHTML = templateText;
23    this.isolateSelect.addEventListener(
24        'change', e => this.handleIsolateChange(e));
25    this.dataViewSelect.addEventListener(
26        'change', e => this.notifySelectionChanged(e));
27    this.dataKindSelect.addEventListener(
28        'change', e => this.notifySelectionChanged(e));
29    this.showTotalsSelect.addEventListener(
30        'change', e => this.notifySelectionChanged(e));
31    this.memoryUsageSampleSelect.addEventListener(
32        'change', e => this.notifySelectionChanged(e));
33    this.timeStartSelect.addEventListener(
34        'change', e => this.notifySelectionChanged(e));
35    this.timeEndSelect.addEventListener(
36        'change', e => this.notifySelectionChanged(e));
37  }
38
39  connectedCallback() {
40    for (let category of CATEGORIES.keys()) {
41      this.$('#categories').appendChild(this.buildCategory(category));
42    }
43  }
44
45  set data(value) {
46    this._data = value;
47    this.dataChanged();
48  }
49
50  get data() {
51    return this._data;
52  }
53
54  get selectedIsolate() {
55    return this._data[this.selection.isolate];
56  }
57
58  get selectedData() {
59    console.assert(this.data, 'invalid data');
60    console.assert(this.selection, 'invalid selection');
61    const time = this.selection.time;
62    return this.selectedIsolate.samples.get(time);
63  }
64
65  $(id) {
66    return this.shadowRoot.querySelector(id);
67  }
68
69  querySelectorAll(query) {
70    return this.shadowRoot.querySelectorAll(query);
71  }
72
73  get dataViewSelect() {
74    return this.$('#data-view-select');
75  }
76
77  get dataKindSelect() {
78    return this.$('#data-kind-select');
79  }
80
81  get isolateSelect() {
82    return this.$('#isolate-select');
83  }
84
85  get memoryUsageSampleSelect() {
86    return this.$('#memory-usage-sample-select');
87  }
88
89  get showTotalsSelect() {
90    return this.$('#show-totals-select');
91  }
92
93  get timeStartSelect() {
94    return this.$('#time-start-select');
95  }
96
97  get timeEndSelect() {
98    return this.$('#time-end-select');
99  }
100
101  buildCategory(name) {
102    const div = document.createElement('div');
103    div.id = name;
104    div.classList.add('box');
105    const ul = document.createElement('ul');
106    div.appendChild(ul);
107    const name_li = document.createElement('li');
108    ul.appendChild(name_li);
109    name_li.innerHTML = CATEGORY_NAMES.get(name);
110    const percent_li = document.createElement('li');
111    ul.appendChild(percent_li);
112    percent_li.innerHTML = '0%';
113    percent_li.id = name + 'PercentContent';
114    const all_li = document.createElement('li');
115    ul.appendChild(all_li);
116    const all_button = document.createElement('button');
117    all_li.appendChild(all_button);
118    all_button.innerHTML = 'All';
119    all_button.addEventListener('click', e => this.selectCategory(name));
120    const none_li = document.createElement('li');
121    ul.appendChild(none_li);
122    const none_button = document.createElement('button');
123    none_li.appendChild(none_button);
124    none_button.innerHTML = 'None';
125    none_button.addEventListener('click', e => this.unselectCategory(name));
126    const innerDiv = document.createElement('div');
127    div.appendChild(innerDiv);
128    innerDiv.id = name + 'Content';
129    const percentDiv = document.createElement('div');
130    div.appendChild(percentDiv);
131    percentDiv.className = 'percentBackground';
132    percentDiv.id = name + 'PercentBackground';
133    return div;
134  }
135
136  dataChanged() {
137    this.selection = {categories: {}, zones: new Map()};
138    this.resetUI(true);
139    this.populateIsolateSelect();
140    this.handleIsolateChange();
141    this.$('#dataSelectionSection').style.display = 'block';
142  }
143
144  populateIsolateSelect() {
145    let isolates = Object.entries(this.data);
146    // Sort by peak heap memory consumption.
147    isolates.sort((a, b) => b[1].peakAllocatedMemory - a[1].peakAllocatedMemory);
148    this.populateSelect(
149        '#isolate-select', isolates, (key, isolate) => isolate.getLabel());
150  }
151
152  resetUI(resetIsolateSelect) {
153    if (resetIsolateSelect) removeAllChildren(this.isolateSelect);
154
155    removeAllChildren(this.dataViewSelect);
156    removeAllChildren(this.dataKindSelect);
157    removeAllChildren(this.memoryUsageSampleSelect);
158    this.clearCategories();
159  }
160
161  handleIsolateChange(e) {
162    this.selection.isolate = this.isolateSelect.value;
163    if (this.selection.isolate.length === 0) {
164      this.selection.isolate = null;
165      return;
166    }
167    this.resetUI(false);
168    this.populateSelect(
169        '#data-view-select', [
170          [VIEW_TOTALS, 'Total memory usage'],
171          [VIEW_BY_ZONE_NAME, 'Selected zones types'],
172          [VIEW_BY_ZONE_CATEGORY, 'Selected zone categories'],
173        ],
174        (key, label) => label, VIEW_TOTALS);
175    this.populateSelect(
176      '#data-kind-select', [
177        [KIND_ALLOCATED_MEMORY, 'Allocated memory per zone'],
178        [KIND_USED_MEMORY, 'Used memory per zone'],
179        [KIND_FREED_MEMORY, 'Freed memory per zone'],
180      ],
181      (key, label) => label, KIND_ALLOCATED_MEMORY);
182
183    this.populateSelect(
184      '#memory-usage-sample-select',
185      [...this.selectedIsolate.samples.entries()].filter(([time, sample]) => {
186        // Remove samples that does not have detailed per-zone data.
187        return sample.zones !== undefined;
188      }),
189      (time, sample, index) => {
190        return ((index + ': ').padStart(6, '\u00A0') +
191          formatSeconds(time).padStart(8, '\u00A0') + ' ' +
192          formatBytes(sample.allocated).padStart(12, '\u00A0'));
193      },
194      this.selectedIsolate.peakUsageTime);
195
196    this.timeStartSelect.value = this.selectedIsolate.start;
197    this.timeEndSelect.value = this.selectedIsolate.end;
198
199    this.populateCategories();
200    this.notifySelectionChanged();
201  }
202
203  notifySelectionChanged(e) {
204    if (!this.selection.isolate) return;
205
206    this.selection.data_view = this.dataViewSelect.value;
207    this.selection.data_kind = this.dataKindSelect.value;
208    this.selection.categories = Object.create(null);
209    this.selection.zones = new Map();
210    this.$('#categories').style.display = 'none';
211    for (let category of CATEGORIES.keys()) {
212      const selected = this.selectedInCategory(category);
213      if (selected.length > 0) this.selection.categories[category] = selected;
214      for (const zone_name of selected) {
215        this.selection.zones.set(zone_name, category);
216      }
217    }
218    this.$('#categories').style.display = 'block';
219    this.selection.category_names = CATEGORY_NAMES;
220    this.selection.show_totals = this.showTotalsSelect.checked;
221    this.selection.time = Number(this.memoryUsageSampleSelect.value);
222    this.selection.timeStart = Number(this.timeStartSelect.value);
223    this.selection.timeEnd = Number(this.timeEndSelect.value);
224    this.updatePercentagesInCategory();
225    this.updatePercentagesInZones();
226    this.dispatchEvent(new CustomEvent(
227        'change', {bubbles: true, composed: true, detail: this.selection}));
228  }
229
230  updatePercentagesInCategory() {
231    const overalls = Object.create(null);
232    let overall = 0;
233    // Reset all categories.
234    this.selection.category_names.forEach((_, category) => {
235      overalls[category] = 0;
236    });
237    // Only update categories that have selections.
238    Object.entries(this.selection.categories).forEach(([category, value]) => {
239      overalls[category] =
240          Object.values(value).reduce(
241              (accu, current) => {
242                  const zone_data = this.selectedData.zones.get(current);
243                  return zone_data === undefined ? accu
244                                                 : accu + zone_data.allocated;
245              }, 0) /
246          KB;
247      overall += overalls[category];
248    });
249    Object.entries(overalls).forEach(([category, category_overall]) => {
250      let percents = category_overall / overall * 100;
251      this.$(`#${category}PercentContent`).innerHTML =
252          `${percents.toFixed(1)}%`;
253      this.$('#' + category + 'PercentBackground').style.left = percents + '%';
254    });
255  }
256
257  updatePercentagesInZones() {
258    const selected_data = this.selectedData;
259    const zones_data = selected_data.zones;
260    const total_allocated = selected_data.allocated;
261    this.querySelectorAll('.zonesSelectBox  input').forEach(checkbox => {
262      const zone_name = checkbox.value;
263      const zone_data = zones_data.get(zone_name);
264      const zone_allocated = zone_data === undefined ? 0 : zone_data.allocated;
265      const percents = zone_allocated / total_allocated;
266      const percent_div = checkbox.parentNode.querySelector('.percentBackground');
267      percent_div.style.left = (percents * 100) + '%';
268      checkbox.parentNode.style.display = 'block';
269    });
270  }
271
272  selectedInCategory(category) {
273    let tmp = [];
274    this.querySelectorAll('input[name=' + category + 'Checkbox]:checked')
275        .forEach(checkbox => tmp.push(checkbox.value));
276    return tmp;
277  }
278
279  createOption(value, text) {
280    const option = document.createElement('option');
281    option.value = value;
282    option.text = text;
283    return option;
284  }
285
286  populateSelect(id, iterable, labelFn = null, autoselect = null) {
287    if (labelFn == null) labelFn = e => e;
288    let index = 0;
289    for (let [key, value] of iterable) {
290      index++;
291      const label = labelFn(key, value, index);
292      const option = this.createOption(key, label);
293      if (autoselect === key) {
294        option.selected = 'selected';
295      }
296      this.$(id).appendChild(option);
297    }
298  }
299
300  clearCategories() {
301    for (const category of CATEGORIES.keys()) {
302      let f = this.$('#' + category + 'Content');
303      while (f.firstChild) {
304        f.removeChild(f.firstChild);
305      }
306    }
307  }
308
309  populateCategories() {
310    this.clearCategories();
311    const categories = Object.create(null);
312    for (let cat of CATEGORIES.keys()) {
313      categories[cat] = [];
314    }
315
316    for (const [zone_name, zone_stats] of this.selectedIsolate.zones) {
317      const category = categoryByZoneName(zone_name);
318      categories[category].push(zone_name);
319    }
320    for (let category of Object.keys(categories)) {
321      categories[category].sort();
322      for (let zone_name of categories[category]) {
323        this.$('#' + category + 'Content')
324            .appendChild(this.createCheckBox(zone_name, category));
325      }
326    }
327  }
328
329  unselectCategory(category) {
330    this.querySelectorAll('input[name=' + category + 'Checkbox]')
331        .forEach(checkbox => checkbox.checked = false);
332    this.notifySelectionChanged();
333  }
334
335  selectCategory(category) {
336    this.querySelectorAll('input[name=' + category + 'Checkbox]')
337        .forEach(checkbox => checkbox.checked = true);
338    this.notifySelectionChanged();
339  }
340
341  createCheckBox(instance_type, category) {
342    const div = document.createElement('div');
343    div.classList.add('zonesSelectBox');
344    div.style.width = "200px";
345    const input = document.createElement('input');
346    div.appendChild(input);
347    input.type = 'checkbox';
348    input.name = category + 'Checkbox';
349    input.checked = 'checked';
350    input.id = instance_type + 'Checkbox';
351    input.instance_type = instance_type;
352    input.value = instance_type;
353    input.addEventListener('change', e => this.notifySelectionChanged(e));
354    const label = document.createElement('label');
355    div.appendChild(label);
356    label.innerText = instance_type;
357    label.htmlFor = instance_type + 'Checkbox';
358    const percentDiv = document.createElement('div');
359    percentDiv.className = 'percentBackground';
360    div.appendChild(percentDiv);
361    return div;
362  }
363});
364