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