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