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