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}