| // 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 selected_zones = this.selection.zones; |
| const zone_names = isolate_data.sorted_zone_names.filter( |
| zone_name => selected_zones.has(zone_name)); |
| 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)); |
| } |
| }); |