• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/*
2 * Copyright (C) 2024 Huawei Device Co., Ltd.
3 * Licensed under the Apache License, Version 2.0 (the "License");
4 * you may not use this file except in compliance with the License.
5 * You may obtain a copy of the License at
6 *
7 *     http://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software
10 * distributed under the License is distributed on an "AS IS" BASIS,
11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 * See the License for the specific language governing permissions and
13 * limitations under the License.
14 */
15import {BaseElement} from '../../base-ui/BaseElement.js';
16import {memoryDottingHtml} from './MemoryDottingHtml.html.js';
17
18export class MemoryDotting extends BaseElement {
19  constructor() {
20    super();
21    this.memoryDataPoints = [];
22    this.sourceData = [];
23  }
24
25  set data(datas) {
26    let context = this.canvas.getContext('2d', {alpha: true});
27    context.clearRect(0, 0, this.canvas.width, this.canvas.height);
28    let firstTimestamp = datas[0].timestamp;
29    let processedData = datas.map(item => ({
30        timestamp: item.timestamp - firstTimestamp,
31        rss: this.convertToMB(item.rss),
32        stage: item.stage,
33        parentStage: item.parentStage
34    }));
35    this.sourceData = processedData;
36    this.drawChart(context, processedData);
37  }
38  convertToMB(value) {
39	return value / (1024 * 1024);
40  };
41
42  initElements() {
43    this.canvas = this.shadowRoot.querySelector('#canvas');
44    this.toolTip = this.shadowRoot.querySelector('#memory-tool-tip');
45  }
46
47  initHtml() {
48    return memoryDottingHtml;
49  }
50
51  connectedCallback() {
52    super.connectedCallback();
53    new ResizeObserver(() => {
54      this.parentElement.style.overflow = 'hidden';
55    }).observe(this.parentElement);
56  }
57
58  disconnectedCallback() {
59    super.disconnectedCallback();
60  }
61
62  showTooltip(event, value, key, time, stage, parentStage) {
63    const tooltipRect = this.toolTip.getBoundingClientRect();
64    let tooltipLeft = event.clientX - (tooltipRect.width + 100);
65    let tooltipTop = event.clientY - tooltipRect.height - 10;
66    if (tooltipLeft + tooltipRect.width > window.innerWidth) {
67      tooltipLeft = event.clientX - tooltipRect.width - 5;
68    } else if (tooltipLeft < 0) {
69      tooltipLeft = event.clientX + 5;
70    }
71    if (tooltipTop < 0) {
72      tooltipTop = event.clientY + 5;
73    }
74    this.toolTip.style.visibility = 'visible';
75    this.toolTip.innerHTML = `X: ${time}<br>Y: ${value}(MB)<br>Type: ${key}<br>stage: ${stage}<br>parentStage: ${parentStage}`;
76    this.toolTip.style.left = tooltipLeft + 'px';
77    this.toolTip.style.top = tooltipTop + 'px';
78  }
79
80  hideTooltip() {
81    this.toolTip.style.visibility = 'hidden';
82  }
83
84  isInsideCircle(mx, my, cx, cy, radius = 5) {
85    return Math.sqrt(Math.pow((mx - cx), 2) + Math.pow((my - cy), 2)) <= radius;
86  }
87
88  static isLocalMaxOrMin(curr, prev, next, threshold) {
89    const rssDiffPrev = Math.abs(curr.rss - prev.rss);
90    const rssDiffNext = Math.abs(curr.rss - next.rss);
91    return (
92      (curr.rss > prev.rss && curr.rss > next.rss && rssDiffPrev >= threshold &&
93      rssDiffNext >= threshold) || (curr.rss < prev.rss && curr.rss < next.rss && rssDiffPrev >=
94      threshold && rssDiffNext >= threshold)
95    );
96  }
97
98  static isTurningPoint(curr, prev, next, threshold) {
99    const rssDiffPrev = Math.abs(curr.rss - prev.rss);
100    const rssDiffNext = Math.abs(curr.rss - next.rss);
101    return (
102      (prev.rss < curr.rss && curr.rss > next.rss && rssDiffPrev >= threshold && rssDiffNext >= threshold) ||
103      (prev.rss > curr.rss && curr.rss < next.rss && rssDiffPrev >= threshold && rssDiffNext >= threshold)
104    );
105  }
106
107  static filterKeyPoints(data, threshold = 500) {
108    if (data.length <= 2) {
109      return data;
110    }
111    let filteredData = [data[0]];
112    for (let i = 1; i < data.length - 1; i++) {
113      const prev = data[i - 1];
114      const curr = data[i];
115      const next = data[i + 1];
116      if (MemoryDotting.isLocalMaxOrMin(curr, prev, next, threshold) ||
117      MemoryDotting.isTurningPoint(curr, prev, next, threshold)) {
118        filteredData.push(curr);
119      }
120  }
121  filteredData.push(data[data.length - 1]);
122  return filteredData;
123  }
124
125
126  getMaxValue() {
127    let rssValues = this.sourceData.map(item => item.rss);
128    let maxValue = Math.max(...rssValues);
129    return maxValue;
130  }
131
132  getMinValue() {
133    let rssValues = this.sourceData.map(item => item.rss);
134    let minValue = Math.min(...rssValues);
135    return minValue;
136  }
137
138  drawChart(ctx, data) {
139    if (!ctx) {
140      return;
141    }
142
143    const {width, height, padding} = this.getCanvasDimensions();
144    ctx.clearRect(0, 0, width, height);
145    const {timestamps, rssValues} = this.extractData(data);
146    const {maxVal, minVal} = this.getMinMaxValues();
147    this.drawGridLines(ctx, width, height, padding);
148    this.drawAxis(ctx, width, height, padding, maxVal, minVal, timestamps);
149    this.addEventListeners(ctx, data);
150    this.drawDataLines(ctx, width, height, padding, maxVal, minVal, timestamps, rssValues, data);
151    this.addLegend(ctx, width, height, padding);
152  }
153
154  getCanvasDimensions() {
155    return {
156      width: this.canvas.clientWidth,
157      height: this.canvas.clientHeight,
158      padding: 50
159    };
160  }
161
162  extractData(data) {
163    return {
164      timestamps: data.map(item => item.timestamp),
165      rssValues: data.map(item => item.rss)
166    };
167  }
168
169  getMinMaxValues() {
170    return {
171      maxVal: this.getMaxValue(),
172      minVal: this.getMinValue()
173    };
174  }
175
176  drawGridLines(ctx, width, height, padding) {
177    ctx.strokeStyle = '#ccc';
178    ctx.lineWidth = 1;
179    for (let x = padding; x < width - padding; x += (width - 2 * padding) / 5) {
180      ctx.beginPath();
181      ctx.moveTo(x, padding);
182      ctx.lineTo(x, height - padding);
183      ctx.stroke();
184    }
185  }
186
187  drawAxis(ctx, width, height, padding, maxVal, minVal, timestamps) {
188    const numTicks = 5;
189    const tickStep = (maxVal - minVal) / numTicks;
190    for (let i = 0; i <= numTicks; i++) {
191      const y = height - padding - (height - 2 * padding) * (i * tickStep) / (maxVal - minVal);
192      ctx.beginPath();
193      ctx.moveTo(padding, y);
194      ctx.lineTo(width - padding, y);
195      ctx.stroke();
196    }
197
198    ctx.beginPath();
199    ctx.moveTo(padding, height - padding);
200    ctx.lineTo(width - padding, height - padding);
201    ctx.strokeStyle = '#000';
202    ctx.lineWidth = 2;
203    ctx.stroke();
204
205    ctx.fillStyle = '#666';
206	let firstTimestamp = timestamps[0];
207	let lastTimestamp = timestamps[timestamps.length - 1];
208	let timeInterval = (lastTimestamp - firstTimestamp) / 8;
209
210	for (let i = 0; i <= 8; i++) {
211		let timestamp = firstTimestamp + i * timeInterval;
212		let x = padding + ((timestamp - firstTimestamp) / (lastTimestamp - firstTimestamp)) * (width - 2 * padding);
213		this.drawVerticalText(ctx, this.formatTimestamp(timestamp), x, height - padding + 20, -90);
214	}
215
216    ctx.beginPath();
217    ctx.moveTo(padding, padding);
218    ctx.lineTo(padding, height - padding);
219    ctx.strokeStyle = '#000';
220    ctx.lineWidth = 2;
221    ctx.stroke();
222
223    for (let i = 0; i <= numTicks; i++) {
224      const y = height - padding - (height - 2 * padding) * (i * tickStep) / (maxVal - minVal);
225      ctx.fillText((minVal + i * tickStep).toFixed(0), padding - 20, y);
226    }
227  }
228
229  addEventListeners(ctx, data) {
230    this.canvas.addEventListener('mousemove', (event) => {
231      const rect = this.canvas.getBoundingClientRect();
232      const mouseX = event.clientX - rect.left;
233      const mouseY = event.clientY - rect.top;
234      for (const dataPoint of this.memoryDataPoints) {
235        if (this.isInsideCircle(mouseX, mouseY, dataPoint.x, dataPoint.y)) {
236          this.showTooltip(event, dataPoint.value, dataPoint.key, dataPoint.time, dataPoint.stage, dataPoint.parentStage);
237          return;
238        }
239      }
240      this.hideTooltip();
241    });
242    this.canvas.addEventListener('mouseout', () => {
243      this.hideTooltip();
244    });
245  }
246
247  drawDataLines(ctx, width, height, padding, maxVal, minVal, timestamps, rssValues, data) {
248    const drawLine = (ctx, values, color, label) => {
249      ctx.lineWidth = 1;
250      ctx.strokeStyle = color;
251      ctx.beginPath();
252      this.drawLine(ctx, timestamps, values, padding, width, height, minVal, maxVal, label, data);
253      ctx.stroke();
254    };
255    drawLine(ctx, rssValues, 'red', 'RSS');
256  }
257
258  addLegend(ctx, width, height, padding) {
259    ctx.fillStyle = '#000';
260    ctx.fillText('RSS', width - 100, padding + 20);
261    ctx.fillStyle = 'red';
262    ctx.fillRect(width - 120, padding + 15, 10, 10);
263  }
264
265  drawLine(ctx, timestamps, values, padding, width, height, minVal, maxVal, type, data) {
266    ctx.beginPath();
267    let pointX = padding;
268    let pointY = height - padding - (values[0] - minVal) * (height - 2 * padding) / (maxVal - minVal);
269    for (let i = 1; i < values.length; i++) {
270      ctx.moveTo(pointX, pointY);
271      if (i === 1) {
272        ctx.arc(pointX, pointY, 4, 0, Math.PI * 2);
273        this.memoryDataPoints.push({
274          x: pointX, y: pointY, value: values[0], key: type,
275          time: this.formatTimestampTitle(timestamps[0]), stage: data[0].stage, parentStage: data[0].parentStage
276        });
277        ctx.fillStyle = ctx.strokeStyle;
278        ctx.fill();
279      }
280      const x = padding + ((timestamps[i] - timestamps[0]) / (timestamps[timestamps.length - 1] - timestamps[0])) * (width - 2 * padding);
281      const y = height - padding - (values[i] - minVal) * (height - 2 * padding) / (maxVal - minVal);
282      ctx.lineTo(x, y);
283      ctx.arc(x, y, 4, 0, Math.PI * 2);
284      this.memoryDataPoints.push({
285        x: x, y: y, value: values[i], key: type,
286        time: this.formatTimestampTitle(timestamps[i]), stage: data[i].stage, parentStage: data[i].parentStage
287      });
288      ctx.fillStyle = ctx.strokeStyle;
289      ctx.fill();
290      pointX = x;
291      pointY = y;
292    }
293    ctx.closePath();
294  }
295
296  formatTimestamp(timestamp) {
297	return timestamp.toFixed(2) + 'ms';
298  }
299
300  formatTimestampTitle(timestamp) {
301	return timestamp + '(ms)';
302}
303
304  drawVerticalText(ctx, text, x, y, rotationAngle) {
305    ctx.save();
306    ctx.textBaseline = 'middle';
307    ctx.textAlign = 'center';
308    ctx.translate(x, y);
309    ctx.rotate(rotationAngle * Math.PI / 180);
310    ctx.fillText(text, 0, 0);
311    ctx.restore();
312  }
313}
314
315if (!customElements.get('memory-dotting')) {
316  customElements.define('memory-dotting', MemoryDotting);
317}