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'; 17import { Rect } from '../trace/timer-shaft/Rect'; 18import { ChartMode, ChartStruct, draw, setFuncFrame } from '../../bean/FrameChartStruct'; 19import { SpApplication } from '../../SpApplication'; 20import { Utils } from '../trace/base/Utils'; 21import { TabPaneFilter } from '../trace/sheet/TabPaneFilter'; 22 23const scaleHeight = 30; // 刻度尺高度 24const depthHeight = 20; // 调用栈高度 25const filterPixel = 2; // 过滤像素 26const textMaxWidth = 50; 27const scaleRatio = 0.2; // 缩放比例 28const ms10 = 10_000_000; 29const jsHapKeys = ['.hap', '.hsp', '.har']; 30const jsStackPath = ['.ts', '.ets', '.js']; 31const textStyle = '12px bold'; 32 33class NodeValue { 34 size: number; 35 count: number; 36 dur: number; 37 eventCount: number; 38 39 constructor() { 40 this.size = 0; 41 this.count = 0; 42 this.dur = 0; 43 this.eventCount = 0; 44 } 45} 46 47@element('tab-framechart') 48export class FrameChart extends BaseElement { 49 private canvas!: HTMLCanvasElement; 50 private canvasContext!: CanvasRenderingContext2D; 51 private floatHint!: HTMLDivElement | undefined | null; // 悬浮框 52 53 private rect: Rect = new Rect(0, 0, 0, 0); 54 private _mode = ChartMode.Byte; 55 private startX = 0; // 画布相对于整个界面的x坐标 56 private startY = 0; // 画布相对于整个界面的y坐标 57 private canvasX = -1; // 鼠标当前所在画布位置x坐标 58 private canvasY = -1; // 鼠标当前所在画布位置y坐标 59 private hintContent = ''; // 悬浮框内容。 html格式字符串 60 private rootNode!: ChartStruct; 61 private currentData: Array<ChartStruct> = []; 62 private xPoint = 0; // x in rect 63 private isFocusing = false; // 鼠标是否在画布范围内 64 private canvasScrollTop = 0; // Tab页上下滚动位置 65 private _maxDepth = 0; 66 private chartClickListenerList: Array<Function> = []; 67 private isUpdateCanvas = false; 68 private isClickMode = false; //是否为点选模式 69 _totalRootData: Array<ChartStruct> = []; //初始化顶部root的数据 70 private totalRootNode!: ChartStruct; 71 private tabPaneFilter: TabPaneFilter | undefined; 72 73 /** 74 * set chart mode 75 * @param mode chart format for data mode 76 */ 77 set mode(mode: ChartMode) { 78 this._mode = mode; 79 } 80 81 set data(val: Array<ChartStruct>) { 82 this.tabPaneFilter = document 83 .querySelector('body > sp-application') 84 ?.shadowRoot?.querySelector('#sp-system-trace') 85 ?.shadowRoot?.querySelector('div > trace-sheet') 86 ?.shadowRoot?.querySelector('#box-native-calltree > tabpane-nm-calltree') 87 ?.shadowRoot?.querySelector('#nm-call-tree-filter') as TabPaneFilter; 88 ChartStruct.lastSelectFuncStruct = undefined; 89 this.setSelectStatusRecursive(ChartStruct.selectFuncStruct, true); 90 ChartStruct.selectFuncStruct = undefined; 91 this.isClickMode = false; 92 this.currentData = val; 93 this.resetTrans(); 94 this.calDrawArgs(true); 95 } 96 97 set tabPaneScrollTop(scrollTop: number) { 98 this.canvasScrollTop = scrollTop; 99 this.hideTip(); 100 } 101 102 get totalRootData(): Array<ChartStruct> { 103 return this._totalRootData; 104 } 105 106 set totalRootData(value: Array<ChartStruct>) { 107 this._totalRootData = value; 108 } 109 110 private get total(): number { 111 return this.getNodeValue(this.rootNode); 112 } 113 114 private getNodeValue(node: ChartStruct): number { 115 let result: number; 116 switch (this._mode) { 117 case ChartMode.Byte: 118 result = node.drawSize || node.size; 119 break; 120 case ChartMode.Count: 121 result = node.drawCount || node.count; 122 break; 123 case ChartMode.Duration: 124 result = node.drawDur || node.dur; 125 break; 126 case ChartMode.EventCount: 127 result = node.drawEventCount || node.eventCount; 128 break; 129 } 130 return result; 131 } 132 133 /** 134 * add callback of chart click 135 * @param callback function of chart click 136 */ 137 public addChartClickListener(callback: Function): void { 138 if (this.chartClickListenerList.indexOf(callback) < 0) { 139 this.chartClickListenerList.push(callback); 140 } 141 } 142 143 /** 144 * remove callback of chart click 145 * @param callback function of chart click 146 */ 147 public removeChartClickListener(callback: Function): void { 148 const index = this.chartClickListenerList.indexOf(callback); 149 if (index > -1) { 150 this.chartClickListenerList.splice(index, 1); 151 } 152 } 153 154 private createRootNode(): void { 155 // 初始化root 156 this.rootNode = new ChartStruct(); 157 this.rootNode.symbol = 'root'; 158 this.rootNode.depth = 0; 159 this.rootNode.percent = 1; 160 this.rootNode.frame = new Rect(0, scaleHeight, this.canvas!.width, depthHeight); 161 for (const node of this.currentData!) { 162 this.rootNode.children.push(node); 163 this.rootNode.count += node.drawCount || node.count; 164 this.rootNode.size += node.drawSize || node.size; 165 this.rootNode.dur += node.drawDur || node.dur; 166 this.rootNode.eventCount += node.drawEventCount || node.eventCount; 167 node.parent = this.rootNode; 168 } 169 this.totalRootNode = new ChartStruct(); 170 this.totalRootNode.symbol = 'root'; 171 this.totalRootNode.depth = 0; 172 this.totalRootNode.percent = 1; 173 this.totalRootNode.frame = new Rect(0, scaleHeight, this.canvas!.width, depthHeight); 174 for (const node of this._totalRootData!) { 175 this.totalRootNode.children.push(node); 176 this.totalRootNode.count += node.drawCount || node.count; 177 this.totalRootNode.size += node.drawSize || node.size; 178 this.totalRootNode.dur += node.drawDur || node.dur; 179 this.totalRootNode.eventCount += node.drawEventCount || node.eventCount; 180 node.parent = this.totalRootNode; 181 } 182 } 183 184 /** 185 * 1.计算调用栈最大深度 186 * 2.计算搜索情况下每个函数块显示的大小(非实际大小) 187 * 3.计算点选情况下每个函数块的显示大小(非实际大小) 188 * @param initRoot 是否初始化root节点 189 */ 190 private calDrawArgs(initRoot: boolean): void { 191 this._maxDepth = 0; 192 if (initRoot) { 193 this.createRootNode(); 194 } 195 this.initData(this.rootNode, 0, true); 196 this.selectInit(); 197 this.setRootValue(); 198 this.rect.width = this.canvas!.width; 199 this.rect.height = (this._maxDepth + 1) * depthHeight + scaleHeight; 200 this.canvas!.style.height = `${this.rect!.height}px`; 201 this.canvas!.height = Math.ceil(this.rect!.height); 202 } 203 204 /** 205 * 点选情况下由点选来设置每个函数的显示Size 206 */ 207 private selectInit(): void { 208 const node = ChartStruct.selectFuncStruct; 209 if (node) { 210 const module = new NodeValue(); 211 node.drawCount = 0; 212 node.drawDur = 0; 213 node.drawSize = 0; 214 node.drawEventCount = 0; 215 for (let child of node.children) { 216 node.drawCount += child.searchCount; 217 node.drawDur += child.searchDur; 218 node.drawSize += child.searchSize; 219 node.drawEventCount += child.searchEventCount; 220 } 221 module.count = node.drawCount = node.drawCount || node.count; 222 module.dur = node.drawDur = node.drawDur || node.dur; 223 module.size = node.drawSize = node.drawSize || node.size; 224 module.eventCount = node.drawEventCount = node.drawEventCount || node.eventCount; 225 226 this.setParentDisplayInfo(node, module, true); 227 this.setChildrenDisplayInfo(node); 228 this.clearOtherDisplayInfo(this.rootNode); 229 } 230 } 231 232 private clearOtherDisplayInfo(node: ChartStruct): void { 233 for (const children of node.children) { 234 if (children.isChartSelect) { 235 children.isChartSelect = false; 236 this.clearOtherDisplayInfo(children); 237 continue; 238 } 239 children.drawCount = 0; 240 children.drawEventCount = 0; 241 children.drawSize = 0; 242 children.drawDur = 0; 243 this.clearOtherDisplayInfo(children); 244 } 245 } 246 247 // 设置root显示区域value 以及占真实value的百分比 248 private setRootValue(): void { 249 let currentValue = ''; 250 let currentValuePercent = 1; 251 switch (this._mode) { 252 case ChartMode.Byte: 253 currentValue = Utils.getBinaryByteWithUnit(this.total); 254 currentValuePercent = this.total / this.rootNode.size; 255 break; 256 case ChartMode.Count: 257 currentValue = `${this.total}`; 258 currentValuePercent = this.total / this.totalRootNode.count; 259 break; 260 case ChartMode.Duration: 261 currentValue = Utils.getProbablyTime(this.total); 262 currentValuePercent = this.total / this.rootNode.dur; 263 break; 264 case ChartMode.EventCount: 265 currentValue = `${this.total}`; 266 currentValuePercent = this.total / this.totalRootNode.eventCount; 267 break; 268 } 269 let endStr = currentValuePercent ? ` (${(currentValuePercent * 100).toFixed(2)}%)` : ''; 270 this.rootNode.symbol = `Root : ${currentValue}${endStr}`; 271 } 272 273 /** 274 * 判断lib中是否包含.ts .ets .js .hap 275 * @param str node.lib 276 * @returns 是否包含 277 */ 278 private isJsStack(str: string): boolean { 279 let keyList = jsStackPath; 280 if (this._mode === ChartMode.Count || this._mode === ChartMode.EventCount) { 281 keyList = jsStackPath.concat(jsHapKeys); 282 } 283 for (const format of keyList) { 284 if (str.indexOf(format) > 0) { 285 return true; 286 } 287 } 288 return false; 289 } 290 291 private clearSuperfluousParams(node: ChartStruct): void { 292 node.id = undefined; 293 node.eventType = undefined; 294 node.parentId = undefined; 295 node.title = undefined; 296 node.eventType = undefined; 297 if (this.mode === ChartMode.Byte) { 298 node.self = undefined; 299 node.eventCount = 0; 300 } 301 if (this._mode !== ChartMode.Count && this._mode !== ChartMode.EventCount) { 302 node.eventCount = 0; 303 node.eventPercent = undefined; 304 } 305 } 306 307 /** 308 * 计算调用栈最大深度,计算每个node显示大小 309 * @param node 函数块 310 * @param depth 当前递归深度 311 * @param calDisplay 该层深度是否需要计算显示大小 312 */ 313 private initData(node: ChartStruct, depth: number, calDisplay: boolean): void { 314 node.depth = depth; 315 depth++; 316 this.clearSuperfluousParams(node); 317 if (this.isJsStack(node.lib)) { 318 node.isJsStack = true; 319 } else { 320 node.isJsStack = false; 321 } 322 323 //设置搜索以及点选的显示值,将点击/搜索的值设置为父节点的显示值,或者反选有子节点并且只有一个子节点 324 this.clearDisplayInfo(node); 325 if ((node.isSearch && calDisplay) || (node.children && calDisplay && node.children.length === 0 && node.isReverseFilter)) { 326 const module = new NodeValue(); 327 module.size = node.drawSize = node.searchSize = node.size; 328 module.count = node.drawCount = node.searchCount = node.count; 329 module.dur = node.drawDur = node.searchDur = node.dur; 330 module.eventCount = node.drawEventCount = node.searchEventCount = node.eventCount; 331 this.setParentDisplayInfo(node, module, false); 332 calDisplay = false; 333 } 334 335 // 设置parent以及计算最大的深度 336 if (node.children && node.children.length > 0) { 337 for (const children of node.children) { 338 children.parent = node; 339 this.initData(children, depth, calDisplay); 340 } 341 } else { 342 this._maxDepth = Math.max(depth, this._maxDepth); 343 } 344 } 345 346 // 递归设置node parent的显示大小 347 private setParentDisplayInfo(node: ChartStruct, module: NodeValue, isSelect?: boolean): void { 348 const parent = node.parent; 349 if (parent) { 350 if (isSelect) { 351 parent.isChartSelect = true; 352 parent.isChartSelectParent = true; 353 parent.drawCount = module.count; 354 parent.drawDur = module.dur; 355 parent.drawSize = module.size; 356 parent.drawEventCount = module.eventCount; 357 } else { 358 parent.searchCount += module.count; 359 parent.searchDur += module.dur; 360 parent.searchSize += module.size; 361 parent.searchEventCount += module.eventCount; 362 // 点击模式下不需要赋值draw value,由点击去 363 if (!this.isClickMode) { 364 parent.drawDur = parent.searchDur; 365 parent.drawCount = parent.searchCount; 366 parent.drawSize = parent.searchSize; 367 parent.drawEventCount = parent.searchEventCount; 368 } 369 } 370 this.setParentDisplayInfo(parent, module, isSelect); 371 } 372 } 373 374 /** 375 * 点击与搜索同时触发情况下,由点击去设置绘制大小 376 * @param node 当前点选的函数 377 * @returns void 378 */ 379 private setChildrenDisplayInfo(node: ChartStruct): void { 380 if (node.children.length < 0) { 381 return; 382 } 383 for (const children of node.children) { 384 children.drawCount = children.searchCount || children.count; 385 children.drawDur = children.searchDur || children.dur; 386 children.drawSize = children.searchSize || children.size; 387 children.drawEventCount = children.searchEventCount || children.eventCount; 388 this.setChildrenDisplayInfo(children); 389 } 390 } 391 392 private clearDisplayInfo(node: ChartStruct): void { 393 node.drawCount = 0; 394 node.drawDur = 0; 395 node.drawSize = 0; 396 node.drawEventCount = 0; 397 node.searchCount = 0; 398 node.searchDur = 0; 399 node.searchSize = 0; 400 node.searchEventCount = 0; 401 } 402 403 /** 404 * 计算每个函数块的坐标信息以及绘制火焰图 405 */ 406 public async calculateChartData(): Promise<void> { 407 this.clearCanvas(); 408 this.canvasContext?.beginPath(); 409 this.canvasContext.font = textStyle; 410 // 绘制刻度线 411 this.drawCalibrationTails(); 412 // 绘制root节点 413 draw(this.canvasContext, this.rootNode); 414 // 设置子节点的位置以及宽高 415 this.setFrameData(this.rootNode); 416 // 绘制子节点 417 this.drawFrameChart(this.rootNode); 418 this.canvasContext?.closePath(); 419 } 420 421 /** 422 * 清空画布 423 */ 424 public clearCanvas(): void { 425 this.canvasContext?.clearRect(0, 0, this.canvas!.width, this.canvas!.height); 426 } 427 428 /** 429 * 在窗口大小变化时调整画布大小 430 */ 431 public updateCanvas(updateWidth: boolean, newWidth?: number): void { 432 if (this.canvas instanceof HTMLCanvasElement) { 433 this.canvas.style.width = `${100}%`; 434 this.canvas.style.height = `${this.rect!.height}px`; 435 if (this.canvas.clientWidth === 0 && newWidth) { 436 this.canvas.width = newWidth - depthHeight * 2; 437 } else { 438 this.canvas.width = this.canvas.clientWidth; 439 } 440 this.canvas.height = Math.ceil(this.rect!.height); 441 this.updateCanvasCoord(); 442 } 443 if ( 444 this.rect.width === 0 || 445 updateWidth || 446 Math.round(newWidth!) !== this.canvas!.width + depthHeight * 2 || 447 newWidth! > this.rect.width 448 ) { 449 this.rect.width = this.canvas!.width; 450 } 451 } 452 453 /** 454 * 更新画布坐标 455 */ 456 private updateCanvasCoord(): void { 457 if (this.canvas instanceof HTMLCanvasElement) { 458 this.isUpdateCanvas = this.canvas.clientWidth !== 0; 459 if (this.canvas.getBoundingClientRect()) { 460 const box = this.canvas.getBoundingClientRect(); 461 const D = document.documentElement; 462 this.startX = box.left + Math.max(D.scrollLeft, document.body.scrollLeft) - D.clientLeft; 463 this.startY = box.top + Math.max(D.scrollTop, document.body.scrollTop) - D.clientTop + this.canvasScrollTop; 464 } 465 } 466 } 467 468 /** 469 * 绘制刻度尺,分为100段,每10段画一条长线 470 */ 471 private drawCalibrationTails(): void { 472 const spApplication = <SpApplication>document.getElementsByTagName('sp-application')[0]; 473 this.canvasContext!.lineWidth = 0.5; 474 this.canvasContext?.moveTo(0, 0); 475 this.canvasContext?.lineTo(this.canvas!.width, 0); 476 for (let i = 0; i <= 10; i++) { 477 let startX = Math.floor((this.canvas!.width / 10) * i); 478 for (let j = 0; j < 10; j++) { 479 this.canvasContext!.lineWidth = 0.5; 480 const startItemX = startX + Math.floor((this.canvas!.width / 100) * j); 481 this.canvasContext?.moveTo(startItemX, 0); 482 this.canvasContext?.lineTo(startItemX, 10); 483 } 484 if (i === 0) { 485 continue; 486 } 487 this.canvasContext!.lineWidth = 1; 488 const sizeRatio = this.canvas!.width / this.rect.width; // scale ratio 489 if (spApplication.dark) { 490 this.canvasContext!.strokeStyle = '#888'; 491 } else { 492 this.canvasContext!.strokeStyle = '#ddd'; 493 } 494 this.canvasContext?.moveTo(startX, 0); 495 this.canvasContext?.lineTo(startX, this.canvas!.height); 496 if (spApplication.dark) { 497 this.canvasContext!.fillStyle = '#fff'; 498 } else { 499 this.canvasContext!.fillStyle = '#000'; 500 } 501 let calibration = ''; 502 switch (this._mode) { 503 case ChartMode.Byte: 504 calibration = Utils.getByteWithUnit(((this.total * sizeRatio) / 10) * i); 505 break; 506 case ChartMode.Duration: 507 calibration = Utils.getProbablyTime(((this.total * sizeRatio) / 10) * i); 508 break; 509 case ChartMode.EventCount: 510 case ChartMode.Count: 511 calibration = `${Math.ceil(((this.total * sizeRatio) / 10) * i)}`; 512 break; 513 } 514 const size = this.canvasContext!.measureText(calibration).width; 515 this.canvasContext?.fillText(calibration, startX - size - 5, depthHeight, textMaxWidth); 516 this.canvasContext?.stroke(); 517 } 518 } 519 520 /** 521 * 设置每个node的宽高,开始坐标 522 * @param node 函数块 523 */ 524 private setFrameData(node: ChartStruct): void { 525 if (node.children.length > 0) { 526 for (const children of node.children) { 527 node.isDraw = false; 528 if (this.isClickMode && ChartStruct.selectFuncStruct) { 529 //处理点击逻辑,当前node为点选调用栈,children不是点选调用栈,width置为0 530 if (!children.isChartSelect) { 531 if (children.frame) { 532 children.frame.x = this.rootNode.frame?.x || 0; 533 children.frame.width = 0; 534 children.percent = 0; 535 } else { 536 children.frame = new Rect(0, 0, 0, 0); 537 } 538 this.setFrameData(children); 539 continue; 540 } 541 } 542 const childrenValue = this.getNodeValue(children); 543 setFuncFrame(children, this.rect, this.total, this._mode); 544 children.percent = childrenValue / this.total; 545 this.setFrameData(children); 546 } 547 } 548 } 549 550 /** 551 * 计算有效数据,当node的宽度太小不足以绘制时 552 * 计算忽略node的size 553 * 忽略的size将转换成width,按照比例平摊到显示的node上 554 * @param node 当前node 555 * @param effectChildList 生效的node 556 */ 557 private calEffectNode(node: ChartStruct, effectChildList: Array<ChartStruct>): number { 558 const ignore = new NodeValue(); 559 for (const children of node.children) { 560 // 小于1px的不绘制,并将其size平均赋值给>1px的 561 if (children.frame!.width >= filterPixel) { 562 effectChildList.push(children); 563 } else { 564 if (node.isChartSelect || this.isSearch(node)) { 565 ignore.size += children.drawSize; 566 ignore.count += children.drawCount; 567 ignore.dur += children.drawDur; 568 ignore.eventCount += children.drawEventCount; 569 } else { 570 ignore.size += children.size; 571 ignore.count += children.count; 572 ignore.dur += children.dur; 573 ignore.eventCount += children.eventCount; 574 } 575 } 576 } 577 let result: number = 0; 578 switch (this._mode) { 579 case ChartMode.Byte: 580 result = ignore.size; 581 break; 582 case ChartMode.Count: 583 result = ignore.count; 584 break; 585 case ChartMode.Duration: 586 result = ignore.dur; 587 break; 588 case ChartMode.EventCount: 589 result = ignore.eventCount; 590 break; 591 } 592 return result; 593 } 594 595 private isSearch(node: ChartStruct): boolean { 596 let result: boolean = false; 597 switch (this._mode) { 598 case ChartMode.Byte: 599 result = node.searchSize > 0; 600 break; 601 case ChartMode.Count: 602 result = node.searchCount > 0; 603 break; 604 case ChartMode.Duration: 605 result = node.searchDur > 0; 606 break; 607 case ChartMode.EventCount: 608 result = node.searchEventCount > 0; 609 break; 610 } 611 return result; 612 } 613 614 /** 615 * 绘制每个函数色块 616 * @param node 函数块 617 */ 618 private drawFrameChart(node: ChartStruct): void { 619 const effectChildList: Array<ChartStruct> = []; 620 const nodeValue = this.getNodeValue(node); 621 622 if (node.children && node.children.length > 0) { 623 const ignoreValue = this.calEffectNode(node, effectChildList); 624 let x = node.frame!.x; 625 if (effectChildList.length > 0) { 626 for (let children of effectChildList) { 627 children.frame!.x = x; 628 const childrenValue = this.getNodeValue(children); 629 children.frame!.width = (childrenValue / (nodeValue - ignoreValue)) * node.frame!.width; 630 x += children.frame!.width; 631 if (this.nodeInCanvas(children)) { 632 draw(this.canvasContext!, children); 633 this.drawFrameChart(children); 634 } 635 } 636 } else { 637 const firstChildren = node.children[0]; 638 firstChildren.frame!.x = node.frame!.x; 639 // perf parent有selfTime 需要所有children的count跟 640 firstChildren.frame!.width = node.frame!.width * (ignoreValue / nodeValue); 641 draw(this.canvasContext!, firstChildren); 642 this.drawFrameChart(firstChildren); 643 } 644 } 645 } 646 647 /** 648 * 根据鼠标当前的坐标递归查找对应的函数块 649 * 650 * @param nodes 651 * @param canvasX 鼠标相对于画布开始点的x坐标 652 * @param canvasY 鼠标相对于画布开始点的y坐标 653 * @returns 当前鼠标位置的函数块 654 */ 655 private searchDataByCoord(nodes: Array<ChartStruct>, canvasX: number, canvasY: number): ChartStruct | null { 656 for (const node of nodes) { 657 if (node.frame?.contains(canvasX, canvasY)) { 658 return node; 659 } else { 660 const result = this.searchDataByCoord(node.children, canvasX, canvasY); 661 // if not found in this branch;search another branch 662 if (!result) { 663 continue; 664 } 665 return result; 666 } 667 } 668 return null; 669 } 670 671 /** 672 * 显示悬浮框信息,更新位置 673 */ 674 private showTip(): void { 675 this.floatHint!.innerHTML = this.hintContent; 676 this.floatHint!.style.display = 'block'; 677 this.floatHint!.style.zIndex = '9999999'; 678 const countSpan = this.floatHint!.querySelector('.count'); 679 if (countSpan) { 680 //@ts-ignore 681 countSpan.onclick = (e) => { 682 this.dispatchEvent( 683 new CustomEvent('td-click', { 684 detail: ChartStruct.tempSelectStruct, 685 composed: true, 686 }) 687 ); 688 // @ts-ignore 689 e.stopPropagation(); 690 }; 691 } 692 let tipArea = 693 this.tabPaneFilter?.getBoundingClientRect().top! - 694 this.canvas.getBoundingClientRect().top - 695 this.canvasScrollTop - 696 scaleHeight; 697 let x = this.canvasX; 698 let y = this.canvasY - this.canvasScrollTop; 699 //右边的函数块悬浮框显示在函数左边 700 if (this.canvasX + this.floatHint!.clientWidth > (this.canvas?.clientWidth || 0)) { 701 x -= this.floatHint!.clientWidth - 1; 702 } else { 703 x += scaleHeight; 704 } 705 //顶部悬浮框显示在函数下边,下半部分悬浮框显示在函数上边 706 if (y > this.floatHint!.clientHeight || y + this.floatHint!.clientHeight > tipArea) { 707 y -= this.floatHint!.clientHeight - 1; 708 } 709 710 this.floatHint!.style.transform = `translate(${x}px,${y}px)`; 711 } 712 713 /** 714 * 递归设置传入node的parent以及children的isSelect 715 * 将上次点选的整条树的isSelect置为false 716 * 将本次点击的整条树的isSelect置为true 717 * @param node 点击的node 718 * @param isSelect 点选 719 */ 720 private setSelectStatusRecursive(node: ChartStruct | undefined, isSelect: boolean): void { 721 if (!node) { 722 return; 723 } 724 node.isChartSelect = isSelect; 725 726 // 处理子节点及其子节点的子节点 727 const stack: ChartStruct[] = [node]; // 使用栈来实现循环处理 728 while (stack.length > 0) { 729 const currentNode = stack.pop(); 730 if (currentNode) { 731 currentNode.children.forEach((child) => { 732 child.isChartSelect = isSelect; 733 stack.push(child); 734 }); 735 } 736 } 737 738 // 处理父节点 739 while (node?.parent) { 740 node.parent.isChartSelect = isSelect; 741 node.parent.isChartSelectParent = isSelect; 742 node = node.parent; 743 } 744 } 745 746 /** 747 * 点选后重绘火焰图 748 */ 749 private clickRedraw(): void { 750 //将上次点选的isSelect置为false 751 if (ChartStruct.lastSelectFuncStruct) { 752 this.setSelectStatusRecursive(ChartStruct.lastSelectFuncStruct!, false); 753 } 754 // 递归设置点选的parent,children为点选状态 755 this.calDrawArgs(false); 756 this.setSelectStatusRecursive(ChartStruct.selectFuncStruct!, true); 757 758 this.calculateChartData(); 759 } 760 761 /** 762 * 点击w s的放缩算法 763 * @param index < 0 缩小 , > 0 放大 764 */ 765 private scale(index: number): void { 766 let newWidth = 0; 767 let deltaWidth = this.rect!.width * scaleRatio; 768 const ratio = 1 + scaleRatio; 769 if (index > 0) { 770 // zoom in 771 newWidth = this.rect!.width + deltaWidth; 772 const sizeRatio = this.canvas!.width / this.rect.width; // max scale 773 switch (this._mode) { 774 case ChartMode.Byte: 775 case ChartMode.Count: 776 case ChartMode.EventCount: 777 if (Math.round((this.total * sizeRatio) / ratio) <= 10) { 778 if (this.xPoint === 0) { 779 return; 780 } 781 newWidth = this.canvas!.width / (10 / this.total); 782 } 783 break; 784 case ChartMode.Duration: 785 if (Math.round((this.total * sizeRatio) / ratio) <= ms10) { 786 if (this.xPoint === 0) { 787 return; 788 } 789 newWidth = this.canvas!.width / (ms10 / this.total); 790 } 791 break; 792 } 793 deltaWidth = newWidth - this.rect!.width; 794 } else { 795 // zoom out 796 newWidth = this.rect!.width - deltaWidth; 797 if (newWidth < this.canvas!.width) { 798 newWidth = this.canvas!.width; 799 this.resetTrans(); 800 } 801 deltaWidth = this.rect!.width - newWidth; 802 } 803 // width not change 804 if (newWidth === this.rect.width) { 805 return; 806 } 807 this.translationByScale(index, deltaWidth, newWidth); 808 } 809 810 private resetTrans(): void { 811 this.xPoint = 0; 812 } 813 814 /** 815 * 放缩之后的平移算法 816 * @param index < 0 缩小 , > 0 放大 817 * @param deltaWidth 放缩增量 818 * @param newWidth 放缩后的宽度 819 */ 820 private translationByScale(index: number, deltaWidth: number, newWidth: number): void { 821 const translationValue = (deltaWidth * (this.canvasX - this.xPoint)) / this.rect.width; 822 if (index > 0) { 823 this.xPoint -= translationValue; 824 } else { 825 this.xPoint += translationValue; 826 } 827 this.rect!.width = newWidth; 828 829 this.translationDraw(); 830 } 831 832 /** 833 * 点击a d 平移 834 * @param index < 0 左移; >0 右移 835 */ 836 private translation(index: number): void { 837 const offset = this.canvas!.width / 10; 838 if (index < 0) { 839 this.xPoint += offset; 840 } else { 841 this.xPoint -= offset; 842 } 843 this.translationDraw(); 844 } 845 846 /** 847 * judge position ro fit canvas and draw 848 */ 849 private translationDraw(): void { 850 // right trans limit 851 if (this.xPoint > 0) { 852 this.xPoint = 0; 853 } 854 // left trans limit 855 if (this.rect.width + this.xPoint < this.canvas!.width) { 856 this.xPoint = this.canvas!.width - this.rect.width; 857 } 858 this.rootNode.frame!.width = this.rect.width; 859 this.rootNode.frame!.x = this.xPoint; 860 this.calculateChartData(); 861 } 862 863 private nodeInCanvas(node: ChartStruct): boolean { 864 if (!node.frame) { 865 return false; 866 } 867 return node.frame.x + node.frame.width >= 0 && node.frame.x < this.canvas.clientWidth; 868 } 869 private onMouseClick(e: MouseEvent): void { 870 if (e.button === 0) { 871 // mouse left button 872 if (ChartStruct.hoverFuncStruct && ChartStruct.hoverFuncStruct !== ChartStruct.selectFuncStruct) { 873 ChartStruct.lastSelectFuncStruct = ChartStruct.selectFuncStruct; 874 ChartStruct.selectFuncStruct = ChartStruct.hoverFuncStruct; 875 ChartStruct.tempSelectStruct = undefined; 876 this.isClickMode = ChartStruct.selectFuncStruct !== this.rootNode; 877 this.rect.width = this.canvas!.clientWidth; 878 // 重置缩放 879 this.resetTrans(); 880 this.rootNode.frame!.x = this.xPoint; 881 this.rootNode.frame!.width = this.rect.width = this.canvas.clientWidth; 882 // 重新绘图 883 this.clickRedraw(); 884 document.dispatchEvent( 885 new CustomEvent('number_calibration', { 886 detail: { 887 time: ChartStruct.selectFuncStruct.tsArray, 888 counts: ChartStruct.selectFuncStruct.countArray, 889 durations: ChartStruct.selectFuncStruct.durArray, 890 }, 891 }) 892 ); 893 } 894 this.hideTip(); 895 } else { 896 // mouse right button 897 if (ChartStruct.hoverFuncStruct) { 898 ChartStruct.tempSelectStruct = ChartStruct.hoverFuncStruct; 899 } 900 } 901 } 902 903 private hideTip(): void { 904 if (this.floatHint) { 905 this.floatHint.style.display = 'none'; 906 } 907 } 908 909 /** 910 * 更新悬浮框内容 911 */ 912 private updateTipContent(): void { 913 const hoverNode = ChartStruct.hoverFuncStruct; 914 if (hoverNode) { 915 const name = hoverNode?.symbol.replace(/</g, '<').replace(/>/g, '>').split(' (')[0]; 916 const percent = ((hoverNode?.percent || 0) * 100).toFixed(2); 917 const threadPercent = this.getCurrentPercentOfThread(hoverNode); 918 const processPercent = this.getCurrentPercentOfProcess(hoverNode); 919 switch (this._mode) { 920 case ChartMode.Byte: 921 const size = Utils.getByteWithUnit(this.getNodeValue(hoverNode)); 922 const countPercent = ((this.getNodeValue(hoverNode) / this.total) * 100).toFixed(2); 923 this.hintContent = ` 924 <span class="bold">Symbol: </span> <span class="text">${name} </span> <br> 925 <span class="bold">Lib: </span> <span class="text">${hoverNode?.lib}</span> <br> 926 <span class="bold">Addr: </span> <span>${hoverNode?.addr}</span> <br> 927 <span class="bold">Size: </span> <span>${size} (${percent}%) </span> <br> 928 <span class="bold">Count: </span> <span>${hoverNode?.count} (${countPercent}%)</span>`; 929 break; 930 case ChartMode.Duration: 931 const duration = Utils.getProbablyTime(this.getNodeValue(hoverNode)); 932 this.hintContent = ` 933 <span class="bold">Name: </span> <span class="text">${name} </span> <br> 934 <span class="bold">Lib: </span> <span class="text">${hoverNode?.lib}</span> <br> 935 <span class="bold">Addr: </span> <span>${hoverNode?.addr}</span> <br> 936 <span class="bold">Duration: </span> <span>${duration}</span>`; 937 break; 938 case ChartMode.EventCount: 939 case ChartMode.Count: 940 const label = ChartMode.Count === this._mode ? 'Count' : 'EventCount'; 941 const count = this.getNodeValue(hoverNode); 942 let sourceHint = ''; 943 if (hoverNode.sourceFile) { 944 const lines = Array.from(hoverNode.lineNumber).sort((a, b) => a - b).join(','); 945 sourceHint = `<span class="bold">Source: </span> <span class="text">${hoverNode?.sourceFile} : ${lines}</span> <br>`; 946 } 947 this.hintContent = ` 948 <span class="bold">Name: </span> <span class="text">${name} </span> <br> 949 <span class="bold">Lib: </span> <span class="text">${hoverNode?.lib}</span> <br> 950 <span class="bold">Addr: </span> <span>${hoverNode?.addr}</span> <br> 951 ${sourceHint} 952 <span class="bold">${label}: </span> <span class="count" jump> ${count}</span>`; 953 break; 954 } 955 if (this._mode !== ChartMode.Byte) { 956 if (threadPercent) { 957 this.hintContent += `<br> <span class="bold">% in current Thread:</span> <span>${threadPercent}%</span>`; 958 } 959 if (processPercent) { 960 this.hintContent += `<br> <span class="bold">% in current Process:</span> <span>${processPercent}%</span>`; 961 } 962 this.hintContent += `<br> <span class="bold">% in all Process: </span> <span> ${percent}%</span>`; 963 } 964 } 965 } 966 967 private getCurrentPercent(node: ChartStruct, isThread: boolean): string { 968 const parentNode = this.findCurrentNode(node, isThread); 969 if (parentNode) { 970 return ((this.getNodeValue(node) / this.getNodeValue(parentNode)) * 100).toFixed(2); 971 } 972 return ''; 973 } 974 975 private findCurrentNode(node: ChartStruct, isThread: boolean): ChartStruct | null { 976 while (node.parent) { 977 if ((isThread && node.parent.isThread) || (!isThread && node.parent.isProcess)) { 978 return node.parent; 979 } 980 node = node.parent; 981 } 982 return null; 983 } 984 985 private getCurrentPercentOfThread(node: ChartStruct): string { 986 return this.getCurrentPercent(node, true); 987 } 988 989 private getCurrentPercentOfProcess(node: ChartStruct): string { 990 return this.getCurrentPercent(node, false); 991 } 992 993 /** 994 * mouse on canvas move event 995 */ 996 private onMouseMove(): void { 997 const lastNode = ChartStruct.hoverFuncStruct; 998 // 鼠标移动到root节点不作显示 999 const hoverRootNode = this.rootNode.frame?.contains(this.canvasX, this.canvasY); 1000 if (hoverRootNode) { 1001 ChartStruct.hoverFuncStruct = this.rootNode; 1002 return; 1003 } 1004 // 查找鼠标所在那个node上 1005 const searchResult = this.searchDataByCoord(this.currentData!, this.canvasX, this.canvasY); 1006 if (searchResult && (searchResult.isDraw || searchResult.depth === 0)) { 1007 ChartStruct.hoverFuncStruct = searchResult; 1008 if (ChartStruct.hoverFuncStruct !== ChartStruct.tempSelectStruct) { 1009 ChartStruct.tempSelectStruct = undefined; 1010 } 1011 // 悬浮的node未改变,不需要更新悬浮框文字信息,不绘图 1012 if (searchResult !== lastNode) { 1013 this.updateTipContent(); 1014 this.calculateChartData(); 1015 } 1016 if (ChartStruct.tempSelectStruct === undefined) { 1017 this.showTip(); 1018 } 1019 } else { 1020 this.hideTip(); 1021 ChartStruct.tempSelectStruct = undefined; 1022 ChartStruct.hoverFuncStruct = undefined; 1023 } 1024 } 1025 1026 /** 1027 * 监听页面Size变化 1028 */ 1029 private listenerResize(): void { 1030 new ResizeObserver(() => { 1031 this.resizeChange(); 1032 if (this.rootNode && this.canvas.clientWidth !== 0 && this.xPoint === 0) { 1033 this.rootNode.frame!.width = this.canvas.clientWidth; 1034 } 1035 }).observe(this); 1036 } 1037 1038 public resizeChange(): void { 1039 if (this.canvas!.getBoundingClientRect()) { 1040 const box = this.canvas!.getBoundingClientRect(); 1041 const element = document.documentElement; 1042 this.startX = box.left + Math.max(element.scrollLeft, document.body.scrollLeft) - element.clientLeft; 1043 this.startY = 1044 box.top + Math.max(element.scrollTop, document.body.scrollTop) - element.clientTop + this.canvasScrollTop; 1045 } 1046 } 1047 1048 public initElements(): void { 1049 this.canvas = this.shadowRoot!.querySelector('#canvas')!; 1050 this.canvasContext = this.canvas.getContext('2d')!; 1051 this.floatHint = this.shadowRoot?.querySelector('#float_hint'); 1052 this.canvas!.oncontextmenu = (): boolean => { 1053 return false; 1054 }; 1055 this.canvas!.onmouseup = (e): void => { 1056 this.onMouseClick(e); 1057 }; 1058 1059 this.canvas!.onmousemove = (e): void => { 1060 if (!this.isUpdateCanvas) { 1061 this.updateCanvasCoord(); 1062 } 1063 this.canvasX = e.clientX - this.startX; 1064 this.canvasY = e.clientY - this.startY + this.canvasScrollTop; 1065 this.isFocusing = true; 1066 this.onMouseMove(); 1067 }; 1068 1069 this.canvas!.onmouseleave = (): void => { 1070 if (!ChartStruct.tempSelectStruct) { 1071 this.isFocusing = false; 1072 this.hideTip(); 1073 } 1074 }; 1075 document.addEventListener('keydown', (e) => { 1076 if (!this.isFocusing) { 1077 return; 1078 } 1079 switch (e.key.toLocaleLowerCase()) { 1080 case 'w': 1081 this.scale(1); 1082 break; 1083 case 's': 1084 this.scale(-1); 1085 break; 1086 case 'a': 1087 this.translation(-1); 1088 break; 1089 case 'd': 1090 this.translation(1); 1091 break; 1092 } 1093 }); 1094 1095 document.addEventListener('keydown', (e) => { 1096 if (!ChartStruct.hoverFuncStruct || !this.isFocusing) { 1097 return; 1098 } 1099 if (e.ctrlKey && e.key.toLocaleLowerCase() === 'c') { 1100 let hoverName: string = ChartStruct.hoverFuncStruct!.symbol.split(' (')[0]; 1101 navigator.clipboard.writeText(hoverName); 1102 } 1103 }); 1104 this.listenerResize(); 1105 } 1106 1107 public initHtml(): string { 1108 return ` 1109 <style> 1110 .frame-tip{ 1111 position:absolute; 1112 left: 0; 1113 background-color: white; 1114 border: 1px solid #f9f9f9; 1115 width: auto; 1116 font-size: 12px; 1117 color: #50809e; 1118 padding: 2px 10px; 1119 display: none; 1120 max-width:400px; 1121 } 1122 .bold{ 1123 font-weight: bold; 1124 } 1125 .count { 1126 text-decoration: underline; 1127 cursor: pointer 1128 } 1129 .text{ 1130 max-width:350px; 1131 word-break: break-all; 1132 } 1133 :host{ 1134 display: flex; 1135 padding: 10px 10px; 1136 } 1137 </style> 1138 <canvas id="canvas"></canvas> 1139 <div id ="float_hint" class="frame-tip"></div>`; 1140 } 1141} 1142