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