// Copyright 2021 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. import {MB} from '../js/helper.mjs'; import {DOM} from '../js/web-api-helper.mjs'; import {getColorFromSpaceName, kSpaceNames} from './space-categories.mjs'; class TrendLineHelper { static re_gc_count = /(?<=(Before|After) GC:)\d+(?=,)/; static re_allocated = /allocated/; static re_space_name = /^[a-z_]+_space/; static snapshotHeaderToXLabel(header) { const gc_count = this.re_gc_count.exec(header)[0]; const alpha = header[0]; return alpha + gc_count; } static getLineSymbolFromTrendLineName(trend_line_name) { const is_allocated_line = this.re_allocated.test(trend_line_name); if (is_allocated_line) { return 'emptyTriangle'; } return 'emptyCircle'; } static getSizeTrendLineName(space_name) { return space_name + ' size'; } static getAllocatedTrendSizeName(space_name) { return space_name + ' allocated'; } static getSpaceNameFromTrendLineName(trend_line_name) { const space_name = this.re_space_name.exec(trend_line_name)[0]; return space_name; } } DOM.defineCustomElement('heap-size-trend-viewer', (templateText) => class HeapSizeTrendViewer extends HTMLElement { constructor() { super(); const shadowRoot = this.attachShadow({mode: 'open'}); shadowRoot.innerHTML = templateText; this.chart = echarts.init(this.$('#chart'), null, { renderer: 'canvas', }); this.chart.getZr().on('click', 'series.line', (params) => { const pointInPixel = [params.offsetX, params.offsetY]; const pointInGrid = this.chart.convertFromPixel({seriesIndex: 0}, pointInPixel); const xIndex = pointInGrid[0]; this.dispatchEvent(new CustomEvent('change', { bubbles: true, composed: true, detail: xIndex, })); this.setXMarkLine(xIndex); }); this.chartXAxisData = null; this.chartSeriesData = null; this.currentIndex = 0; window.addEventListener('resize', () => { this.chart.resize(); }); } $(id) { return this.shadowRoot.querySelector(id); } set data(value) { this._data = value; this.stateChanged(); } get data() { return this._data; } hide() { this.$('#container').style.display = 'none'; } show() { this.$('#container').style.display = 'block'; } stateChanged() { this.initTrendLineNames(); this.initXAxisDataAndSeries(); this.drawChart(); } initTrendLineNames() { this.trend_line_names = []; for (const space_name of kSpaceNames) { this.trend_line_names.push( TrendLineHelper.getSizeTrendLineName(space_name)); this.trend_line_names.push( TrendLineHelper.getAllocatedTrendSizeName(space_name)); } } // X axis represent the moment before or after nth GC : [B1,A1,...Bn,An]. initXAxisDataAndSeries() { this.chartXAxisData = []; this.chartSeriesData = []; let trend_line_name_data_dict = {}; for (const trend_line_name of this.trend_line_names) { trend_line_name_data_dict[trend_line_name] = []; } // Init x axis data and trend line series. for (const snapshot of this.data) { this.chartXAxisData.push( TrendLineHelper.snapshotHeaderToXLabel(snapshot.header)); for (const [space_name, pageinfos] of Object.entries(snapshot.data)) { const size_trend_line_name = TrendLineHelper.getSizeTrendLineName(space_name); const allocated_trend_line_name = TrendLineHelper.getAllocatedTrendSizeName(space_name); let size_sum = 0; let allocated_sum = 0; for (const pageinfo of pageinfos) { size_sum += pageinfo[2] - pageinfo[1]; allocated_sum += pageinfo[3]; } trend_line_name_data_dict[size_trend_line_name].push(size_sum); trend_line_name_data_dict[allocated_trend_line_name].push( allocated_sum); } } // Init mark line series as the first series const markline_series = { name: 'mark-line', type: 'line', markLine: { silent: true, symbol: 'none', label: { show: false, }, lineStyle: { color: '#333', }, data: [ { xAxis: 0, }, ], }, }; this.chartSeriesData.push(markline_series); for (const [trend_line_name, trend_line_data] of Object.entries( trend_line_name_data_dict)) { const color = getColorFromSpaceName( TrendLineHelper.getSpaceNameFromTrendLineName(trend_line_name)); const trend_line_series = { name: trend_line_name, type: 'line', data: trend_line_data, lineStyle: { color: color, }, itemStyle: { color: color, }, symbol: TrendLineHelper.getLineSymbolFromTrendLineName(trend_line_name), symbolSize: 8, }; this.chartSeriesData.push(trend_line_series); } } setXMarkLine(index) { if (index < 0 || index >= this.data.length) { console.error('Invalid index:', index); return; } // Set the mark-line series this.chartSeriesData[0].markLine.data[0].xAxis = index; this.chart.setOption({ series: this.chartSeriesData, }); this.currentIndex = index; } drawChart() { const option = { dataZoom: [ { type: 'inside', filterMode: 'weakFilter', }, { type: 'slider', filterMode: 'weakFilter', labelFormatter: '', }, ], title: { text: 'Size Trend', left: 'center', }, tooltip: { trigger: 'axis', position(point, params, dom, rect, size) { let ret_x = point[0] + 10; if (point[0] > size.viewSize[0] * 0.7) { ret_x = point[0] - dom.clientWidth - 10; } return [ret_x, '85%']; }, formatter(params) { const colorSpan = (color) => ''; let result = '
' + params[0].axisValue + '
'; params.forEach((item) => { const xx = '' + colorSpan(item.color) + ' ' + item.seriesName + ': ' + (item.data / MB).toFixed(2) + 'MB' + '
'; result += xx; }); return result; }, }, legend: { data: this.trend_line_names, top: '6%', type: 'scroll', }, xAxis: { minInterval: 1, type: 'category', boundaryGap: false, data: this.chartXAxisData, }, yAxis: { type: 'value', axisLabel: { formatter(value, index) { return (value / MB).toFixed(3) + 'MB'; }, }, }, series: this.chartSeriesData, }; this.show(); this.chart.resize(); this.chart.setOption(option); } });