* Copyright (C) 2022 Huawei Device Co., Ltd.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { BaseElement, element } from '../../../base-ui/BaseElement';
import { Rect } from '../trace/timer-shaft/Rect';
import { ChartMode, ChartStruct, draw, setFuncFrame, splitSymbol } from '../../bean/FrameChartStruct';
import { SpApplication } from '../../SpApplication';
import { Utils } from '../trace/base/Utils';
import { TabPaneFilter } from '../trace/sheet/TabPaneFilter';
const scaleHeight = 30;
const depthHeight = 20;
const filterPixel = 2;
const textMaxWidth = 50;
const scaleRatio = 0.2;
const ms10 = 10_000_000;
const jsHapKeys = ['.hap', '.hsp', '.har'];
const jsStackPath = ['.ts', '.ets', '.js'];
const textStyle = '12px bold';
const asyncStack = {
'libark_jsruntime.so': ['AsyncStackTraceManager::SavePromiseNode'],
'libtaskpool.z.so': ['Task::Task'],
'libworker.z.so': ['Worker::Worker'],
'libffrt.so': ['ffrt_submit_base', 'ffrt::QueueHandler::Dispatch'],
'libeventhandler.z.so': ['sendEvent', 'DistributeEvent'],
'libuv.so': ['uv_timer_start', 'uv_queue_work'],
'libarkweb_engine.so': ['Runtime_DfxPromiseThen', 'Runtime_SetAsyncTraceOfMicrotask'],
}
class NodeValue {
size: number;
count: number;
dur: number;
eventCount: number;
constructor() {
this.size = 0;
this.count = 0;
this.dur = 0;
this.eventCount = 0;
}
}
@element('tab-framechart')
export class FrameChart extends BaseElement {
private canvas!: HTMLCanvasElement;
private canvasContext!: CanvasRenderingContext2D;
private floatHint!: HTMLDivElement | undefined | null;
private rect: Rect = new Rect(0, 0, 0, 0);
private _mode = ChartMode.Byte;
private startX = 0;
private startY = 0;
private canvasX = -1;
private canvasY = -1;
private hintContent = '';
private rootNode!: ChartStruct;
private currentData: Array<ChartStruct> = [];
private xPoint = 0;
private isFocusing = false;
private canvasScrollTop = 0;
private _maxDepth = 0;
private chartClickListenerList: Array<Function> = [];
private isUpdateCanvas = false;
private isClickMode = false;
_totalRootData: Array<ChartStruct> = [];
private totalRootNode!: ChartStruct;
private tabPaneFilter: TabPaneFilter | undefined;
private _isShow: boolean = true;
* set chart mode
* @param mode chart format for data mode
*/
set mode(mode: ChartMode) {
this._mode = mode;
}
set data(val: Array<ChartStruct>) {
this.tabPaneFilter = document
.querySelector('body > sp-application')
?.shadowRoot?.querySelector('#sp-system-trace')
?.shadowRoot?.querySelector('div > trace-sheet')
?.shadowRoot?.querySelector('#box-native-calltree > tabpane-nm-calltree')
?.shadowRoot?.querySelector('#nm-call-tree-filter') as TabPaneFilter;
ChartStruct.lastSelectFuncStruct = undefined;
this.setSelectStatusRecursive(ChartStruct.selectFuncStruct, true);
ChartStruct.selectFuncStruct = undefined;
this.isClickMode = false;
this.currentData = val;
this.resetTrans();
this.calDrawArgs(true);
}
set tabPaneScrollTop(scrollTop: number) {
this.canvasScrollTop = scrollTop;
this.hideTip();
}
get totalRootData(): Array<ChartStruct> {
return this._totalRootData;
}
set totalRootData(value: Array<ChartStruct>) {
this._totalRootData = value;
}
private get total(): number {
return this.getNodeValue(this.rootNode);
}
private getNodeValue(node: ChartStruct): number {
let result: number;
switch (this._mode) {
case ChartMode.Byte:
result = node.drawSize || node.size;
break;
case ChartMode.Count:
result = node.drawCount || node.count;
break;
case ChartMode.Duration:
result = node.drawDur || node.dur;
break;
case ChartMode.EventCount:
result = node.drawEventCount || node.eventCount;
break;
}
return result;
}
* add callback of chart click
* @param callback function of chart click
*/
public addChartClickListener(callback: Function): void {
if (this.chartClickListenerList.indexOf(callback) < 0) {
this.chartClickListenerList.push(callback);
}
}
* remove callback of chart click
* @param callback function of chart click
*/
public removeChartClickListener(callback: Function): void {
const index = this.chartClickListenerList.indexOf(callback);
if (index > -1) {
this.chartClickListenerList.splice(index, 1);
}
}
private createRootNode(): void {
this.rootNode = new ChartStruct();
this.rootNode.symbol = 'root';
this.rootNode.depth = 0;
this.rootNode.percent = 1;
this.rootNode.frame = new Rect(0, scaleHeight, this.canvas!.width, depthHeight);
for (const node of this.currentData!) {
this.rootNode.children.push(node);
this.rootNode.count += node.drawCount || node.count;
this.rootNode.size += node.drawSize || node.size;
this.rootNode.dur += node.drawDur || node.dur;
this.rootNode.eventCount += node.drawEventCount || node.eventCount;
node.parent = this.rootNode;
}
this.totalRootNode = new ChartStruct();
this.totalRootNode.symbol = 'root';
this.totalRootNode.depth = 0;
this.totalRootNode.percent = 1;
this.totalRootNode.frame = new Rect(0, scaleHeight, this.canvas!.width, depthHeight);
for (const node of this._totalRootData!) {
this.totalRootNode.children.push(node);
this.totalRootNode.count += node.drawCount || node.count;
this.totalRootNode.size += node.drawSize || node.size;
this.totalRootNode.dur += node.drawDur || node.dur;
this.totalRootNode.eventCount += node.drawEventCount || node.eventCount;
node.parent = this.totalRootNode;
}
}
* 1.计算调用栈最大深度
* 2.计算搜索情况下每个函数块显示的大小(非实际大小)
* 3.计算点选情况下每个函数块的显示大小(非实际大小)
* @param initRoot 是否初始化root节点
*/
private calDrawArgs(initRoot: boolean): void {
this._maxDepth = 0;
if (initRoot) {
this.createRootNode();
}
this.initData(this.rootNode, 0, true);
this.selectInit();
this.setRootValue();
this.rect.width = this.canvas!.width;
this.rect.height = (this._maxDepth + 1) * depthHeight + scaleHeight;
this.canvas!.style.height = `${this.rect!.height}px`;
this.canvas!.height = Math.ceil(this.rect!.height);
}
* 点选情况下由点选来设置每个函数的显示Size
*/
private selectInit(): void {
const node = ChartStruct.selectFuncStruct;
if (node) {
const module = new NodeValue();
node.drawCount = 0;
node.drawDur = 0;
node.drawSize = 0;
node.drawEventCount = 0;
for (let child of node.children) {
node.drawCount += child.searchCount;
node.drawDur += child.searchDur;
node.drawSize += child.searchSize;
node.drawEventCount += child.searchEventCount;
}
module.count = node.drawCount = node.drawCount || node.count;
module.dur = node.drawDur = node.drawDur || node.dur;
module.size = node.drawSize = node.drawSize || node.size;
module.eventCount = node.drawEventCount = node.drawEventCount || node.eventCount;
this.setParentDisplayInfo(node, module, true);
this.setChildrenDisplayInfo(node);
this.clearOtherDisplayInfo(this.rootNode);
}
}
private clearOtherDisplayInfo(node: ChartStruct): void {
for (const children of node.children) {
if (children.isChartSelect) {
children.isChartSelect = false;
this.clearOtherDisplayInfo(children);
continue;
}
children.drawCount = 0;
children.drawEventCount = 0;
children.drawSize = 0;
children.drawDur = 0;
this.clearOtherDisplayInfo(children);
}
}
private setRootValue(): void {
let currentValue = '';
let currentValuePercent = 1;
switch (this._mode) {
case ChartMode.Byte:
currentValue = Utils.getBinaryByteWithUnit(this.total);
currentValuePercent = this.total / this.rootNode.size;
break;
case ChartMode.Count:
currentValue = `${this.total}`;
currentValuePercent = this.total / this.totalRootNode.count;
break;
case ChartMode.Duration:
currentValue = Utils.getProbablyTime(this.total);
currentValuePercent = this.total / this.rootNode.dur;
break;
case ChartMode.EventCount:
currentValue = `${this.total}`;
currentValuePercent = this.total / this.totalRootNode.eventCount;
break;
}
let endStr = currentValuePercent ? ` (${(currentValuePercent * 100).toFixed(2)}%)` : '';
this.rootNode.symbol = `Root : ${currentValue}${endStr}`;
}
* 判断lib中是否包含.ts .ets .js .hap
* @param str node.lib
* @returns 是否包含
*/
private isJsStack(str: string): boolean {
let keyList = jsStackPath;
if (this._mode === ChartMode.Count || this._mode === ChartMode.EventCount) {
keyList = jsStackPath.concat(jsHapKeys);
}
for (const format of keyList) {
if (str.indexOf(format) > 0) {
return true;
}
}
return false;
}
* 判断lib中是否具体so文件
* @param str node.lib
* @param symbol node.symbol
* @returns 是否包含
*/
private isExpectedFunction(str: string, symbol: string): boolean {
const keyList = Object.keys(asyncStack);
if (keyList.includes(str)) {
const funcList = (asyncStack as Record<string, string[]>)[str];
if (funcList.includes(symbol)) {
return true;
}
}
return false;
}
private clearSuperfluousParams(node: ChartStruct): void {
node.id = undefined;
node.eventType = undefined;
node.parentId = undefined;
node.title = undefined;
node.eventType = undefined;
if (this.mode === ChartMode.Byte) {
node.self = undefined;
node.eventCount = 0;
}
if (this._mode !== ChartMode.Count && this._mode !== ChartMode.EventCount) {
node.eventCount = 0;
node.eventPercent = undefined;
}
}
* 计算调用栈最大深度,计算每个node显示大小
* @param node 函数块
* @param depth 当前递归深度
* @param calDisplay 该层深度是否需要计算显示大小
*/
private initData(node: ChartStruct, depth: number, calDisplay: boolean): void {
node.depth = depth;
depth++;
this.clearSuperfluousParams(node);
if (this.isJsStack(node.lib)) {
node.isJsStack = true;
} else {
node.isJsStack = false;
}
node.isAsyncStack = this.isExpectedFunction(node.lib, splitSymbol(node));
this.clearDisplayInfo(node);
if ((node.isSearch && calDisplay) || (node.children && calDisplay && node.children.length === 0 && node.isReverseFilter)) {
const module = new NodeValue();
module.size = node.drawSize = node.searchSize = node.size;
module.count = node.drawCount = node.searchCount = node.count;
module.dur = node.drawDur = node.searchDur = node.dur;
module.eventCount = node.drawEventCount = node.searchEventCount = node.eventCount;
this.setParentDisplayInfo(node, module, false);
calDisplay = false;
}
if (node.children && node.children.length > 0) {
for (const children of node.children) {
children.parent = node;
this.initData(children, depth, calDisplay);
}
} else {
this._maxDepth = Math.max(depth, this._maxDepth);
}
}
private setParentDisplayInfo(node: ChartStruct, module: NodeValue, isSelect?: boolean): void {
const parent = node.parent;
if (parent) {
if (isSelect) {
parent.isChartSelect = true;
parent.isChartSelectParent = true;
parent.drawCount = module.count;
parent.drawDur = module.dur;
parent.drawSize = module.size;
parent.drawEventCount = module.eventCount;
} else {
parent.searchCount += module.count;
parent.searchDur += module.dur;
parent.searchSize += module.size;
parent.searchEventCount += module.eventCount;
if (!this.isClickMode) {
parent.drawDur = parent.searchDur;
parent.drawCount = parent.searchCount;
parent.drawSize = parent.searchSize;
parent.drawEventCount = parent.searchEventCount;
}
}
this.setParentDisplayInfo(parent, module, isSelect);
}
}
* 点击与搜索同时触发情况下,由点击去设置绘制大小
* @param node 当前点选的函数
* @returns void
*/
private setChildrenDisplayInfo(node: ChartStruct): void {
if (node.children.length < 0) {
return;
}
for (const children of node.children) {
children.drawCount = children.searchCount || children.count;
children.drawDur = children.searchDur || children.dur;
children.drawSize = children.searchSize || children.size;
children.drawEventCount = children.searchEventCount || children.eventCount;
this.setChildrenDisplayInfo(children);
}
}
private clearDisplayInfo(node: ChartStruct): void {
node.drawCount = 0;
node.drawDur = 0;
node.drawSize = 0;
node.drawEventCount = 0;
node.searchCount = 0;
node.searchDur = 0;
node.searchSize = 0;
node.searchEventCount = 0;
}
* 计算每个函数块的坐标信息以及绘制火焰图
*/
public async calculateChartData(): Promise<void> {
this.clearCanvas();
this.canvasContext?.beginPath();
this.canvasContext.font = textStyle;
this.drawCalibrationTails();
draw(this.canvasContext, this.rootNode);
this.setFrameData(this.rootNode);
this.drawFrameChart(this.rootNode);
this.canvasContext?.closePath();
}
* 清空画布
*/
public clearCanvas(): void {
this.canvasContext?.clearRect(0, 0, this.canvas!.width, this.canvas!.height);
}
* 在窗口大小变化时调整画布大小
*/
public updateCanvas(updateWidth: boolean, newWidth?: number): void {
if (this.canvas instanceof HTMLCanvasElement) {
this.canvas.style.width = `${100}%`;
this.canvas.style.height = `${this.rect!.height}px`;
if (this.canvas.clientWidth === 0 && newWidth) {
this.canvas.width = newWidth - depthHeight * 2;
} else {
this.canvas.width = this.canvas.clientWidth;
}
this.canvas.height = Math.ceil(this.rect!.height);
this.updateCanvasCoord();
}
if (
this.rect.width === 0 ||
updateWidth ||
Math.round(newWidth!) !== this.canvas!.width + depthHeight * 2 ||
newWidth! > this.rect.width
) {
this.rect.width = this.canvas!.width;
}
}
* 更新画布坐标
*/
private updateCanvasCoord(): void {
if (this.canvas instanceof HTMLCanvasElement) {
this.isUpdateCanvas = this.canvas.clientWidth !== 0;
if (this.canvas.getBoundingClientRect()) {
const box = this.canvas.getBoundingClientRect();
const D = document.documentElement;
this.startX = box.left + Math.max(D.scrollLeft, document.body.scrollLeft) - D.clientLeft;
this.startY = box.top + Math.max(D.scrollTop, document.body.scrollTop) - D.clientTop + this.canvasScrollTop;
}
}
}
* 绘制刻度尺,分为100段,每10段画一条长线
*/
private drawCalibrationTails(): void {
const spApplication = <SpApplication>document.getElementsByTagName('sp-application')[0];
this.canvasContext!.lineWidth = 0.5;
this.canvasContext?.moveTo(0, 0);
this.canvasContext?.lineTo(this.canvas!.width, 0);
for (let i = 0; i <= 10; i++) {
let startX = Math.floor((this.canvas!.width / 10) * i);
for (let j = 0; j < 10; j++) {
this.canvasContext!.lineWidth = 0.5;
const startItemX = startX + Math.floor((this.canvas!.width / 100) * j);
this.canvasContext?.moveTo(startItemX, 0);
this.canvasContext?.lineTo(startItemX, 10);
}
if (i === 0) {
continue;
}
this.canvasContext!.lineWidth = 1;
const sizeRatio = this.canvas!.width / this.rect.width;
if (spApplication.dark) {
this.canvasContext!.strokeStyle = '#888';
} else {
this.canvasContext!.strokeStyle = '#ddd';
}
this.canvasContext?.moveTo(startX, 0);
this.canvasContext?.lineTo(startX, this.canvas!.height);
if (spApplication.dark) {
this.canvasContext!.fillStyle = '#fff';
} else {
this.canvasContext!.fillStyle = '#000';
}
let calibration = '';
switch (this._mode) {
case ChartMode.Byte:
calibration = Utils.getByteWithUnit(((this.total * sizeRatio) / 10) * i);
break;
case ChartMode.Duration:
calibration = Utils.getProbablyTime(((this.total * sizeRatio) / 10) * i);
break;
case ChartMode.EventCount:
case ChartMode.Count:
calibration = `${Math.ceil(((this.total * sizeRatio) / 10) * i)}`;
break;
}
const size = this.canvasContext!.measureText(calibration).width;
this.canvasContext?.fillText(calibration, startX - size - 5, depthHeight, textMaxWidth);
this.canvasContext?.stroke();
}
}
* 设置每个node的宽高,开始坐标
* @param node 函数块
*/
private setFrameData(node: ChartStruct): void {
if (node.children.length > 0) {
for (const children of node.children) {
node.isDraw = false;
if (this.isClickMode && ChartStruct.selectFuncStruct) {
if (!children.isChartSelect) {
if (children.frame) {
children.frame.x = this.rootNode.frame?.x || 0;
children.frame.width = 0;
children.percent = 0;
} else {
children.frame = new Rect(0, 0, 0, 0);
}
this.setFrameData(children);
continue;
}
}
const childrenValue = this.getNodeValue(children);
setFuncFrame(children, this.rect, this.total, this._mode);
children.percent = childrenValue / this.total;
this.setFrameData(children);
}
}
}
* 计算有效数据,当node的宽度太小不足以绘制时
* 计算忽略node的size
* 忽略的size将转换成width,按照比例平摊到显示的node上
* @param node 当前node
* @param effectChildList 生效的node
*/
private calEffectNode(node: ChartStruct, effectChildList: Array<ChartStruct>): number {
const ignore = new NodeValue();
for (const children of node.children) {
if (children.frame!.width >= filterPixel) {
effectChildList.push(children);
} else {
if (node.isChartSelect || this.isSearch(node)) {
ignore.size += children.drawSize;
ignore.count += children.drawCount;
ignore.dur += children.drawDur;
ignore.eventCount += children.drawEventCount;
} else {
ignore.size += children.size;
ignore.count += children.count;
ignore.dur += children.dur;
ignore.eventCount += children.eventCount;
}
}
}
let result: number = 0;
switch (this._mode) {
case ChartMode.Byte:
result = ignore.size;
break;
case ChartMode.Count:
result = ignore.count;
break;
case ChartMode.Duration:
result = ignore.dur;
break;
case ChartMode.EventCount:
result = ignore.eventCount;
break;
}
return result;
}
private isSearch(node: ChartStruct): boolean {
let result: boolean = false;
switch (this._mode) {
case ChartMode.Byte:
result = node.searchSize > 0;
break;
case ChartMode.Count:
result = node.searchCount > 0;
break;
case ChartMode.Duration:
result = node.searchDur > 0;
break;
case ChartMode.EventCount:
result = node.searchEventCount > 0;
break;
}
return result;
}
* 绘制每个函数色块
* @param node 函数块
*/
private drawFrameChart(node: ChartStruct): void {
const effectChildList: Array<ChartStruct> = [];
const nodeValue = this.getNodeValue(node);
if (node.children && node.children.length > 0) {
const ignoreValue = this.calEffectNode(node, effectChildList);
let x = node.frame!.x;
if (effectChildList.length > 0) {
for (let children of effectChildList) {
children.frame!.x = x;
const childrenValue = this.getNodeValue(children);
children.frame!.width = (childrenValue / (nodeValue - ignoreValue)) * node.frame!.width;
x += children.frame!.width;
if (this.nodeInCanvas(children)) {
draw(this.canvasContext!, children);
this.drawFrameChart(children);
}
}
} else {
const firstChildren = node.children[0];
firstChildren.frame!.x = node.frame!.x;
firstChildren.frame!.width = node.frame!.width * (ignoreValue / nodeValue);
draw(this.canvasContext!, firstChildren);
this.drawFrameChart(firstChildren);
}
}
}
* 根据鼠标当前的坐标递归查找对应的函数块
*
* @param nodes
* @param canvasX 鼠标相对于画布开始点的x坐标
* @param canvasY 鼠标相对于画布开始点的y坐标
* @returns 当前鼠标位置的函数块
*/
private searchDataByCoord(nodes: Array<ChartStruct>, canvasX: number, canvasY: number): ChartStruct | null {
for (const node of nodes) {
if (node.frame?.contains(canvasX, canvasY)) {
return node;
} else {
const result = this.searchDataByCoord(node.children, canvasX, canvasY);
if (!result) {
continue;
}
return result;
}
}
return null;
}
* 显示悬浮框信息,更新位置
*/
private showTip(): void {
this.floatHint!.innerHTML = this.hintContent;
this.floatHint!.style.display = 'block';
this.floatHint!.style.zIndex = '9999999';
const countSpan = this.floatHint!.querySelector('.count');
if (countSpan) {
countSpan.onclick = (e) => {
this.dispatchEvent(
new CustomEvent('td-click', {
detail: ChartStruct.tempSelectStruct,
composed: true,
})
);
e.stopPropagation();
};
}
let tipArea =
this.tabPaneFilter?.getBoundingClientRect().top! -
this.canvas.getBoundingClientRect().top -
this.canvasScrollTop -
scaleHeight;
let x = this.canvasX;
let y = this.canvasY - this.canvasScrollTop;
if (this.canvasX + this.floatHint!.clientWidth > (this.canvas?.clientWidth || 0)) {
x -= this.floatHint!.clientWidth - 1;
} else {
x += scaleHeight;
}
if (y > this.floatHint!.clientHeight || y + this.floatHint!.clientHeight > tipArea) {
y -= this.floatHint!.clientHeight - 1;
}
this.floatHint!.style.transform = `translate(${x}px,${y}px)`;
}
* 递归设置传入node的parent以及children的isSelect
* 将上次点选的整条树的isSelect置为false
* 将本次点击的整条树的isSelect置为true
* @param node 点击的node
* @param isSelect 点选
*/
private setSelectStatusRecursive(node: ChartStruct | undefined, isSelect: boolean): void {
if (!node) {
return;
}
node.isChartSelect = isSelect;
const stack: ChartStruct[] = [node];
while (stack.length > 0) {
const currentNode = stack.pop();
if (currentNode) {
currentNode.children.forEach((child) => {
child.isChartSelect = isSelect;
stack.push(child);
});
}
}
while (node?.parent) {
node.parent.isChartSelect = isSelect;
node.parent.isChartSelectParent = isSelect;
node = node.parent;
}
}
* 点选后重绘火焰图
*/
private clickRedraw(): void {
if (ChartStruct.lastSelectFuncStruct) {
this.setSelectStatusRecursive(ChartStruct.lastSelectFuncStruct!, false);
}
this.calDrawArgs(false);
this.setSelectStatusRecursive(ChartStruct.selectFuncStruct!, true);
this.calculateChartData();
}
* 点击w s的放缩算法
* @param index < 0 缩小 , > 0 放大
*/
private scale(index: number): void {
let newWidth = 0;
let deltaWidth = this.rect!.width * scaleRatio;
const ratio = 1 + scaleRatio;
if (index > 0) {
newWidth = this.rect!.width + deltaWidth;
const sizeRatio = this.canvas!.width / this.rect.width;
switch (this._mode) {
case ChartMode.Byte:
case ChartMode.Count:
case ChartMode.EventCount:
if (Math.round((this.total * sizeRatio) / ratio) <= 10) {
if (this.xPoint === 0) {
return;
}
newWidth = this.canvas!.width / (10 / this.total);
}
break;
case ChartMode.Duration:
if (Math.round((this.total * sizeRatio) / ratio) <= ms10) {
if (this.xPoint === 0) {
return;
}
newWidth = this.canvas!.width / (ms10 / this.total);
}
break;
}
deltaWidth = newWidth - this.rect!.width;
} else {
newWidth = this.rect!.width - deltaWidth;
if (newWidth < this.canvas!.width) {
newWidth = this.canvas!.width;
this.resetTrans();
}
deltaWidth = this.rect!.width - newWidth;
}
if (newWidth === this.rect.width) {
return;
}
this.translationByScale(index, deltaWidth, newWidth);
}
private resetTrans(): void {
this.xPoint = 0;
}
* 放缩之后的平移算法
* @param index < 0 缩小 , > 0 放大
* @param deltaWidth 放缩增量
* @param newWidth 放缩后的宽度
*/
private translationByScale(index: number, deltaWidth: number, newWidth: number): void {
const translationValue = (deltaWidth * (this.canvasX - this.xPoint)) / this.rect.width;
if (index > 0) {
this.xPoint -= translationValue;
} else {
this.xPoint += translationValue;
}
this.rect!.width = newWidth;
this.translationDraw();
}
* 点击a d 平移
* @param index < 0 左移; >0 右移
*/
private translation(index: number): void {
const offset = this.canvas!.width / 10;
if (index < 0) {
this.xPoint += offset;
} else {
this.xPoint -= offset;
}
this.translationDraw();
}
* judge position ro fit canvas and draw
*/
private translationDraw(): void {
if (this.xPoint > 0) {
this.xPoint = 0;
}
if (this.rect.width + this.xPoint < this.canvas!.width) {
this.xPoint = this.canvas!.width - this.rect.width;
}
this.rootNode.frame!.width = this.rect.width;
this.rootNode.frame!.x = this.xPoint;
this.calculateChartData();
}
private nodeInCanvas(node: ChartStruct): boolean {
if (!node.frame) {
return false;
}
return node.frame.x + node.frame.width >= 0 && node.frame.x < this.canvas.clientWidth;
}
private onMouseClick(e: MouseEvent): void {
if (e.button === 0) {
if (ChartStruct.hoverFuncStruct && ChartStruct.hoverFuncStruct !== ChartStruct.selectFuncStruct) {
ChartStruct.lastSelectFuncStruct = ChartStruct.selectFuncStruct;
ChartStruct.selectFuncStruct = ChartStruct.hoverFuncStruct;
ChartStruct.tempSelectStruct = undefined;
this.isClickMode = ChartStruct.selectFuncStruct !== this.rootNode;
this.rect.width = this.canvas!.clientWidth;
this.resetTrans();
this.rootNode.frame!.x = this.xPoint;
this.rootNode.frame!.width = this.rect.width = this.canvas.clientWidth;
this.clickRedraw();
document.dispatchEvent(
new CustomEvent('number_calibration', {
detail: {
time: ChartStruct.selectFuncStruct.tsArray,
counts: ChartStruct.selectFuncStruct.countArray,
durations: ChartStruct.selectFuncStruct.durArray,
isShow:this._isShow
},
})
);
}
this.hideTip();
} else {
if (ChartStruct.hoverFuncStruct) {
ChartStruct.tempSelectStruct = ChartStruct.hoverFuncStruct;
}
}
}
private hideTip(): void {
if (this.floatHint) {
this.floatHint.style.display = 'none';
}
}
* 更新悬浮框内容
*/
private updateTipContent(): void {
const hoverNode = ChartStruct.hoverFuncStruct;
if (hoverNode) {
const name = hoverNode?.symbol.replace(/</g, '<').replace(/>/g, '>').split(' (')[0];
const percent = ((hoverNode?.percent || 0) * 100).toFixed(2);
const threadPercent = this.getCurrentPercentOfThread(hoverNode);
const processPercent = this.getCurrentPercentOfProcess(hoverNode);
switch (this._mode) {
case ChartMode.Byte: {
const size = Utils.getByteWithUnit(this.getNodeValue(hoverNode));
const countPercent = ((this.getNodeValue(hoverNode) / this.total) * 100).toFixed(2);
const counters = hoverNode.drawCount || hoverNode.count;
this.hintContent = `
<span class="bold">Symbol: </span> <span class="text">${name} </span> <br>
<span class="bold">Lib: </span> <span class="text">${hoverNode?.lib}</span> <br>
<span class="bold">Addr: </span> <span>${hoverNode?.addr}</span> <br>
<span class="bold">Size: </span> <span>${size} (${percent}%) </span> <br>
<span class="bold">Count: </span> <span>${counters} (${countPercent}%)</span>`;
break;
}
case ChartMode.Duration: {
const duration = Utils.getProbablyTime(this.getNodeValue(hoverNode));
this.hintContent = `
<span class="bold">Name: </span> <span class="text">${name} </span> <br>
<span class="bold">Lib: </span> <span class="text">${hoverNode?.lib}</span> <br>
<span class="bold">Addr: </span> <span>${hoverNode?.addr}</span> <br>
<span class="bold">Duration: </span> <span>${duration}</span>`;
break;
}
case ChartMode.EventCount:
case ChartMode.Count: {
const label = ChartMode.Count === this._mode ? 'Count' : 'EventCount';
const count = this.getNodeValue(hoverNode);
let sourceHint = '';
if (hoverNode.sourceFile) {
const lines = Array.from(hoverNode.lineNumber).sort((a, b) => a - b).join(',');
sourceHint = `<span class="bold">Source: </span> <span class="text">${hoverNode?.sourceFile} : ${lines}</span> <br>`;
}
this.hintContent = `
<span class="bold">Name: </span> <span class="text">${name} </span> <br>
<span class="bold">Lib: </span> <span class="text">${hoverNode?.lib}</span> <br>
<span class="bold">Addr: </span> <span>${hoverNode?.addr}</span> <br>
${sourceHint}
<span class="bold">${label}: </span> <span class="count" jump> ${(count).toLocaleString()}</span>`;
break;
}
}
if (this._mode !== ChartMode.Byte) {
if (threadPercent) {
this.hintContent += `<br> <span class="bold">% in current Thread:</span> <span>${threadPercent}%</span>`;
}
if (processPercent) {
this.hintContent += `<br> <span class="bold">% in current Process:</span> <span>${processPercent}%</span>`;
}
this.hintContent += `<br> <span class="bold">% in all Process: </span> <span> ${percent}%</span>`;
}
}
}
private getCurrentPercent(node: ChartStruct, isThread: boolean): string {
const parentNode = this.findCurrentNode(node, isThread);
if (parentNode) {
return ((this.getNodeValue(node) / this.getNodeValue(parentNode)) * 100).toFixed(2);
}
return '';
}
private findCurrentNode(node: ChartStruct, isThread: boolean): ChartStruct | null {
while (node.parent) {
if ((isThread && node.parent.isThread) || (!isThread && node.parent.isProcess)) {
return node.parent;
}
node = node.parent;
}
return null;
}
private getCurrentPercentOfThread(node: ChartStruct): string {
return this.getCurrentPercent(node, true);
}
private getCurrentPercentOfProcess(node: ChartStruct): string {
return this.getCurrentPercent(node, false);
}
* mouse on canvas move event
*/
private onMouseMove(): void {
const lastNode = ChartStruct.hoverFuncStruct;
const hoverRootNode = this.rootNode.frame?.contains(this.canvasX, this.canvasY);
if (hoverRootNode) {
ChartStruct.hoverFuncStruct = this.rootNode;
return;
}
const searchResult = this.searchDataByCoord(this.currentData!, this.canvasX, this.canvasY);
if (searchResult && (searchResult.isDraw || searchResult.depth === 0)) {
ChartStruct.hoverFuncStruct = searchResult;
if (ChartStruct.hoverFuncStruct !== ChartStruct.tempSelectStruct) {
ChartStruct.tempSelectStruct = undefined;
}
if (searchResult !== lastNode) {
this.updateTipContent();
this.calculateChartData();
}
if (ChartStruct.tempSelectStruct === undefined) {
this.showTip();
}
} else {
this.hideTip();
ChartStruct.tempSelectStruct = undefined;
ChartStruct.hoverFuncStruct = undefined;
}
}
* 监听页面Size变化
*/
private listenerResize(): void {
new ResizeObserver(() => {
this.resizeChange();
if (this.rootNode && this.canvas.clientWidth !== 0 && this.xPoint === 0) {
this.rootNode.frame!.width = this.canvas.clientWidth;
}
}).observe(this);
}
public resizeChange(): void {
if (this.canvas!.getBoundingClientRect()) {
const box = this.canvas!.getBoundingClientRect();
const element = document.documentElement;
this.startX = box.left + Math.max(element.scrollLeft, document.body.scrollLeft) - element.clientLeft;
this.startY =
box.top + Math.max(element.scrollTop, document.body.scrollTop) - element.clientTop + this.canvasScrollTop;
}
}
public initElements(): void {
this.canvas = this.shadowRoot!.querySelector('#canvas')!;
this.canvasContext = this.canvas.getContext('2d')!;
this.floatHint = this.shadowRoot?.querySelector('#float_hint');
this.canvas!.oncontextmenu = (): boolean => {
return false;
};
this.canvas!.onmouseup = (e): void => {
this.onMouseClick(e);
};
this.canvas!.onmousemove = (e): void => {
if (!this.isUpdateCanvas) {
this.updateCanvasCoord();
}
this.canvasX = e.clientX - this.startX;
this.canvasY = e.clientY - this.startY + this.canvasScrollTop;
this.isFocusing = true;
this.onMouseMove();
};
this.canvas!.onmouseleave = (): void => {
if (!ChartStruct.tempSelectStruct) {
this.isFocusing = false;
this.hideTip();
}
};
document.addEventListener('keydown', (e) => {
if (!this.isFocusing) {
return;
}
switch (e.key.toLocaleLowerCase()) {
case 'w':
this.scale(1);
break;
case 's':
this.scale(-1);
break;
case 'a':
this.translation(-1);
break;
case 'd':
this.translation(1);
break;
}
});
document.addEventListener('keydown', (e) => {
if (!ChartStruct.hoverFuncStruct || !this.isFocusing) {
return;
}
if (e.ctrlKey && e.key.toLocaleLowerCase() === 'c') {
let hoverName: string = ChartStruct.hoverFuncStruct!.symbol.split(' (')[0];
navigator.clipboard.writeText(hoverName);
}
});
this.listenerResize();
this.addEventListener('show-calibration', (e: Event) => {
const customEvent = e as CustomEvent<{ isShow: boolean }>;
this._isShow = customEvent.detail.isShow;
});
}
public initHtml(): string {
return `
<style>
.frame-tip{
position:absolute;
left: 0;
background-color: white;
border: 1px solid #f9f9f9;
width: auto;
font-size: 12px;
color: #50809e;
padding: 2px 10px;
display: none;
max-width:400px;
}
.bold{
font-weight: bold;
}
.count {
text-decoration: underline;
cursor: pointer
}
.text{
max-width:350px;
word-break: break-all;
}
:host{
display: flex;
padding: 10px 10px;
}
</style>
<canvas id="canvas"></canvas>
<div id ="float_hint" class="frame-tip"></div>`;
}
}