blob: e9d7280508a02edc9d48bfbfab81c4158fd69933 [file] [log] [blame]
// Copyright 2020 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';
import {categoryByZoneName} from './categories.js';
import {
VIEW_TOTALS,
VIEW_BY_ZONE_NAME,
VIEW_BY_ZONE_CATEGORY,
KIND_ALLOCATED_MEMORY,
KIND_USED_MEMORY,
KIND_FREED_MEMORY,
} from './details-selection.js';
defineCustomElement('global-timeline', (templateText) =>
class GlobalTimeline extends HTMLElement {
constructor() {
super();
const shadowRoot = this.attachShadow({mode: 'open'});
shadowRoot.innerHTML = templateText;
}
$(id) {
return this.shadowRoot.querySelector(id);
}
set data(value) {
this._data = value;
this.stateChanged();
}
get data() {
return this._data;
}
set selection(value) {
this._selection = value;
this.stateChanged();
}
get selection() {
return this._selection;
}
isValid() {
return this.data && this.selection;
}
hide() {
this.$('#container').style.display = 'none';
}
show() {
this.$('#container').style.display = 'block';
}
stateChanged() {
if (this.isValid()) {
const isolate_data = this.data[this.selection.isolate];
const peakAllocatedMemory = isolate_data.peakAllocatedMemory;
this.$('#peak-memory-label').innerText = formatBytes(peakAllocatedMemory);
this.drawChart();
} else {
this.hide();
}
}
getZoneLabels(zone_names) {
switch (this.selection.data_kind) {
case KIND_ALLOCATED_MEMORY:
return zone_names.map(name => {
return {label: name + " (allocated)", type: 'number'};
});
case KIND_USED_MEMORY:
return zone_names.map(name => {
return {label: name + " (used)", type: 'number'};
});
case KIND_FREED_MEMORY:
return zone_names.map(name => {
return {label: name + " (freed)", type: 'number'};
});
default:
// Don't show detailed per-zone information.
return [];
}
}
getTotalsData() {
const isolate_data = this.data[this.selection.isolate];
const labels = [
{ label: "Time", type: "number" },
{ label: "Total allocated", type: "number" },
{ label: "Total used", type: "number" },
{ label: "Total freed", type: "number" },
];
const chart_data = [labels];
const timeStart = this.selection.timeStart;
const timeEnd = this.selection.timeEnd;
const filter_entries = timeStart > 0 || timeEnd > 0;
for (const [time, zone_data] of isolate_data.samples) {
if (filter_entries && (time < timeStart || time > timeEnd)) continue;
const data = [];
data.push(time * kMillis2Seconds);
data.push(zone_data.allocated / KB);
data.push(zone_data.used / KB);
data.push(zone_data.freed / KB);
chart_data.push(data);
}
return chart_data;
}
getZoneData() {
const isolate_data = this.data[this.selection.isolate];
const zone_names = isolate_data.sorted_zone_names;
const selected_zones = this.selection.zones;
const data_kind = this.selection.data_kind;
const show_totals = this.selection.show_totals;
const zones_labels = this.getZoneLabels(zone_names);
const totals_labels = show_totals
? [
{ label: "Total allocated", type: "number" },
{ label: "Total used", type: "number" },
{ label: "Total freed", type: "number" },
]
: [];
const labels = [
{ label: "Time", type: "number" },
...totals_labels,
...zones_labels,
];
const chart_data = [labels];
const timeStart = this.selection.timeStart;
const timeEnd = this.selection.timeEnd;
const filter_entries = timeStart > 0 || timeEnd > 0;
for (const [time, zone_data] of isolate_data.samples) {
if (filter_entries && (time < timeStart || time > timeEnd)) continue;
const active_zone_stats = Object.create(null);
if (zone_data.zones !== undefined) {
for (const [zone_name, zone_stats] of zone_data.zones) {
if (!selected_zones.has(zone_name)) continue; // Not selected, skip.
const current_stats = active_zone_stats[zone_name];
if (current_stats === undefined) {
active_zone_stats[zone_name] =
{ allocated: zone_stats.allocated,
used: zone_stats.used,
freed: zone_stats.freed,
};
} else {
// We've got two zones with the same name.
console.log("=== Duplicate zone names: " + zone_name);
// Sum stats.
current_stats.allocated += zone_stats.allocated;
current_stats.used += zone_stats.used;
current_stats.freed += zone_stats.freed;
}
}
}
const data = [];
data.push(time * kMillis2Seconds);
if (show_totals) {
data.push(zone_data.allocated / KB);
data.push(zone_data.used / KB);
data.push(zone_data.freed / KB);
}
zone_names.forEach(zone => {
const sample = active_zone_stats[zone];
let value = null;
if (sample !== undefined) {
if (data_kind == KIND_ALLOCATED_MEMORY) {
value = sample.allocated / KB;
} else if (data_kind == KIND_FREED_MEMORY) {
value = sample.freed / KB;
} else {
// KIND_USED_MEMORY
value = sample.used / KB;
}
}
data.push(value);
});
chart_data.push(data);
}
return chart_data;
}
getCategoryData() {
const isolate_data = this.data[this.selection.isolate];
const categories = Object.keys(this.selection.categories);
const categories_names =
categories.map(k => this.selection.category_names.get(k));
const selected_zones = this.selection.zones;
const data_kind = this.selection.data_kind;
const show_totals = this.selection.show_totals;
const categories_labels = this.getZoneLabels(categories_names);
const totals_labels = show_totals
? [
{ label: "Total allocated", type: "number" },
{ label: "Total used", type: "number" },
{ label: "Total freed", type: "number" },
]
: [];
const labels = [
{ label: "Time", type: "number" },
...totals_labels,
...categories_labels,
];
const chart_data = [labels];
const timeStart = this.selection.timeStart;
const timeEnd = this.selection.timeEnd;
const filter_entries = timeStart > 0 || timeEnd > 0;
for (const [time, zone_data] of isolate_data.samples) {
if (filter_entries && (time < timeStart || time > timeEnd)) continue;
const active_category_stats = Object.create(null);
if (zone_data.zones !== undefined) {
for (const [zone_name, zone_stats] of zone_data.zones) {
const category = selected_zones.get(zone_name);
if (category === undefined) continue; // Zone was not selected.
const current_stats = active_category_stats[category];
if (current_stats === undefined) {
active_category_stats[category] =
{ allocated: zone_stats.allocated,
used: zone_stats.used,
freed: zone_stats.freed,
};
} else {
// Sum stats.
current_stats.allocated += zone_stats.allocated;
current_stats.used += zone_stats.used;
current_stats.freed += zone_stats.freed;
}
}
}
const data = [];
data.push(time * kMillis2Seconds);
if (show_totals) {
data.push(zone_data.allocated / KB);
data.push(zone_data.used / KB);
data.push(zone_data.freed / KB);
}
categories.forEach(category => {
const sample = active_category_stats[category];
let value = null;
if (sample !== undefined) {
if (data_kind == KIND_ALLOCATED_MEMORY) {
value = sample.allocated / KB;
} else if (data_kind == KIND_FREED_MEMORY) {
value = sample.freed / KB;
} else {
// KIND_USED_MEMORY
value = sample.used / KB;
}
}
data.push(value);
});
chart_data.push(data);
}
return chart_data;
}
getChartData() {
switch (this.selection.data_view) {
case VIEW_BY_ZONE_NAME:
return this.getZoneData();
case VIEW_BY_ZONE_CATEGORY:
return this.getCategoryData();
case VIEW_TOTALS:
default:
return this.getTotalsData();
}
}
getChartOptions() {
const options = {
isStacked: true,
interpolateNulls: true,
hAxis: {
format: '###.##s',
title: 'Time [s]',
},
vAxis: {
format: '#,###KB',
title: 'Memory consumption [KBytes]'
},
chartArea: {left:100, width: '85%', height: '70%'},
legend: {position: 'top', maxLines: '1'},
pointsVisible: true,
pointSize: 3,
explorer: {},
};
// Overlay total allocated/used points on top of the graph.
const series = {}
if (this.selection.data_view == VIEW_TOTALS) {
series[0] = {type: 'line', color: "red"};
series[1] = {type: 'line', color: "blue"};
series[2] = {type: 'line', color: "orange"};
} else if (this.selection.show_totals) {
series[0] = {type: 'line', color: "red", lineDashStyle: [13, 13]};
series[1] = {type: 'line', color: "blue", lineDashStyle: [13, 13]};
series[2] = {type: 'line', color: "orange", lineDashStyle: [13, 13]};
}
return Object.assign(options, {series: series});
}
drawChart() {
console.assert(this.data, 'invalid data');
console.assert(this.selection, 'invalid selection');
const chart_data = this.getChartData();
const data = google.visualization.arrayToDataTable(chart_data);
const options = this.getChartOptions();
const chart = new google.visualization.AreaChart(this.$('#chart'));
this.show();
chart.draw(data, google.charts.Line.convertOptions(options));
}
});