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