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