• 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 {categoryByZoneName} from './categories.js';
8
9import {
10  VIEW_TOTALS,
11  VIEW_BY_ZONE_NAME,
12  VIEW_BY_ZONE_CATEGORY,
13
14  KIND_ALLOCATED_MEMORY,
15  KIND_USED_MEMORY,
16  KIND_FREED_MEMORY,
17} from './details-selection.js';
18
19defineCustomElement('global-timeline', (templateText) =>
20 class GlobalTimeline extends HTMLElement {
21  constructor() {
22    super();
23    const shadowRoot = this.attachShadow({mode: 'open'});
24    shadowRoot.innerHTML = templateText;
25  }
26
27  $(id) {
28    return this.shadowRoot.querySelector(id);
29  }
30
31  set data(value) {
32    this._data = value;
33    this.stateChanged();
34  }
35
36  get data() {
37    return this._data;
38  }
39
40  set selection(value) {
41    this._selection = value;
42    this.stateChanged();
43  }
44
45  get selection() {
46    return this._selection;
47  }
48
49  isValid() {
50    return this.data && this.selection;
51  }
52
53  hide() {
54    this.$('#container').style.display = 'none';
55  }
56
57  show() {
58    this.$('#container').style.display = 'block';
59  }
60
61  stateChanged() {
62    if (this.isValid()) {
63      const isolate_data = this.data[this.selection.isolate];
64      const peakAllocatedMemory = isolate_data.peakAllocatedMemory;
65      this.$('#peak-memory-label').innerText = formatBytes(peakAllocatedMemory);
66      this.drawChart();
67    } else {
68      this.hide();
69    }
70  }
71
72  getZoneLabels(zone_names) {
73    switch (this.selection.data_kind) {
74      case KIND_ALLOCATED_MEMORY:
75        return zone_names.map(name => {
76          return {label: name + " (allocated)", type: 'number'};
77        });
78
79      case KIND_USED_MEMORY:
80        return zone_names.map(name => {
81          return {label: name + " (used)", type: 'number'};
82        });
83
84        case KIND_FREED_MEMORY:
85          return zone_names.map(name => {
86            return {label: name + " (freed)", type: 'number'};
87          });
88
89        default:
90        // Don't show detailed per-zone information.
91        return [];
92    }
93  }
94
95  getTotalsData() {
96    const isolate_data = this.data[this.selection.isolate];
97    const labels = [
98      { label: "Time", type: "number" },
99      { label: "Total allocated", type: "number" },
100      { label: "Total used", type: "number" },
101      { label: "Total freed", type: "number" },
102    ];
103    const chart_data = [labels];
104
105    const timeStart = this.selection.timeStart;
106    const timeEnd = this.selection.timeEnd;
107    const filter_entries = timeStart > 0 || timeEnd > 0;
108
109    for (const [time, zone_data] of isolate_data.samples) {
110      if (filter_entries && (time < timeStart || time > timeEnd)) continue;
111      const data = [];
112      data.push(time * kMillis2Seconds);
113      data.push(zone_data.allocated / KB);
114      data.push(zone_data.used / KB);
115      data.push(zone_data.freed / KB);
116      chart_data.push(data);
117    }
118    return chart_data;
119  }
120
121  getZoneData() {
122    const isolate_data = this.data[this.selection.isolate];
123    const selected_zones = this.selection.zones;
124    const zone_names = isolate_data.sorted_zone_names.filter(
125        zone_name => selected_zones.has(zone_name));
126    const data_kind = this.selection.data_kind;
127    const show_totals = this.selection.show_totals;
128    const zones_labels = this.getZoneLabels(zone_names);
129
130    const totals_labels = show_totals
131        ? [
132            { label: "Total allocated", type: "number" },
133            { label: "Total used", type: "number" },
134            { label: "Total freed", type: "number" },
135          ]
136        : [];
137
138    const labels = [
139      { label: "Time", type: "number" },
140      ...totals_labels,
141      ...zones_labels,
142    ];
143    const chart_data = [labels];
144
145    const timeStart = this.selection.timeStart;
146    const timeEnd = this.selection.timeEnd;
147    const filter_entries = timeStart > 0 || timeEnd > 0;
148
149    for (const [time, zone_data] of isolate_data.samples) {
150      if (filter_entries && (time < timeStart || time > timeEnd)) continue;
151      const active_zone_stats = Object.create(null);
152      if (zone_data.zones !== undefined) {
153        for (const [zone_name, zone_stats] of zone_data.zones) {
154          if (!selected_zones.has(zone_name)) continue;  // Not selected, skip.
155
156          const current_stats = active_zone_stats[zone_name];
157          if (current_stats === undefined) {
158            active_zone_stats[zone_name] =
159                { allocated: zone_stats.allocated,
160                  used: zone_stats.used,
161                  freed: zone_stats.freed,
162                };
163          } else {
164            // We've got two zones with the same name.
165            console.log("=== Duplicate zone names: " + zone_name);
166            // Sum stats.
167            current_stats.allocated += zone_stats.allocated;
168            current_stats.used += zone_stats.used;
169            current_stats.freed += zone_stats.freed;
170          }
171        }
172      }
173
174      const data = [];
175      data.push(time * kMillis2Seconds);
176      if (show_totals) {
177        data.push(zone_data.allocated / KB);
178        data.push(zone_data.used / KB);
179        data.push(zone_data.freed / KB);
180      }
181
182      zone_names.forEach(zone => {
183        const sample = active_zone_stats[zone];
184        let value = null;
185        if (sample !== undefined) {
186          if (data_kind == KIND_ALLOCATED_MEMORY) {
187            value = sample.allocated / KB;
188          } else if (data_kind == KIND_FREED_MEMORY) {
189            value = sample.freed / KB;
190          } else {
191            // KIND_USED_MEMORY
192            value = sample.used / KB;
193          }
194        }
195        data.push(value);
196      });
197      chart_data.push(data);
198    }
199    return chart_data;
200  }
201
202  getCategoryData() {
203    const isolate_data = this.data[this.selection.isolate];
204    const categories = Object.keys(this.selection.categories);
205    const categories_names =
206        categories.map(k => this.selection.category_names.get(k));
207    const selected_zones = this.selection.zones;
208    const data_kind = this.selection.data_kind;
209    const show_totals = this.selection.show_totals;
210
211    const categories_labels = this.getZoneLabels(categories_names);
212
213    const totals_labels = show_totals
214        ? [
215            { label: "Total allocated", type: "number" },
216            { label: "Total used", type: "number" },
217            { label: "Total freed", type: "number" },
218          ]
219        : [];
220
221    const labels = [
222      { label: "Time", type: "number" },
223      ...totals_labels,
224      ...categories_labels,
225    ];
226    const chart_data = [labels];
227
228    const timeStart = this.selection.timeStart;
229    const timeEnd = this.selection.timeEnd;
230    const filter_entries = timeStart > 0 || timeEnd > 0;
231
232    for (const [time, zone_data] of isolate_data.samples) {
233      if (filter_entries && (time < timeStart || time > timeEnd)) continue;
234      const active_category_stats = Object.create(null);
235      if (zone_data.zones !== undefined) {
236        for (const [zone_name, zone_stats] of zone_data.zones) {
237          const category = selected_zones.get(zone_name);
238          if (category === undefined) continue;  // Zone was not selected.
239
240          const current_stats = active_category_stats[category];
241          if (current_stats === undefined) {
242            active_category_stats[category] =
243                { allocated: zone_stats.allocated,
244                  used: zone_stats.used,
245                  freed: zone_stats.freed,
246                };
247          } else {
248            // Sum stats.
249            current_stats.allocated += zone_stats.allocated;
250            current_stats.used += zone_stats.used;
251            current_stats.freed += zone_stats.freed;
252          }
253        }
254      }
255
256      const data = [];
257      data.push(time * kMillis2Seconds);
258      if (show_totals) {
259        data.push(zone_data.allocated / KB);
260        data.push(zone_data.used / KB);
261        data.push(zone_data.freed / KB);
262      }
263
264      categories.forEach(category => {
265        const sample = active_category_stats[category];
266        let value = null;
267        if (sample !== undefined) {
268          if (data_kind == KIND_ALLOCATED_MEMORY) {
269            value = sample.allocated / KB;
270          } else if (data_kind == KIND_FREED_MEMORY) {
271            value = sample.freed / KB;
272          } else {
273            // KIND_USED_MEMORY
274            value = sample.used / KB;
275          }
276        }
277        data.push(value);
278      });
279      chart_data.push(data);
280    }
281    return chart_data;
282  }
283
284  getChartData() {
285    switch (this.selection.data_view) {
286      case VIEW_BY_ZONE_NAME:
287        return this.getZoneData();
288      case VIEW_BY_ZONE_CATEGORY:
289        return this.getCategoryData();
290      case VIEW_TOTALS:
291      default:
292        return this.getTotalsData();
293      }
294  }
295
296  getChartOptions() {
297    const options = {
298      isStacked: true,
299      interpolateNulls: true,
300      hAxis: {
301        format: '###.##s',
302        title: 'Time [s]',
303      },
304      vAxis: {
305        format: '#,###KB',
306        title: 'Memory consumption [KBytes]'
307      },
308      chartArea: {left:100, width: '85%', height: '70%'},
309      legend: {position: 'top', maxLines: '1'},
310      pointsVisible: true,
311      pointSize: 3,
312      explorer: {},
313    };
314
315    // Overlay total allocated/used points on top of the graph.
316    const series = {}
317    if (this.selection.data_view == VIEW_TOTALS) {
318      series[0] = {type: 'line', color: "red"};
319      series[1] = {type: 'line', color: "blue"};
320      series[2] = {type: 'line', color: "orange"};
321    } else if (this.selection.show_totals) {
322      series[0] = {type: 'line', color: "red", lineDashStyle: [13, 13]};
323      series[1] = {type: 'line', color: "blue", lineDashStyle: [13, 13]};
324      series[2] = {type: 'line', color: "orange", lineDashStyle: [13, 13]};
325    }
326    return Object.assign(options, {series: series});
327  }
328
329  drawChart() {
330    console.assert(this.data, 'invalid data');
331    console.assert(this.selection, 'invalid selection');
332
333    const chart_data = this.getChartData();
334
335    const data = google.visualization.arrayToDataTable(chart_data);
336    const options = this.getChartOptions();
337    const chart = new google.visualization.AreaChart(this.$('#chart'));
338    this.show();
339    chart.draw(data, google.charts.Line.convertOptions(options));
340  }
341});
342