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