• 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.js";
17import { NativeHookCallInfo } from "../bean/NativeHook.js";
18import { ChartMode, ChartStruct, Rect } from "../database/ProcedureWorkerCommon.js";
19import { SpApplication } from "../SpApplication.js";
20import { Utils } from "./trace/base/Utils.js";
21
22const TAG: string = "FrameChart";
23const scaleHeight = 30;
24const depthHeight = 20;
25const filterPixiel = 2;
26const sideLenght = 8;
27
28@element('tab-framechart')
29export class FrameChart extends BaseElement {
30    private canvas: HTMLCanvasElement | undefined | null;
31    private cavasContext: CanvasRenderingContext2D | undefined | null;
32    private floatHint: HTMLDivElement | undefined | null;
33
34    private rect: Rect = new Rect(0, 0, 0, 0);
35    private _mode = ChartMode.Call;
36    private startX = 0; // canvas start x coord
37    private startY = 0; // canvas start y coord
38    private canvasX = -1; // canvas current x
39    private canvasY = -1; // canvas current y
40    private hintContent = ""; // float hoint inner html content
41
42    private historyList: Array<Array<ChartStruct>> = [];
43    private currentSize = 0;
44    private currentCount = 0;
45    private currentData: Array<ChartStruct> = [];
46    private xPoint = 0; // x in rect
47    private isFocusing = false;
48    private canvasScrollTop = 0;
49    private _maxDepth = 0;
50    private chartClickListenerList: Array<Function> = [];
51    private isUpdateCanvas = false;
52
53    static get observedAttributes() {
54        return []
55    }
56
57    /**
58     * set chart mode
59     * @param mode chart format for data mode
60     */
61    set mode(mode: ChartMode) {
62        this._mode = mode;
63    }
64
65    set data(val: Array<ChartStruct> | any) {
66        this.historyList = [];
67        ChartStruct.lastSelectFuncStruct = undefined;
68        this.currentData = val;
69        this.resetTrans();
70        this.caldrawArgs();
71        for (let callback of this.chartClickListenerList) {
72            callback(true);
73        }
74
75    }
76
77    set tabPaneScrollTop(scrollTop: number) {
78        this.canvasScrollTop = scrollTop;
79        this.hideFloatHint();
80    }
81
82    /**
83     * add callback of chart click
84     * @param callback function of chart click
85     */
86    public addChartClickListener(callback: Function) {
87        if (this.chartClickListenerList.indexOf(callback) < 0) {
88            this.chartClickListenerList.push(callback);
89        }
90    }
91
92    /**
93     * remove callback of chart click
94     * @param callback function of chart click
95     */
96    public removeChartClickListener(callback: Function) {
97        let index = this.chartClickListenerList.indexOf(callback);
98        if (index > -1) {
99            this.chartClickListenerList.splice(index, 1);
100        }
101    }
102
103    /**
104     * cal total count size and max Depth
105     */
106    private caldrawArgs(): void {
107        this.currentCount = 0;
108        this.currentSize = 0;
109        this._maxDepth = 0;
110        for (let rootNode of this.currentData!) {
111            this.currentCount += rootNode.count;
112            this.currentSize += rootNode.size;
113            let depth = 0;
114            this.calMaxDepth(rootNode, depth);
115        }
116        this.rect.width = this.canvas!.width
117        this.rect.height = (this._maxDepth + 1) * 20 + scaleHeight; // 20px/depth and 30 is scale height
118        this.canvas!.style.height = this.rect!.height + "px";
119        this.canvas!.height = Math.ceil(this.rect!.height);
120    }
121
122    /**
123     * cal max Depth
124     * @param node every child node
125     * @param depth current depth
126     */
127    private calMaxDepth(node: ChartStruct, depth: number): void {
128        node.depth = depth;
129        depth++;
130        if (node.children && node.children.length > 0) {
131            for (let children of node.children) {
132                this.calMaxDepth(children, depth);
133            }
134        } else {
135            this._maxDepth = Math.max(depth, this._maxDepth);
136        }
137    }
138
139    /**
140     * calculate Data and draw chart
141     */
142    async calculateChartData() {
143        this.clearCanvas();
144        this.cavasContext?.beginPath();
145        this.drawScale();
146        let x = this.xPoint;
147        switch (this._mode) {
148            case ChartMode.Byte:
149                for (let node of this.currentData!) {
150                    let width = Math.ceil(node.size / this.currentSize * this.rect!.width);
151                    let height = depthHeight; // 20px / depth
152                    // ensure the data for first depth frame
153                    if (!node.frame) {
154                        node.frame = new Rect(x, scaleHeight, width, height)
155                    } else {
156                        node.frame!.x = x;
157                        node.frame!.y = scaleHeight;
158                        node.frame!.width = width;
159                        node.frame!.height = height;
160                    }
161                    // not draw when rect not in canvas
162                    if (x + width >= 0 && x < this.canvas!.width) {
163                        NativeHookCallInfo.draw(this.cavasContext!, node, node.size / this.currentSize);
164                        this.drawFrameChart(node);
165                    }
166                    x += width;
167                }
168                break;
169            case ChartMode.Count:
170                for (let node of this.currentData!) {
171                    let width = Math.ceil(node.count / this.currentCount * this.rect!.width);
172                    let height = depthHeight; // 20px / depth
173                    // ensure the data for first depth frame
174                    if (!node.frame) {
175                        node.frame = new Rect(x, scaleHeight, width, height)
176                    } else {
177                        node.frame!.x = x;
178                        node.frame!.y = scaleHeight;
179                        node.frame!.width = width;
180                        node.frame!.height = height;
181                    }
182                    // not draw when rect not in canvas
183                    if (x + width >= 0 && x < this.canvas!.width) {
184                        NativeHookCallInfo.draw(this.cavasContext!, node, node.count / this.currentCount);
185                    }
186                    this.drawFrameChart(node);
187                    x += width;
188                }
189                break;
190        }
191        this.drawTriangleOnScale();
192        this.cavasContext?.closePath();
193    }
194
195    /**
196     * draw last selected resct position on scale
197     */
198    private drawTriangleOnScale(): void {
199        if (ChartStruct.lastSelectFuncStruct) {
200            this.cavasContext!.fillStyle = `rgba(${82}, ${145}, ${255})`;
201            let x = Math.ceil(ChartStruct.lastSelectFuncStruct.frame!.x +
202                ChartStruct.lastSelectFuncStruct.frame!.width / 2)
203            if (x < 0) x = sideLenght / 2;
204            if (x > this.canvas!.width) x = this.canvas!.width - sideLenght;
205            this.cavasContext!.moveTo(x - sideLenght / 2, scaleHeight - sideLenght);
206            this.cavasContext!.lineTo(x + sideLenght / 2, scaleHeight - sideLenght);
207            this.cavasContext!.lineTo(x, scaleHeight);
208            this.cavasContext!.lineTo(x - sideLenght / 2, scaleHeight - sideLenght);
209            this.cavasContext?.fill();
210        }
211    }
212
213    /**
214     * clear canvas all data
215     */
216    public clearCanvas(): void {
217        this.cavasContext?.clearRect(0, 0, this.canvas!.width, this.canvas!.height);
218    }
219
220    /**
221     * update canvas size
222     */
223    public updateCanvas(updateWidth: boolean, newWidth?: number): void {
224        if (this.canvas instanceof HTMLCanvasElement) {
225            this.canvas.style.width = 100 + "%";
226            this.canvas.style.height = this.rect!.height + "px";
227            if (this.canvas.clientWidth == 0 && newWidth) {
228                this.canvas.width = newWidth - 40;
229            } else {
230                this.canvas.width = this.canvas.clientWidth;
231            }
232            this.canvas.height = Math.ceil(this.rect!.height);
233            this.updateCanvasCoord();
234        }
235        if (this.rect.width == 0 || updateWidth ||
236            Math.round(newWidth!) != this.canvas!.width + 40 || newWidth! > this.rect.width) {
237            this.rect.width = this.canvas!.width
238        }
239    }
240
241    /**
242     * updateCanvasCoord
243     */
244    private updateCanvasCoord(): void {
245        if (this.canvas instanceof HTMLCanvasElement) {
246            this.isUpdateCanvas = this.canvas.clientWidth != 0;
247            if (this.canvas.getBoundingClientRect()) {
248                let box = this.canvas.getBoundingClientRect();
249                let D = document.documentElement;
250                this.startX = box.left + Math.max(D.scrollLeft, document.body.scrollLeft) - D.clientLeft;
251                this.startY = box.top + Math.max(D.scrollTop, document.body.scrollTop) - D.clientTop + this.canvasScrollTop;
252            }
253        }
254    }
255
256    /**
257     * draw top Scale Into 100 pieces
258     */
259    private drawScale(): void {
260        let spApplication = <SpApplication>document.getElementsByTagName("sp-application")[0];
261        // line
262        this.cavasContext!.lineWidth = 0.5;
263        this.cavasContext?.moveTo(0, 0);
264        this.cavasContext?.lineTo(this.canvas!.width, 0);
265
266        for (let i = 0; i <= 10; i++) {
267            let startX = Math.floor(this.canvas!.width / 10 * i);
268            for (let j = 0; j < 10; j++) {
269                // children scale
270                this.cavasContext!.lineWidth = 0.5;
271                let startItemX = startX + Math.floor(this.canvas!.width / 100 * j);
272                this.cavasContext?.moveTo(startItemX, 0);
273                this.cavasContext?.lineTo(startItemX, 10);
274            }
275            if (i == 0) continue; // skip first Size is 0
276            // long line every 10 count
277            this.cavasContext!.lineWidth = 1;
278            let sizeRatio = this.canvas!.width / this.rect.width; // scale ratio
279            if (spApplication.dark) {
280                this.cavasContext!.strokeStyle = "#888";
281            } else {
282                this.cavasContext!.strokeStyle = "#ddd";
283            }
284            this.cavasContext?.moveTo(startX, 0);
285            this.cavasContext?.lineTo(startX, this.canvas!.height);
286            if (spApplication.dark) {
287                this.cavasContext!.fillStyle = "#fff";
288            } else {
289                this.cavasContext!.fillStyle = "#000";
290            }
291            let scale = '';
292            if (this._mode == ChartMode.Byte) {
293                scale = Utils.getByteWithUnit(this.currentSize * sizeRatio / 10 * i);
294            } else {
295                scale = (this.currentCount * sizeRatio / 10 * i).toFixed(0) + '';
296            }
297            this.cavasContext?.fillText(scale, startX + 5, depthHeight, 50); // 50 is Text max Lenght
298            this.cavasContext?.stroke();
299        }
300    }
301
302    /**
303     * draw chart
304     * @param node draw chart by every piece
305     */
306    drawFrameChart(node: ChartStruct) {
307        if (node.children && node.children.length > 0) {
308            for (let children of node.children) {
309                children.parent = node;
310                let percent = 0;
311                if (this._mode == ChartMode.Byte) {
312                    NativeHookCallInfo.setFuncFrame(children, this.rect, this.currentSize, this._mode);
313                    percent = children.size / this.currentSize;
314                } else {
315                    NativeHookCallInfo.setFuncFrame(children, this.rect, this.currentCount, this._mode);
316                    percent = children.count / this.currentCount;
317                }
318                // not draw when rect not in canvas
319                if ((children.frame!.x + children.frame!.width >= 0 &&
320                    children.frame!.x < this.canvas!.width && children.frame!.width > filterPixiel) || children.needShow) {
321                    NativeHookCallInfo.draw(this.cavasContext!, children, percent);
322                }
323                this.drawFrameChart(children);
324            }
325        }
326    }
327
328    /**
329     * find target node from tree by mouse position
330     *
331     * @param nodes tree nodes
332     * @param canvasX x coord of canvas
333     * @param canvasY y coord of canvas
334     * @returns target node
335     */
336    private searchData(nodes: Array<ChartStruct>, canvasX: number, canvasY: number): any {
337        for (let node of nodes) {
338            if (node.frame?.contains(canvasX, canvasY)) {
339                return node;
340            } else {
341                let result = this.searchData(node.children, canvasX, canvasY);
342                if (!result) continue; // if not found in this branch;search another branch
343                return result;
344            }
345        }
346        return null;
347    }
348
349    /**
350     * show float hint and update position
351     */
352    private updateFloatHint(): void {
353        this.floatHint!.innerHTML = this.hintContent;
354        this.floatHint!.style.display = 'flex';
355        let x = this.canvasX;
356        let y = this.canvasY - this.canvasScrollTop;
357        //right rect hint show left
358        if (this.canvasX + this.floatHint!.clientWidth > (this.canvas?.clientWidth || 0)) {
359            x -= this.floatHint!.clientWidth - 1;
360        } else {
361            x += scaleHeight;
362        }
363        //bottom rect hint show top
364        y -= this.floatHint!.clientHeight - 1;
365
366        this.floatHint!.style.transform = `translate(${x}px,${y}px)`;
367    }
368
369    /**
370     * redraw Chart while click to scale chart
371     * @param selectData select Rect data as array
372     */
373    private redrawChart(selectData: Array<ChartStruct>): void {
374        this.currentData = selectData;
375        if (selectData.length == 0) return;
376        this.caldrawArgs();
377        this.calculateChartData();
378    }
379
380    /**
381     * press w to zoom in, s to zoom out
382     * @param index < 0 zoom out , > 0 zoom in
383     */
384    private scale(index: number): void {
385        let newWidth = 0;
386        // zoom in
387        let deltaWidth = this.rect!.width * 0.2;
388        if (index > 0) {
389            newWidth = this.rect!.width + deltaWidth;
390            // max scale
391            let sizeRatio = this.canvas!.width / this.rect.width;
392            if (this._mode == ChartMode.Byte) {
393                if (Math.round(this.currentSize * sizeRatio) <= 10) {
394                    newWidth = this.canvas!.width / (10 / this.currentSize);
395                }
396            } else {
397                if (Math.round(this.currentCount * sizeRatio) <= 10) {
398                    if (this.xPoint == 0) {
399                        return;
400                    }
401                    newWidth = this.canvas!.width / (10 / this.currentCount);
402                }
403            }
404            deltaWidth = newWidth - this.rect!.width;
405        } else { // zoom out
406            newWidth = this.rect!.width - deltaWidth;
407            // min scale
408            if (newWidth < this.canvas!.width) {
409                newWidth = this.canvas!.width;
410                this.resetTrans();
411            }
412            deltaWidth = this.rect!.width - newWidth;
413        }
414        // width not change
415        if (newWidth == this.rect.width) return;
416        this.translationByScale(index, deltaWidth, newWidth);
417    }
418
419    private resetTrans() {
420        this.xPoint = 0;
421    }
422
423    /**
424     * translation after scale
425     * @param index is zoom in
426     * @param deltaWidth scale delta width
427     * @param newWidth rect width after scale
428     */
429    private translationByScale(index: number, deltaWidth: number, newWidth: number): void {
430        let translationValue = deltaWidth * (this.canvasX - this.xPoint) / this.rect.width;
431        if (index > 0) {
432            this.xPoint -= translationValue;
433        } else {
434            this.xPoint += translationValue;
435        }
436        this.rect!.width = newWidth;
437        this.translationDraw();
438    }
439
440    /**
441     * press a/d to translate rect
442     * @param index left or right
443     */
444    private translation(index: number): void {
445        let offset = this.canvas!.width / 10;
446        if (index < 0) {
447            this.xPoint += offset;
448        } else {
449            this.xPoint -= offset;
450        }
451        this.translationDraw();
452    }
453
454    /**
455     * judge position ro fit canvas and draw
456     */
457    private translationDraw(): void {
458        // rightad trans limit
459        if (this.xPoint > 0) {
460            this.xPoint = 0;
461        }
462        // left trans limit
463        if (this.rect.width + this.xPoint < this.canvas!.width) {
464            this.xPoint = this.canvas!.width - this.rect.width;
465        }
466        this.calculateChartData();
467    }
468
469    /**
470     * canvas click
471     * @param e MouseEvent
472     */
473    private onMouseClick(e: MouseEvent): void {
474        if (e.button == 0) { // mouse left button
475            if (ChartStruct.hoverFuncStruct && ChartStruct.hoverFuncStruct != ChartStruct.selectFuncStruct) {
476                this.drawDataSet(ChartStruct.lastSelectFuncStruct!, false);
477                ChartStruct.lastSelectFuncStruct = undefined;
478                ChartStruct.selectFuncStruct = ChartStruct.hoverFuncStruct;
479                this.historyList.push(this.currentData!);
480                let selectData = new Array<ChartStruct>();
481                selectData.push(ChartStruct.selectFuncStruct!);
482                // reset scale and translation
483                this.rect.width = this.canvas!.clientWidth;
484                this.resetTrans();
485                this.redrawChart(selectData);
486                for (let callback of this.chartClickListenerList) {
487                    callback(false);
488                }
489            }
490        } else if (e.button == 2) { // mouse right button
491            ChartStruct.selectFuncStruct = undefined;
492            if (this.currentData.length == 1 && this.historyList.length > 0) {
493                ChartStruct.lastSelectFuncStruct = this.currentData[0];
494                this.drawDataSet(ChartStruct.lastSelectFuncStruct, true);
495            }
496            if (this.historyList.length > 0) {
497                // reset scale and translation
498                this.rect.width = this.canvas!.clientWidth;
499                this.resetTrans();
500                this.redrawChart(this.historyList.pop()!);
501            }
502            if (this.historyList.length === 0) {
503                for (let callback of this.chartClickListenerList) {
504                    callback(true);
505                }
506            }
507        }
508        this.hideFloatHint();
509    }
510
511    private hideFloatHint(){
512        if (this.floatHint) {
513            this.floatHint.style.display = 'none';
514        }
515    }
516
517    /**
518     * set current select rect parents will show
519     * @param data current noode
520     * @param isShow is show in chart
521     */
522    private drawDataSet(data: ChartStruct, isShow: boolean): void {
523        if (data) {
524            data.needShow = isShow;
525            if (data.parent) {
526                this.drawDataSet(data.parent, isShow);
527            }
528        }
529    }
530
531    /**
532     * mouse on canvas move event
533     */
534    private onMouseMove(): void {
535        let lastNode = ChartStruct.hoverFuncStruct;
536        let searchResult = this.searchData(this.currentData!, this.canvasX, this.canvasY);
537        if (searchResult && (searchResult.frame!.width > filterPixiel ||
538            searchResult.needShow || searchResult.depth == 0)) {
539            ChartStruct.hoverFuncStruct = searchResult;
540            // judge current node is hover redraw chart
541            if (searchResult != lastNode) {
542                let name = ChartStruct.hoverFuncStruct?.symbol;
543                if (this._mode == ChartMode.Byte) {
544                    let size = Utils.getByteWithUnit(ChartStruct.hoverFuncStruct!.size);
545                    this.hintContent = `<span>Name: ${name} </span><span>Size: ${size}</span>`;
546                } else {
547                    let count = ChartStruct.hoverFuncStruct!.count;
548                    this.hintContent = `<span>Name: ${name} </span><span>Count: ${count}</span>`;
549                }
550                this.calculateChartData();
551            }
552            // pervent float hint trigger onmousemove event
553            this.updateFloatHint();
554        } else {
555            this.hideFloatHint();
556            ChartStruct.hoverFuncStruct = undefined;
557        }
558    }
559
560    initElements(): void {
561        this.canvas = this.shadowRoot?.querySelector("#canvas");
562        this.cavasContext = this.canvas?.getContext("2d");
563        this.floatHint = this.shadowRoot?.querySelector('#float_hint');
564
565        this.canvas!.oncontextmenu = () => {
566            return false;
567        };
568        this.canvas!.onmouseup = (e) => {
569            this.onMouseClick(e);
570        }
571
572        this.canvas!.onmousemove = (e) => {
573            if (!this.isUpdateCanvas) {
574                this.updateCanvasCoord();
575            }
576            this.canvasX = e.clientX - this.startX;
577            this.canvasY = e.clientY - this.startY + this.canvasScrollTop;
578            this.isFocusing = true;
579            this.onMouseMove();
580        };
581
582        this.canvas!.onmouseleave = () => {
583            ChartStruct.selectFuncStruct = undefined;
584            this.isFocusing = false;
585            this.hideFloatHint();
586        };
587
588        document.addEventListener('keydown', (e) => {
589            if (!this.isFocusing) return;
590            switch (e.key.toLocaleLowerCase()) {
591                case 'w':
592                    this.scale(1);
593                    break;
594                case 's':
595                    this.scale(-1);
596                    break;
597                case 'a':
598                    this.translation(-1);
599                    break;
600                case 'd':
601                    this.translation(1);
602                    break;
603            }
604        });
605        new ResizeObserver((entries) => {
606            if (this.canvas!.getBoundingClientRect()) {
607                let box = this.canvas!.getBoundingClientRect();
608                let D = document.documentElement;
609                this.startX = box.left + Math.max(D.scrollLeft, document.body.scrollLeft) - D.clientLeft;
610                this.startY = box.top + Math.max(D.scrollTop, document.body.scrollTop) - D.clientTop + this.canvasScrollTop;
611            }
612        }).observe(document.documentElement);
613    }
614
615    initHtml(): string {
616        return `
617            <style>
618            :host{
619                display: flex;
620                padding: 10px 10px;
621            }
622            .tip{
623                position:absolute;
624                left: 0;
625                background-color: white;
626                border: 1px solid #f9f9f9;
627                width: auto;
628                font-size: 8px;
629                color: #50809e;
630                flex-direction: column;
631                justify-content: center;
632                align-items: flex-start;
633                padding: 2px 10px;
634                display: none;
635                user-select: none;
636            }
637            </style>
638            <canvas id="canvas"></canvas>
639            <div id ="float_hint" class="tip"></div>`;
640    }
641}