1// Copyright 2021 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 5import {MB} from '../js/helper.mjs'; 6import {DOM} from '../js/web-api-helper.mjs'; 7 8import {getColorFromSpaceName, kSpaceNames} from './space-categories.mjs'; 9 10class TrendLineHelper { 11 static re_gc_count = /(?<=(Before|After) GC:)\d+(?=,)/; 12 static re_allocated = /allocated/; 13 static re_space_name = /^[a-z_]+_space/; 14 15 static snapshotHeaderToXLabel(header) { 16 const gc_count = this.re_gc_count.exec(header)[0]; 17 const alpha = header[0]; 18 return alpha + gc_count; 19 } 20 21 static getLineSymbolFromTrendLineName(trend_line_name) { 22 const is_allocated_line = this.re_allocated.test(trend_line_name); 23 if (is_allocated_line) { 24 return 'emptyTriangle'; 25 } 26 return 'emptyCircle'; 27 } 28 29 static getSizeTrendLineName(space_name) { 30 return space_name + ' size'; 31 } 32 33 static getAllocatedTrendSizeName(space_name) { 34 return space_name + ' allocated'; 35 } 36 37 static getSpaceNameFromTrendLineName(trend_line_name) { 38 const space_name = this.re_space_name.exec(trend_line_name)[0]; 39 return space_name; 40 } 41} 42 43DOM.defineCustomElement('heap-size-trend-viewer', 44 (templateText) => 45 class HeapSizeTrendViewer extends HTMLElement { 46 constructor() { 47 super(); 48 const shadowRoot = this.attachShadow({mode: 'open'}); 49 shadowRoot.innerHTML = templateText; 50 this.chart = echarts.init(this.$('#chart'), null, { 51 renderer: 'canvas', 52 }); 53 this.chart.getZr().on('click', 'series.line', (params) => { 54 const pointInPixel = [params.offsetX, params.offsetY]; 55 const pointInGrid = 56 this.chart.convertFromPixel({seriesIndex: 0}, pointInPixel); 57 const xIndex = pointInGrid[0]; 58 this.dispatchEvent(new CustomEvent('change', { 59 bubbles: true, 60 composed: true, 61 detail: xIndex, 62 })); 63 this.setXMarkLine(xIndex); 64 }); 65 this.chartXAxisData = null; 66 this.chartSeriesData = null; 67 this.currentIndex = 0; 68 window.addEventListener('resize', () => { 69 this.chart.resize(); 70 }); 71 } 72 73 $(id) { 74 return this.shadowRoot.querySelector(id); 75 } 76 77 set data(value) { 78 this._data = value; 79 this.stateChanged(); 80 } 81 82 get data() { 83 return this._data; 84 } 85 86 hide() { 87 this.$('#container').style.display = 'none'; 88 } 89 90 show() { 91 this.$('#container').style.display = 'block'; 92 } 93 94 stateChanged() { 95 this.initTrendLineNames(); 96 this.initXAxisDataAndSeries(); 97 this.drawChart(); 98 } 99 100 initTrendLineNames() { 101 this.trend_line_names = []; 102 for (const space_name of kSpaceNames) { 103 this.trend_line_names.push( 104 TrendLineHelper.getSizeTrendLineName(space_name)); 105 this.trend_line_names.push( 106 TrendLineHelper.getAllocatedTrendSizeName(space_name)); 107 } 108 } 109 110 // X axis represent the moment before or after nth GC : [B1,A1,...Bn,An]. 111 initXAxisDataAndSeries() { 112 this.chartXAxisData = []; 113 this.chartSeriesData = []; 114 let trend_line_name_data_dict = {}; 115 116 for (const trend_line_name of this.trend_line_names) { 117 trend_line_name_data_dict[trend_line_name] = []; 118 } 119 120 // Init x axis data and trend line series. 121 for (const snapshot of this.data) { 122 this.chartXAxisData.push( 123 TrendLineHelper.snapshotHeaderToXLabel(snapshot.header)); 124 for (const [space_name, pageinfos] of Object.entries(snapshot.data)) { 125 const size_trend_line_name = 126 TrendLineHelper.getSizeTrendLineName(space_name); 127 const allocated_trend_line_name = 128 TrendLineHelper.getAllocatedTrendSizeName(space_name); 129 let size_sum = 0; 130 let allocated_sum = 0; 131 for (const pageinfo of pageinfos) { 132 size_sum += pageinfo[2] - pageinfo[1]; 133 allocated_sum += pageinfo[3]; 134 } 135 trend_line_name_data_dict[size_trend_line_name].push(size_sum); 136 trend_line_name_data_dict[allocated_trend_line_name].push( 137 allocated_sum); 138 } 139 } 140 141 // Init mark line series as the first series 142 const markline_series = { 143 name: 'mark-line', 144 type: 'line', 145 146 markLine: { 147 silent: true, 148 symbol: 'none', 149 label: { 150 show: false, 151 }, 152 lineStyle: { 153 color: '#333', 154 }, 155 data: [ 156 { 157 xAxis: 0, 158 }, 159 ], 160 }, 161 }; 162 this.chartSeriesData.push(markline_series); 163 164 for (const [trend_line_name, trend_line_data] of Object.entries( 165 trend_line_name_data_dict)) { 166 const color = getColorFromSpaceName( 167 TrendLineHelper.getSpaceNameFromTrendLineName(trend_line_name)); 168 const trend_line_series = { 169 name: trend_line_name, 170 type: 'line', 171 data: trend_line_data, 172 lineStyle: { 173 color: color, 174 }, 175 itemStyle: { 176 color: color, 177 }, 178 symbol: TrendLineHelper.getLineSymbolFromTrendLineName(trend_line_name), 179 symbolSize: 8, 180 }; 181 this.chartSeriesData.push(trend_line_series); 182 } 183 } 184 185 setXMarkLine(index) { 186 if (index < 0 || index >= this.data.length) { 187 console.error('Invalid index:', index); 188 return; 189 } 190 // Set the mark-line series 191 this.chartSeriesData[0].markLine.data[0].xAxis = index; 192 this.chart.setOption({ 193 series: this.chartSeriesData, 194 }); 195 this.currentIndex = index; 196 } 197 198 drawChart() { 199 const option = { 200 dataZoom: [ 201 { 202 type: 'inside', 203 filterMode: 'weakFilter', 204 }, 205 { 206 type: 'slider', 207 filterMode: 'weakFilter', 208 labelFormatter: '', 209 }, 210 ], 211 title: { 212 text: 'Size Trend', 213 left: 'center', 214 }, 215 tooltip: { 216 trigger: 'axis', 217 position(point, params, dom, rect, size) { 218 let ret_x = point[0] + 10; 219 if (point[0] > size.viewSize[0] * 0.7) { 220 ret_x = point[0] - dom.clientWidth - 10; 221 } 222 return [ret_x, '85%']; 223 }, 224 formatter(params) { 225 const colorSpan = (color) => 226 '<span style="display:inline-block;margin-right:1px;border-radius:5px;width:9px;height:9px;background-color:' + 227 color + '"></span>'; 228 let result = '<p>' + params[0].axisValue + '</p>'; 229 params.forEach((item) => { 230 const xx = '<p style="margin:0;">' + colorSpan(item.color) + ' ' + 231 item.seriesName + ': ' + (item.data / MB).toFixed(2) + 'MB' + 232 '</p>'; 233 result += xx; 234 }); 235 236 return result; 237 }, 238 }, 239 legend: { 240 data: this.trend_line_names, 241 top: '6%', 242 type: 'scroll', 243 }, 244 245 xAxis: { 246 minInterval: 1, 247 type: 'category', 248 boundaryGap: false, 249 data: this.chartXAxisData, 250 }, 251 yAxis: { 252 type: 'value', 253 axisLabel: { 254 formatter(value, index) { 255 return (value / MB).toFixed(3) + 'MB'; 256 }, 257 }, 258 }, 259 260 series: this.chartSeriesData, 261 }; 262 this.show(); 263 this.chart.resize(); 264 this.chart.setOption(option); 265 } 266}); 267