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