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