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