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