1/* 2 * Copyright (C) 2022 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 */ 15 16import { BaseElement, element } from "../../base-ui/BaseElement.js"; 17import { NativeHookCallInfo } from "../bean/NativeHook.js"; 18import { ChartMode, ChartStruct, Rect } from "../database/ProcedureWorkerCommon.js"; 19import { SpApplication } from "../SpApplication.js"; 20import { Utils } from "./trace/base/Utils.js"; 21 22const TAG: string = "FrameChart"; 23const scaleHeight = 30; 24const depthHeight = 20; 25const filterPixiel = 2; 26const sideLenght = 8; 27 28@element('tab-framechart') 29export class FrameChart extends BaseElement { 30 private canvas: HTMLCanvasElement | undefined | null; 31 private cavasContext: CanvasRenderingContext2D | undefined | null; 32 private floatHint: HTMLDivElement | undefined | null; 33 34 private rect: Rect = new Rect(0, 0, 0, 0); 35 private _mode = ChartMode.Call; 36 private startX = 0; // canvas start x coord 37 private startY = 0; // canvas start y coord 38 private canvasX = -1; // canvas current x 39 private canvasY = -1; // canvas current y 40 private hintContent = ""; // float hoint inner html content 41 42 private historyList: Array<Array<ChartStruct>> = []; 43 private currentSize = 0; 44 private currentCount = 0; 45 private currentData: Array<ChartStruct> = []; 46 private xPoint = 0; // x in rect 47 private isFocusing = false; 48 private canvasScrollTop = 0; 49 private _maxDepth = 0; 50 private chartClickListenerList: Array<Function> = []; 51 private isUpdateCanvas = false; 52 53 static get observedAttributes() { 54 return [] 55 } 56 57 /** 58 * set chart mode 59 * @param mode chart format for data mode 60 */ 61 set mode(mode: ChartMode) { 62 this._mode = mode; 63 } 64 65 set data(val: Array<ChartStruct> | any) { 66 this.historyList = []; 67 ChartStruct.lastSelectFuncStruct = undefined; 68 this.currentData = val; 69 this.resetTrans(); 70 this.caldrawArgs(); 71 for (let callback of this.chartClickListenerList) { 72 callback(true); 73 } 74 75 } 76 77 set tabPaneScrollTop(scrollTop: number) { 78 this.canvasScrollTop = scrollTop; 79 this.hideFloatHint(); 80 } 81 82 /** 83 * add callback of chart click 84 * @param callback function of chart click 85 */ 86 public addChartClickListener(callback: Function) { 87 if (this.chartClickListenerList.indexOf(callback) < 0) { 88 this.chartClickListenerList.push(callback); 89 } 90 } 91 92 /** 93 * remove callback of chart click 94 * @param callback function of chart click 95 */ 96 public removeChartClickListener(callback: Function) { 97 let index = this.chartClickListenerList.indexOf(callback); 98 if (index > -1) { 99 this.chartClickListenerList.splice(index, 1); 100 } 101 } 102 103 /** 104 * cal total count size and max Depth 105 */ 106 private caldrawArgs(): void { 107 this.currentCount = 0; 108 this.currentSize = 0; 109 this._maxDepth = 0; 110 for (let rootNode of this.currentData!) { 111 this.currentCount += rootNode.count; 112 this.currentSize += rootNode.size; 113 let depth = 0; 114 this.calMaxDepth(rootNode, depth); 115 } 116 this.rect.width = this.canvas!.width 117 this.rect.height = (this._maxDepth + 1) * 20 + scaleHeight; // 20px/depth and 30 is scale height 118 this.canvas!.style.height = this.rect!.height + "px"; 119 this.canvas!.height = Math.ceil(this.rect!.height); 120 } 121 122 /** 123 * cal max Depth 124 * @param node every child node 125 * @param depth current depth 126 */ 127 private calMaxDepth(node: ChartStruct, depth: number): void { 128 node.depth = depth; 129 depth++; 130 if (node.children && node.children.length > 0) { 131 for (let children of node.children) { 132 this.calMaxDepth(children, depth); 133 } 134 } else { 135 this._maxDepth = Math.max(depth, this._maxDepth); 136 } 137 } 138 139 /** 140 * calculate Data and draw chart 141 */ 142 async calculateChartData() { 143 this.clearCanvas(); 144 this.cavasContext?.beginPath(); 145 this.drawScale(); 146 let x = this.xPoint; 147 switch (this._mode) { 148 case ChartMode.Byte: 149 for (let node of this.currentData!) { 150 let width = Math.ceil(node.size / this.currentSize * this.rect!.width); 151 let height = depthHeight; // 20px / depth 152 // ensure the data for first depth frame 153 if (!node.frame) { 154 node.frame = new Rect(x, scaleHeight, width, height) 155 } else { 156 node.frame!.x = x; 157 node.frame!.y = scaleHeight; 158 node.frame!.width = width; 159 node.frame!.height = height; 160 } 161 // not draw when rect not in canvas 162 if (x + width >= 0 && x < this.canvas!.width) { 163 NativeHookCallInfo.draw(this.cavasContext!, node, node.size / this.currentSize); 164 this.drawFrameChart(node); 165 } 166 x += width; 167 } 168 break; 169 case ChartMode.Count: 170 for (let node of this.currentData!) { 171 let width = Math.ceil(node.count / this.currentCount * this.rect!.width); 172 let height = depthHeight; // 20px / depth 173 // ensure the data for first depth frame 174 if (!node.frame) { 175 node.frame = new Rect(x, scaleHeight, width, height) 176 } else { 177 node.frame!.x = x; 178 node.frame!.y = scaleHeight; 179 node.frame!.width = width; 180 node.frame!.height = height; 181 } 182 // not draw when rect not in canvas 183 if (x + width >= 0 && x < this.canvas!.width) { 184 NativeHookCallInfo.draw(this.cavasContext!, node, node.count / this.currentCount); 185 } 186 this.drawFrameChart(node); 187 x += width; 188 } 189 break; 190 } 191 this.drawTriangleOnScale(); 192 this.cavasContext?.closePath(); 193 } 194 195 /** 196 * draw last selected resct position on scale 197 */ 198 private drawTriangleOnScale(): void { 199 if (ChartStruct.lastSelectFuncStruct) { 200 this.cavasContext!.fillStyle = `rgba(${82}, ${145}, ${255})`; 201 let x = Math.ceil(ChartStruct.lastSelectFuncStruct.frame!.x + 202 ChartStruct.lastSelectFuncStruct.frame!.width / 2) 203 if (x < 0) x = sideLenght / 2; 204 if (x > this.canvas!.width) x = this.canvas!.width - sideLenght; 205 this.cavasContext!.moveTo(x - sideLenght / 2, scaleHeight - sideLenght); 206 this.cavasContext!.lineTo(x + sideLenght / 2, scaleHeight - sideLenght); 207 this.cavasContext!.lineTo(x, scaleHeight); 208 this.cavasContext!.lineTo(x - sideLenght / 2, scaleHeight - sideLenght); 209 this.cavasContext?.fill(); 210 } 211 } 212 213 /** 214 * clear canvas all data 215 */ 216 public clearCanvas(): void { 217 this.cavasContext?.clearRect(0, 0, this.canvas!.width, this.canvas!.height); 218 } 219 220 /** 221 * update canvas size 222 */ 223 public updateCanvas(updateWidth: boolean, newWidth?: number): void { 224 if (this.canvas instanceof HTMLCanvasElement) { 225 this.canvas.style.width = 100 + "%"; 226 this.canvas.style.height = this.rect!.height + "px"; 227 if (this.canvas.clientWidth == 0 && newWidth) { 228 this.canvas.width = newWidth - 40; 229 } else { 230 this.canvas.width = this.canvas.clientWidth; 231 } 232 this.canvas.height = Math.ceil(this.rect!.height); 233 this.updateCanvasCoord(); 234 } 235 if (this.rect.width == 0 || updateWidth || 236 Math.round(newWidth!) != this.canvas!.width + 40 || newWidth! > this.rect.width) { 237 this.rect.width = this.canvas!.width 238 } 239 } 240 241 /** 242 * updateCanvasCoord 243 */ 244 private updateCanvasCoord(): void { 245 if (this.canvas instanceof HTMLCanvasElement) { 246 this.isUpdateCanvas = this.canvas.clientWidth != 0; 247 if (this.canvas.getBoundingClientRect()) { 248 let box = this.canvas.getBoundingClientRect(); 249 let D = document.documentElement; 250 this.startX = box.left + Math.max(D.scrollLeft, document.body.scrollLeft) - D.clientLeft; 251 this.startY = box.top + Math.max(D.scrollTop, document.body.scrollTop) - D.clientTop + this.canvasScrollTop; 252 } 253 } 254 } 255 256 /** 257 * draw top Scale Into 100 pieces 258 */ 259 private drawScale(): void { 260 let spApplication = <SpApplication>document.getElementsByTagName("sp-application")[0]; 261 // line 262 this.cavasContext!.lineWidth = 0.5; 263 this.cavasContext?.moveTo(0, 0); 264 this.cavasContext?.lineTo(this.canvas!.width, 0); 265 266 for (let i = 0; i <= 10; i++) { 267 let startX = Math.floor(this.canvas!.width / 10 * i); 268 for (let j = 0; j < 10; j++) { 269 // children scale 270 this.cavasContext!.lineWidth = 0.5; 271 let startItemX = startX + Math.floor(this.canvas!.width / 100 * j); 272 this.cavasContext?.moveTo(startItemX, 0); 273 this.cavasContext?.lineTo(startItemX, 10); 274 } 275 if (i == 0) continue; // skip first Size is 0 276 // long line every 10 count 277 this.cavasContext!.lineWidth = 1; 278 let sizeRatio = this.canvas!.width / this.rect.width; // scale ratio 279 if (spApplication.dark) { 280 this.cavasContext!.strokeStyle = "#888"; 281 } else { 282 this.cavasContext!.strokeStyle = "#ddd"; 283 } 284 this.cavasContext?.moveTo(startX, 0); 285 this.cavasContext?.lineTo(startX, this.canvas!.height); 286 if (spApplication.dark) { 287 this.cavasContext!.fillStyle = "#fff"; 288 } else { 289 this.cavasContext!.fillStyle = "#000"; 290 } 291 let scale = ''; 292 if (this._mode == ChartMode.Byte) { 293 scale = Utils.getByteWithUnit(this.currentSize * sizeRatio / 10 * i); 294 } else { 295 scale = (this.currentCount * sizeRatio / 10 * i).toFixed(0) + ''; 296 } 297 this.cavasContext?.fillText(scale, startX + 5, depthHeight, 50); // 50 is Text max Lenght 298 this.cavasContext?.stroke(); 299 } 300 } 301 302 /** 303 * draw chart 304 * @param node draw chart by every piece 305 */ 306 drawFrameChart(node: ChartStruct) { 307 if (node.children && node.children.length > 0) { 308 for (let children of node.children) { 309 children.parent = node; 310 let percent = 0; 311 if (this._mode == ChartMode.Byte) { 312 NativeHookCallInfo.setFuncFrame(children, this.rect, this.currentSize, this._mode); 313 percent = children.size / this.currentSize; 314 } else { 315 NativeHookCallInfo.setFuncFrame(children, this.rect, this.currentCount, this._mode); 316 percent = children.count / this.currentCount; 317 } 318 // not draw when rect not in canvas 319 if ((children.frame!.x + children.frame!.width >= 0 && 320 children.frame!.x < this.canvas!.width && children.frame!.width > filterPixiel) || children.needShow) { 321 NativeHookCallInfo.draw(this.cavasContext!, children, percent); 322 } 323 this.drawFrameChart(children); 324 } 325 } 326 } 327 328 /** 329 * find target node from tree by mouse position 330 * 331 * @param nodes tree nodes 332 * @param canvasX x coord of canvas 333 * @param canvasY y coord of canvas 334 * @returns target node 335 */ 336 private searchData(nodes: Array<ChartStruct>, canvasX: number, canvasY: number): any { 337 for (let node of nodes) { 338 if (node.frame?.contains(canvasX, canvasY)) { 339 return node; 340 } else { 341 let result = this.searchData(node.children, canvasX, canvasY); 342 if (!result) continue; // if not found in this branch;search another branch 343 return result; 344 } 345 } 346 return null; 347 } 348 349 /** 350 * show float hint and update position 351 */ 352 private updateFloatHint(): void { 353 this.floatHint!.innerHTML = this.hintContent; 354 this.floatHint!.style.display = 'flex'; 355 let x = this.canvasX; 356 let y = this.canvasY - this.canvasScrollTop; 357 //right rect hint show left 358 if (this.canvasX + this.floatHint!.clientWidth > (this.canvas?.clientWidth || 0)) { 359 x -= this.floatHint!.clientWidth - 1; 360 } else { 361 x += scaleHeight; 362 } 363 //bottom rect hint show top 364 y -= this.floatHint!.clientHeight - 1; 365 366 this.floatHint!.style.transform = `translate(${x}px,${y}px)`; 367 } 368 369 /** 370 * redraw Chart while click to scale chart 371 * @param selectData select Rect data as array 372 */ 373 private redrawChart(selectData: Array<ChartStruct>): void { 374 this.currentData = selectData; 375 if (selectData.length == 0) return; 376 this.caldrawArgs(); 377 this.calculateChartData(); 378 } 379 380 /** 381 * press w to zoom in, s to zoom out 382 * @param index < 0 zoom out , > 0 zoom in 383 */ 384 private scale(index: number): void { 385 let newWidth = 0; 386 // zoom in 387 let deltaWidth = this.rect!.width * 0.2; 388 if (index > 0) { 389 newWidth = this.rect!.width + deltaWidth; 390 // max scale 391 let sizeRatio = this.canvas!.width / this.rect.width; 392 if (this._mode == ChartMode.Byte) { 393 if (Math.round(this.currentSize * sizeRatio) <= 10) { 394 newWidth = this.canvas!.width / (10 / this.currentSize); 395 } 396 } else { 397 if (Math.round(this.currentCount * sizeRatio) <= 10) { 398 if (this.xPoint == 0) { 399 return; 400 } 401 newWidth = this.canvas!.width / (10 / this.currentCount); 402 } 403 } 404 deltaWidth = newWidth - this.rect!.width; 405 } else { // zoom out 406 newWidth = this.rect!.width - deltaWidth; 407 // min scale 408 if (newWidth < this.canvas!.width) { 409 newWidth = this.canvas!.width; 410 this.resetTrans(); 411 } 412 deltaWidth = this.rect!.width - newWidth; 413 } 414 // width not change 415 if (newWidth == this.rect.width) return; 416 this.translationByScale(index, deltaWidth, newWidth); 417 } 418 419 private resetTrans() { 420 this.xPoint = 0; 421 } 422 423 /** 424 * translation after scale 425 * @param index is zoom in 426 * @param deltaWidth scale delta width 427 * @param newWidth rect width after scale 428 */ 429 private translationByScale(index: number, deltaWidth: number, newWidth: number): void { 430 let translationValue = deltaWidth * (this.canvasX - this.xPoint) / this.rect.width; 431 if (index > 0) { 432 this.xPoint -= translationValue; 433 } else { 434 this.xPoint += translationValue; 435 } 436 this.rect!.width = newWidth; 437 this.translationDraw(); 438 } 439 440 /** 441 * press a/d to translate rect 442 * @param index left or right 443 */ 444 private translation(index: number): void { 445 let offset = this.canvas!.width / 10; 446 if (index < 0) { 447 this.xPoint += offset; 448 } else { 449 this.xPoint -= offset; 450 } 451 this.translationDraw(); 452 } 453 454 /** 455 * judge position ro fit canvas and draw 456 */ 457 private translationDraw(): void { 458 // rightad trans limit 459 if (this.xPoint > 0) { 460 this.xPoint = 0; 461 } 462 // left trans limit 463 if (this.rect.width + this.xPoint < this.canvas!.width) { 464 this.xPoint = this.canvas!.width - this.rect.width; 465 } 466 this.calculateChartData(); 467 } 468 469 /** 470 * canvas click 471 * @param e MouseEvent 472 */ 473 private onMouseClick(e: MouseEvent): void { 474 if (e.button == 0) { // mouse left button 475 if (ChartStruct.hoverFuncStruct && ChartStruct.hoverFuncStruct != ChartStruct.selectFuncStruct) { 476 this.drawDataSet(ChartStruct.lastSelectFuncStruct!, false); 477 ChartStruct.lastSelectFuncStruct = undefined; 478 ChartStruct.selectFuncStruct = ChartStruct.hoverFuncStruct; 479 this.historyList.push(this.currentData!); 480 let selectData = new Array<ChartStruct>(); 481 selectData.push(ChartStruct.selectFuncStruct!); 482 // reset scale and translation 483 this.rect.width = this.canvas!.clientWidth; 484 this.resetTrans(); 485 this.redrawChart(selectData); 486 for (let callback of this.chartClickListenerList) { 487 callback(false); 488 } 489 } 490 } else if (e.button == 2) { // mouse right button 491 ChartStruct.selectFuncStruct = undefined; 492 if (this.currentData.length == 1 && this.historyList.length > 0) { 493 ChartStruct.lastSelectFuncStruct = this.currentData[0]; 494 this.drawDataSet(ChartStruct.lastSelectFuncStruct, true); 495 } 496 if (this.historyList.length > 0) { 497 // reset scale and translation 498 this.rect.width = this.canvas!.clientWidth; 499 this.resetTrans(); 500 this.redrawChart(this.historyList.pop()!); 501 } 502 if (this.historyList.length === 0) { 503 for (let callback of this.chartClickListenerList) { 504 callback(true); 505 } 506 } 507 } 508 this.hideFloatHint(); 509 } 510 511 private hideFloatHint(){ 512 if (this.floatHint) { 513 this.floatHint.style.display = 'none'; 514 } 515 } 516 517 /** 518 * set current select rect parents will show 519 * @param data current noode 520 * @param isShow is show in chart 521 */ 522 private drawDataSet(data: ChartStruct, isShow: boolean): void { 523 if (data) { 524 data.needShow = isShow; 525 if (data.parent) { 526 this.drawDataSet(data.parent, isShow); 527 } 528 } 529 } 530 531 /** 532 * mouse on canvas move event 533 */ 534 private onMouseMove(): void { 535 let lastNode = ChartStruct.hoverFuncStruct; 536 let searchResult = this.searchData(this.currentData!, this.canvasX, this.canvasY); 537 if (searchResult && (searchResult.frame!.width > filterPixiel || 538 searchResult.needShow || searchResult.depth == 0)) { 539 ChartStruct.hoverFuncStruct = searchResult; 540 // judge current node is hover redraw chart 541 if (searchResult != lastNode) { 542 let name = ChartStruct.hoverFuncStruct?.symbol; 543 if (this._mode == ChartMode.Byte) { 544 let size = Utils.getByteWithUnit(ChartStruct.hoverFuncStruct!.size); 545 this.hintContent = `<span>Name: ${name} </span><span>Size: ${size}</span>`; 546 } else { 547 let count = ChartStruct.hoverFuncStruct!.count; 548 this.hintContent = `<span>Name: ${name} </span><span>Count: ${count}</span>`; 549 } 550 this.calculateChartData(); 551 } 552 // pervent float hint trigger onmousemove event 553 this.updateFloatHint(); 554 } else { 555 this.hideFloatHint(); 556 ChartStruct.hoverFuncStruct = undefined; 557 } 558 } 559 560 initElements(): void { 561 this.canvas = this.shadowRoot?.querySelector("#canvas"); 562 this.cavasContext = this.canvas?.getContext("2d"); 563 this.floatHint = this.shadowRoot?.querySelector('#float_hint'); 564 565 this.canvas!.oncontextmenu = () => { 566 return false; 567 }; 568 this.canvas!.onmouseup = (e) => { 569 this.onMouseClick(e); 570 } 571 572 this.canvas!.onmousemove = (e) => { 573 if (!this.isUpdateCanvas) { 574 this.updateCanvasCoord(); 575 } 576 this.canvasX = e.clientX - this.startX; 577 this.canvasY = e.clientY - this.startY + this.canvasScrollTop; 578 this.isFocusing = true; 579 this.onMouseMove(); 580 }; 581 582 this.canvas!.onmouseleave = () => { 583 ChartStruct.selectFuncStruct = undefined; 584 this.isFocusing = false; 585 this.hideFloatHint(); 586 }; 587 588 document.addEventListener('keydown', (e) => { 589 if (!this.isFocusing) return; 590 switch (e.key.toLocaleLowerCase()) { 591 case 'w': 592 this.scale(1); 593 break; 594 case 's': 595 this.scale(-1); 596 break; 597 case 'a': 598 this.translation(-1); 599 break; 600 case 'd': 601 this.translation(1); 602 break; 603 } 604 }); 605 new ResizeObserver((entries) => { 606 if (this.canvas!.getBoundingClientRect()) { 607 let box = this.canvas!.getBoundingClientRect(); 608 let D = document.documentElement; 609 this.startX = box.left + Math.max(D.scrollLeft, document.body.scrollLeft) - D.clientLeft; 610 this.startY = box.top + Math.max(D.scrollTop, document.body.scrollTop) - D.clientTop + this.canvasScrollTop; 611 } 612 }).observe(document.documentElement); 613 } 614 615 initHtml(): string { 616 return ` 617 <style> 618 :host{ 619 display: flex; 620 padding: 10px 10px; 621 } 622 .tip{ 623 position:absolute; 624 left: 0; 625 background-color: white; 626 border: 1px solid #f9f9f9; 627 width: auto; 628 font-size: 8px; 629 color: #50809e; 630 flex-direction: column; 631 justify-content: center; 632 align-items: flex-start; 633 padding: 2px 10px; 634 display: none; 635 user-select: none; 636 } 637 </style> 638 <canvas id="canvas"></canvas> 639 <div id ="float_hint" class="tip"></div>`; 640 } 641}