@kylincloud/flamegraph 0.35.28 → 0.36.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -0
- package/dist/FlameGraph/FlameGraphComponent/Flamegraph.d.ts +16 -2
- package/dist/FlameGraph/FlameGraphComponent/Flamegraph.d.ts.map +1 -1
- package/dist/FlameGraph/FlameGraphComponent/Flamegraph_render.d.ts +15 -2
- package/dist/FlameGraph/FlameGraphComponent/Flamegraph_render.d.ts.map +1 -1
- package/dist/FlameGraph/FlameGraphComponent/Highlight.d.ts.map +1 -1
- package/dist/FlameGraph/FlameGraphComponent/index.d.ts.map +1 -1
- package/dist/FlameGraph/normalize.d.ts.map +1 -1
- package/dist/FlameGraph/uniqueness.d.ts.map +1 -1
- package/dist/ProfilerTable.d.ts.map +1 -1
- package/dist/Tooltip/Tooltip.d.ts.map +1 -1
- package/dist/flamegraphRenderWorker.js +2 -0
- package/dist/flamegraphRenderWorker.js.map +1 -0
- package/dist/index.cjs.js +4 -4
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.esm.js +4 -4
- package/dist/index.esm.js.map +1 -1
- package/dist/index.node.cjs.js +4 -4
- package/dist/index.node.cjs.js.map +1 -1
- package/dist/index.node.esm.js +4 -4
- package/dist/index.node.esm.js.map +1 -1
- package/dist/shims/Table.d.ts +15 -1
- package/dist/shims/Table.d.ts.map +1 -1
- package/dist/workers/createFlamegraphRenderWorker.d.ts +2 -0
- package/dist/workers/createFlamegraphRenderWorker.d.ts.map +1 -0
- package/dist/workers/flamegraphRenderWorker.d.ts +2 -0
- package/dist/workers/flamegraphRenderWorker.d.ts.map +1 -0
- package/dist/workers/profilerTableWorker.d.ts +73 -0
- package/dist/workers/profilerTableWorker.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/FlameGraph/FlameGraphComponent/Flamegraph.ts +33 -8
- package/src/FlameGraph/FlameGraphComponent/Flamegraph_render.ts +289 -85
- package/src/FlameGraph/FlameGraphComponent/Highlight.tsx +43 -17
- package/src/FlameGraph/FlameGraphComponent/index.tsx +119 -1
- package/src/FlameGraph/normalize.ts +9 -7
- package/src/FlameGraph/uniqueness.ts +69 -59
- package/src/ProfilerTable.tsx +421 -33
- package/src/Tooltip/Tooltip.tsx +49 -16
- package/src/shims/Table.module.scss +5 -0
- package/src/shims/Table.tsx +195 -5
- package/src/workers/createFlamegraphRenderWorker.ts +23 -0
- package/src/workers/flamegraphRenderWorker.ts +192 -0
- package/src/workers/profilerTableWorker.ts +342 -0
package/src/ProfilerTable.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/ProfilerTable.tsx
|
|
2
|
-
import React, { useRef, RefObject, useCallback, useState, useEffect } from 'react';
|
|
2
|
+
import React, { useRef, RefObject, useCallback, useState, useEffect, useMemo } from 'react';
|
|
3
3
|
import type Color from 'color';
|
|
4
4
|
import cl from 'classnames';
|
|
5
5
|
import type { Maybe } from 'true-myth';
|
|
@@ -31,10 +31,20 @@ import {
|
|
|
31
31
|
type FlamegraphMessages,
|
|
32
32
|
} from './i18n';
|
|
33
33
|
import type { ViewTypes } from './FlameGraph/FlameGraphComponent/viewTypes';
|
|
34
|
+
import {
|
|
35
|
+
createProfilerTableWorker,
|
|
36
|
+
type ProfilerTableWorkerRow,
|
|
37
|
+
type ProfilerTableWorkerRequest,
|
|
38
|
+
type ProfilerTableWorkerResponse,
|
|
39
|
+
} from './workers/profilerTableWorker';
|
|
34
40
|
|
|
35
41
|
import styles from './ProfilerTable.module.scss';
|
|
36
42
|
|
|
37
43
|
const zero = (v?: number) => v || 0;
|
|
44
|
+
const canUseWorker =
|
|
45
|
+
typeof Worker !== 'undefined' &&
|
|
46
|
+
typeof Blob !== 'undefined' &&
|
|
47
|
+
typeof URL !== 'undefined';
|
|
38
48
|
|
|
39
49
|
interface SingleCell {
|
|
40
50
|
type: 'single';
|
|
@@ -56,6 +66,8 @@ interface DoubleCell {
|
|
|
56
66
|
rightTicks: number;
|
|
57
67
|
}
|
|
58
68
|
|
|
69
|
+
type VirtualRange = { start: number; end: number };
|
|
70
|
+
|
|
59
71
|
function generateCellSingle(
|
|
60
72
|
ff: typeof singleFF,
|
|
61
73
|
cell: SingleCell,
|
|
@@ -147,6 +159,23 @@ function generateTable(
|
|
|
147
159
|
return Array.from(hash.values());
|
|
148
160
|
}
|
|
149
161
|
|
|
162
|
+
const tableCache = new WeakMap<
|
|
163
|
+
Flamebearer,
|
|
164
|
+
((SingleCell | DoubleCell) & { name: string })[]
|
|
165
|
+
>();
|
|
166
|
+
|
|
167
|
+
function getTableCells(
|
|
168
|
+
flamebearer: Flamebearer
|
|
169
|
+
): ((SingleCell | DoubleCell) & { name: string })[] {
|
|
170
|
+
const cached = tableCache.get(flamebearer);
|
|
171
|
+
if (cached) {
|
|
172
|
+
return cached;
|
|
173
|
+
}
|
|
174
|
+
const next = generateTable(flamebearer);
|
|
175
|
+
tableCache.set(flamebearer, next);
|
|
176
|
+
return next;
|
|
177
|
+
}
|
|
178
|
+
|
|
150
179
|
/**
|
|
151
180
|
* 根据函数名在 flamebearer 中查找 total 值最大的节点位置
|
|
152
181
|
* 返回 { i, j } 坐标,用于聚焦
|
|
@@ -264,7 +293,7 @@ const getTableBody = ({
|
|
|
264
293
|
}: GetTableBodyRowsProps): TableBodyType => {
|
|
265
294
|
const { numTicks, maxSelf, sampleRate, spyName, units } = flamebearer;
|
|
266
295
|
|
|
267
|
-
const tableBodyCells =
|
|
296
|
+
const tableBodyCells = [...getTableCells(flamebearer)].sort(
|
|
268
297
|
(a, b) => b.total - a.total
|
|
269
298
|
);
|
|
270
299
|
const m = sortByDirection === 'asc' ? 1 : -1;
|
|
@@ -468,11 +497,120 @@ function Table({
|
|
|
468
497
|
onFocusOnNode,
|
|
469
498
|
updateView,
|
|
470
499
|
enableSandwichView = true,
|
|
471
|
-
|
|
500
|
+
virtualize,
|
|
501
|
+
virtualizeRowHeight,
|
|
502
|
+
virtualizeOverscan,
|
|
503
|
+
scrollRef,
|
|
504
|
+
}: ProfilerTableProps & {
|
|
505
|
+
isDoubles: boolean;
|
|
506
|
+
virtualize?: boolean;
|
|
507
|
+
virtualizeRowHeight?: number;
|
|
508
|
+
virtualizeOverscan?: number;
|
|
509
|
+
scrollRef?: RefObject<HTMLElement>;
|
|
510
|
+
}) {
|
|
472
511
|
const i18n = useFlamegraphI18n();
|
|
473
512
|
const [menuProps, toggleMenu] = useMenuState({ transition: true });
|
|
474
513
|
const [anchorPoint, setAnchorPoint] = useState({ x: 0, y: 0 });
|
|
475
514
|
const [contextMenuTarget, setContextMenuTarget] = useState<string | null>(null);
|
|
515
|
+
const useWorker = canUseWorker;
|
|
516
|
+
const workerRef = useRef<Worker | null>(null);
|
|
517
|
+
const flamebearerRef = useRef<Flamebearer | null>(null);
|
|
518
|
+
const rangeRequestIdRef = useRef(0);
|
|
519
|
+
const focusRequestIdRef = useRef(0);
|
|
520
|
+
const pendingFocusRef = useRef(
|
|
521
|
+
new Map<number, (node: { i: number; j: number } | null) => void>()
|
|
522
|
+
);
|
|
523
|
+
const [workerReady, setWorkerReady] = useState(false);
|
|
524
|
+
const [rowCount, setRowCount] = useState(0);
|
|
525
|
+
const [visibleRowData, setVisibleRowData] = useState<ProfilerTableWorkerRow[]>(
|
|
526
|
+
[]
|
|
527
|
+
);
|
|
528
|
+
const [virtualRange, setVirtualRange] = useState<VirtualRange>({
|
|
529
|
+
start: 0,
|
|
530
|
+
end: 0,
|
|
531
|
+
});
|
|
532
|
+
const [requestedRange, setRequestedRange] = useState<VirtualRange | null>(null);
|
|
533
|
+
const [dataVersion, setDataVersion] = useState(0);
|
|
534
|
+
const [workerInstance, setWorkerInstance] = useState(0);
|
|
535
|
+
|
|
536
|
+
useEffect(() => {
|
|
537
|
+
if (!useWorker) {
|
|
538
|
+
return () => {};
|
|
539
|
+
}
|
|
540
|
+
const worker = createProfilerTableWorker();
|
|
541
|
+
workerRef.current = worker;
|
|
542
|
+
setWorkerInstance((prev) => prev + 1);
|
|
543
|
+
|
|
544
|
+
const handleMessage = (event: MessageEvent<ProfilerTableWorkerResponse>) => {
|
|
545
|
+
const msg = event.data;
|
|
546
|
+
if (!msg || !msg.type) {
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
switch (msg.type) {
|
|
550
|
+
case 'ready': {
|
|
551
|
+
setRowCount(msg.payload.rowCount);
|
|
552
|
+
setWorkerReady(true);
|
|
553
|
+
setDataVersion((prev) => prev + 1);
|
|
554
|
+
break;
|
|
555
|
+
}
|
|
556
|
+
case 'range': {
|
|
557
|
+
if (msg.payload.requestId !== rangeRequestIdRef.current) {
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
setVisibleRowData(msg.payload.rows);
|
|
561
|
+
setVirtualRange({ start: msg.payload.start, end: msg.payload.end });
|
|
562
|
+
break;
|
|
563
|
+
}
|
|
564
|
+
case 'findNode': {
|
|
565
|
+
const resolver = pendingFocusRef.current.get(msg.payload.requestId);
|
|
566
|
+
if (resolver) {
|
|
567
|
+
pendingFocusRef.current.delete(msg.payload.requestId);
|
|
568
|
+
resolver(msg.payload.node);
|
|
569
|
+
}
|
|
570
|
+
break;
|
|
571
|
+
}
|
|
572
|
+
default:
|
|
573
|
+
break;
|
|
574
|
+
}
|
|
575
|
+
};
|
|
576
|
+
|
|
577
|
+
worker.addEventListener('message', handleMessage as EventListener);
|
|
578
|
+
return () => {
|
|
579
|
+
worker.removeEventListener('message', handleMessage as EventListener);
|
|
580
|
+
worker.terminate();
|
|
581
|
+
workerRef.current = null;
|
|
582
|
+
pendingFocusRef.current.clear();
|
|
583
|
+
};
|
|
584
|
+
}, [useWorker]);
|
|
585
|
+
|
|
586
|
+
const requestFocusNode = useCallback(
|
|
587
|
+
(name: string) => {
|
|
588
|
+
if (!useWorker || !workerRef.current) {
|
|
589
|
+
return Promise.resolve(findNodeByName(flamebearer, name));
|
|
590
|
+
}
|
|
591
|
+
const requestId = ++focusRequestIdRef.current;
|
|
592
|
+
return new Promise<{ i: number; j: number } | null>((resolve) => {
|
|
593
|
+
pendingFocusRef.current.set(requestId, resolve);
|
|
594
|
+
workerRef.current?.postMessage({
|
|
595
|
+
type: 'findNode',
|
|
596
|
+
payload: { name, requestId },
|
|
597
|
+
} satisfies ProfilerTableWorkerRequest);
|
|
598
|
+
});
|
|
599
|
+
},
|
|
600
|
+
[flamebearer, useWorker]
|
|
601
|
+
);
|
|
602
|
+
|
|
603
|
+
const focusByName = useCallback(
|
|
604
|
+
(name: string) => {
|
|
605
|
+
if (!onFocusOnNode) return;
|
|
606
|
+
requestFocusNode(name).then((node) => {
|
|
607
|
+
if (node) {
|
|
608
|
+
onFocusOnNode(node.i, node.j);
|
|
609
|
+
}
|
|
610
|
+
});
|
|
611
|
+
},
|
|
612
|
+
[onFocusOnNode, requestFocusNode]
|
|
613
|
+
);
|
|
476
614
|
|
|
477
615
|
// 双击处理:高亮 + 聚焦到火焰图
|
|
478
616
|
const handleDoubleClick = useCallback(
|
|
@@ -483,14 +621,9 @@ function Table({
|
|
|
483
621
|
}
|
|
484
622
|
|
|
485
623
|
// 然后聚焦到火焰图
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
const node = findNodeByName(flamebearer, name);
|
|
489
|
-
if (node) {
|
|
490
|
-
onFocusOnNode(node.i, node.j);
|
|
491
|
-
}
|
|
624
|
+
focusByName(name);
|
|
492
625
|
},
|
|
493
|
-
[
|
|
626
|
+
[focusByName, selectedItem, handleTableItemClick]
|
|
494
627
|
);
|
|
495
628
|
|
|
496
629
|
// 右键菜单处理
|
|
@@ -511,14 +644,10 @@ function Table({
|
|
|
511
644
|
|
|
512
645
|
// 菜单项:聚焦到此函数
|
|
513
646
|
const handleFocusClick = useCallback(() => {
|
|
514
|
-
if (!contextMenuTarget
|
|
515
|
-
|
|
516
|
-
const node = findNodeByName(flamebearer, contextMenuTarget);
|
|
517
|
-
if (node) {
|
|
518
|
-
onFocusOnNode(node.i, node.j);
|
|
519
|
-
}
|
|
647
|
+
if (!contextMenuTarget) return;
|
|
648
|
+
focusByName(contextMenuTarget);
|
|
520
649
|
handleMenuClose();
|
|
521
|
-
}, [contextMenuTarget,
|
|
650
|
+
}, [contextMenuTarget, focusByName, handleMenuClose]);
|
|
522
651
|
|
|
523
652
|
// 菜单项:复制函数名
|
|
524
653
|
const handleCopyClick = useCallback(() => {
|
|
@@ -590,25 +719,273 @@ function Table({
|
|
|
590
719
|
);
|
|
591
720
|
|
|
592
721
|
const tableSortProps = useTableSort(tableFormat as any);
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
722
|
+
|
|
723
|
+
useEffect(() => {
|
|
724
|
+
if (!useWorker || !workerRef.current) {
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
727
|
+
const worker = workerRef.current;
|
|
728
|
+
const payload = {
|
|
597
729
|
sortBy: tableSortProps.sortBy,
|
|
598
730
|
sortByDirection: tableSortProps.sortByDirection,
|
|
599
|
-
fitMode,
|
|
600
|
-
handleTableItemClick,
|
|
601
731
|
highlightQuery,
|
|
732
|
+
};
|
|
733
|
+
|
|
734
|
+
if (flamebearerRef.current !== flamebearer) {
|
|
735
|
+
flamebearerRef.current = flamebearer;
|
|
736
|
+
setWorkerReady(false);
|
|
737
|
+
setRowCount(0);
|
|
738
|
+
setVisibleRowData([]);
|
|
739
|
+
worker.postMessage({
|
|
740
|
+
type: 'init',
|
|
741
|
+
payload: {
|
|
742
|
+
flamebearer,
|
|
743
|
+
...payload,
|
|
744
|
+
},
|
|
745
|
+
} satisfies ProfilerTableWorkerRequest);
|
|
746
|
+
} else {
|
|
747
|
+
setWorkerReady(false);
|
|
748
|
+
setVisibleRowData([]);
|
|
749
|
+
worker.postMessage({
|
|
750
|
+
type: 'update',
|
|
751
|
+
payload,
|
|
752
|
+
} satisfies ProfilerTableWorkerRequest);
|
|
753
|
+
}
|
|
754
|
+
}, [
|
|
755
|
+
flamebearer,
|
|
756
|
+
highlightQuery,
|
|
757
|
+
tableSortProps.sortBy,
|
|
758
|
+
tableSortProps.sortByDirection,
|
|
759
|
+
useWorker,
|
|
760
|
+
workerInstance,
|
|
761
|
+
]);
|
|
762
|
+
|
|
763
|
+
const handleRangeChange = useCallback((start: number, end: number) => {
|
|
764
|
+
setRequestedRange((prev) => {
|
|
765
|
+
if (prev && prev.start === start && prev.end === end) {
|
|
766
|
+
return prev;
|
|
767
|
+
}
|
|
768
|
+
return { start, end };
|
|
769
|
+
});
|
|
770
|
+
}, []);
|
|
771
|
+
|
|
772
|
+
useEffect(() => {
|
|
773
|
+
if (!useWorker || !workerRef.current || !requestedRange) {
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
776
|
+
const requestId = ++rangeRequestIdRef.current;
|
|
777
|
+
workerRef.current.postMessage({
|
|
778
|
+
type: 'range',
|
|
779
|
+
payload: {
|
|
780
|
+
start: requestedRange.start,
|
|
781
|
+
end: requestedRange.end,
|
|
782
|
+
requestId,
|
|
783
|
+
},
|
|
784
|
+
} satisfies ProfilerTableWorkerRequest);
|
|
785
|
+
}, [requestedRange, dataVersion, useWorker]);
|
|
786
|
+
|
|
787
|
+
const { numTicks, maxSelf, sampleRate, spyName, units } = flamebearer;
|
|
788
|
+
const formatter = useMemo(
|
|
789
|
+
() => getFormatter(numTicks, sampleRate, units),
|
|
790
|
+
[numTicks, sampleRate, units]
|
|
791
|
+
);
|
|
792
|
+
const nameCellStyle = useMemo(() => fitIntoTableCell(fitMode), [fitMode]);
|
|
793
|
+
const isRowSelected = useCallback(
|
|
794
|
+
(name: string) => selectedItem.isJust && selectedItem.value === name,
|
|
795
|
+
[selectedItem]
|
|
796
|
+
);
|
|
797
|
+
const nameCell = useCallback(
|
|
798
|
+
(name: string) => (
|
|
799
|
+
<button className="table-item-button">
|
|
800
|
+
<div className="symbol-name" style={nameCellStyle}>
|
|
801
|
+
<bdi dir="ltr">{name}</bdi>
|
|
802
|
+
</div>
|
|
803
|
+
</button>
|
|
804
|
+
),
|
|
805
|
+
[nameCellStyle]
|
|
806
|
+
);
|
|
807
|
+
|
|
808
|
+
const buildSingleRow = useCallback(
|
|
809
|
+
(x: Extract<ProfilerTableWorkerRow, { type: 'single' }>): BodyRow => {
|
|
810
|
+
const pn = getPackageNameFromStackTrace(spyName, x.name);
|
|
811
|
+
const color = colorBasedOnPackageName(palette, pn);
|
|
812
|
+
return {
|
|
813
|
+
'data-row': `single;${x.name};${x.self};${x.total}`,
|
|
814
|
+
isRowSelected: isRowSelected(x.name),
|
|
815
|
+
onClick: () => {
|
|
816
|
+
handleTableItemClick({ name: x.name });
|
|
817
|
+
},
|
|
818
|
+
onContextMenu: (e: React.MouseEvent) => handleContextMenu(e, x.name),
|
|
819
|
+
onDoubleClick: () => handleDoubleClick(x.name),
|
|
820
|
+
cells: [
|
|
821
|
+
{ value: nameCell(x.name) },
|
|
822
|
+
{
|
|
823
|
+
value: formatter.format(x.self, sampleRate),
|
|
824
|
+
style: backgroundImageStyle(x.self, maxSelf, color),
|
|
825
|
+
},
|
|
826
|
+
{
|
|
827
|
+
value: formatter.format(x.total, sampleRate),
|
|
828
|
+
style: backgroundImageStyle(x.total, numTicks, color),
|
|
829
|
+
},
|
|
830
|
+
],
|
|
831
|
+
};
|
|
832
|
+
},
|
|
833
|
+
[
|
|
834
|
+
formatter,
|
|
835
|
+
handleContextMenu,
|
|
836
|
+
handleDoubleClick,
|
|
837
|
+
handleTableItemClick,
|
|
838
|
+
isRowSelected,
|
|
839
|
+
maxSelf,
|
|
840
|
+
nameCell,
|
|
841
|
+
numTicks,
|
|
602
842
|
palette,
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
843
|
+
sampleRate,
|
|
844
|
+
spyName,
|
|
845
|
+
]
|
|
846
|
+
);
|
|
847
|
+
|
|
848
|
+
const buildDoubleRow = useCallback(
|
|
849
|
+
(x: Extract<ProfilerTableWorkerRow, { type: 'double' }>): BodyRow => {
|
|
850
|
+
const leftPercent = ratioToPercent(
|
|
851
|
+
x.leftTicks > 0 ? x.totalLeft / x.leftTicks : 0
|
|
852
|
+
);
|
|
853
|
+
const rghtPercent = ratioToPercent(
|
|
854
|
+
x.rightTicks > 0 ? x.totalRght / x.rightTicks : 0
|
|
855
|
+
);
|
|
856
|
+
|
|
857
|
+
let totalDiff = diffPercent(leftPercent, rghtPercent);
|
|
858
|
+
if (!Number.isFinite(totalDiff)) {
|
|
859
|
+
totalDiff = 0;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
let diffCellColor = '';
|
|
863
|
+
if (totalDiff > 0) {
|
|
864
|
+
diffCellColor = palette.badColor.rgb().string();
|
|
865
|
+
} else if (totalDiff < 0) {
|
|
866
|
+
diffCellColor = palette.goodColor.rgb().string();
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
let diffValue = '';
|
|
870
|
+
if (x.totalLeft === 0 && x.totalRght > 0) {
|
|
871
|
+
diffValue = i18n.diffNew;
|
|
872
|
+
} else if (x.totalRght === 0 && x.totalLeft > 0) {
|
|
873
|
+
diffValue = i18n.diffRemoved;
|
|
874
|
+
} else if (totalDiff === 0) {
|
|
875
|
+
diffValue = '0%';
|
|
876
|
+
} else if (totalDiff > 0) {
|
|
877
|
+
diffValue = `+${totalDiff.toFixed(2)}%`;
|
|
878
|
+
} else if (totalDiff < 0) {
|
|
879
|
+
diffValue = `${totalDiff.toFixed(2)}%`;
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
return {
|
|
883
|
+
'data-row': `double;${x.name};${x.totalLeft};${x.leftTicks};${x.totalRght};${x.rightTicks}`,
|
|
884
|
+
isRowSelected: isRowSelected(x.name),
|
|
885
|
+
onClick: () => {
|
|
886
|
+
handleTableItemClick({ name: x.name });
|
|
887
|
+
},
|
|
888
|
+
onContextMenu: (e: React.MouseEvent) => handleContextMenu(e, x.name),
|
|
889
|
+
onDoubleClick: () => handleDoubleClick(x.name),
|
|
890
|
+
cells: [
|
|
891
|
+
{ value: nameCell(x.name) },
|
|
892
|
+
{ value: `${leftPercent} %` },
|
|
893
|
+
{ value: `${rghtPercent} %` },
|
|
894
|
+
{
|
|
895
|
+
value: diffValue,
|
|
896
|
+
style: {
|
|
897
|
+
color: diffCellColor,
|
|
898
|
+
},
|
|
899
|
+
},
|
|
900
|
+
],
|
|
901
|
+
};
|
|
902
|
+
},
|
|
903
|
+
[
|
|
904
|
+
handleContextMenu,
|
|
905
|
+
handleDoubleClick,
|
|
906
|
+
handleTableItemClick,
|
|
907
|
+
i18n,
|
|
908
|
+
isRowSelected,
|
|
909
|
+
nameCell,
|
|
910
|
+
palette,
|
|
911
|
+
]
|
|
912
|
+
);
|
|
913
|
+
|
|
914
|
+
const visibleRows = useMemo(() => {
|
|
915
|
+
if (!useWorker) {
|
|
916
|
+
return [];
|
|
917
|
+
}
|
|
918
|
+
return visibleRowData.map((row) =>
|
|
919
|
+
row.type === 'double'
|
|
920
|
+
? buildDoubleRow(row)
|
|
921
|
+
: buildSingleRow(row)
|
|
922
|
+
);
|
|
923
|
+
}, [buildDoubleRow, buildSingleRow, useWorker, visibleRowData]);
|
|
924
|
+
|
|
925
|
+
const table = useMemo(() => {
|
|
926
|
+
if (useWorker) {
|
|
927
|
+
if (workerReady && rowCount === 0) {
|
|
928
|
+
return {
|
|
929
|
+
headRow: tableFormat as any,
|
|
930
|
+
type: 'not-filled' as const,
|
|
931
|
+
value: (
|
|
932
|
+
<div className="unsupported-format">{i18n.noItemsFound}</div>
|
|
933
|
+
),
|
|
934
|
+
};
|
|
935
|
+
}
|
|
936
|
+
return {
|
|
937
|
+
headRow: tableFormat as any,
|
|
938
|
+
type: 'virtual' as const,
|
|
939
|
+
rowCount,
|
|
940
|
+
rows: visibleRows,
|
|
941
|
+
range: virtualRange,
|
|
942
|
+
onRangeChange: handleRangeChange,
|
|
943
|
+
};
|
|
944
|
+
}
|
|
945
|
+
return {
|
|
946
|
+
headRow: tableFormat as any,
|
|
947
|
+
...getTableBody({
|
|
948
|
+
flamebearer,
|
|
949
|
+
sortBy: tableSortProps.sortBy,
|
|
950
|
+
sortByDirection: tableSortProps.sortByDirection,
|
|
951
|
+
fitMode,
|
|
952
|
+
handleTableItemClick,
|
|
953
|
+
highlightQuery,
|
|
954
|
+
palette,
|
|
955
|
+
selectedItem,
|
|
956
|
+
messages: i18n,
|
|
957
|
+
onRowContextMenu: handleContextMenu,
|
|
958
|
+
onRowDoubleClick: handleDoubleClick,
|
|
959
|
+
onFocusOnNode,
|
|
960
|
+
updateView,
|
|
961
|
+
enableSandwichView,
|
|
962
|
+
}),
|
|
963
|
+
};
|
|
964
|
+
}, [
|
|
965
|
+
buildDoubleRow,
|
|
966
|
+
buildSingleRow,
|
|
967
|
+
enableSandwichView,
|
|
968
|
+
fitMode,
|
|
969
|
+
flamebearer,
|
|
970
|
+
handleContextMenu,
|
|
971
|
+
handleDoubleClick,
|
|
972
|
+
handleRangeChange,
|
|
973
|
+
handleTableItemClick,
|
|
974
|
+
highlightQuery,
|
|
975
|
+
i18n,
|
|
976
|
+
onFocusOnNode,
|
|
977
|
+
palette,
|
|
978
|
+
rowCount,
|
|
979
|
+
selectedItem,
|
|
980
|
+
tableFormat,
|
|
981
|
+
tableSortProps.sortBy,
|
|
982
|
+
tableSortProps.sortByDirection,
|
|
983
|
+
updateView,
|
|
984
|
+
useWorker,
|
|
985
|
+
visibleRows,
|
|
986
|
+
virtualRange,
|
|
987
|
+
workerReady,
|
|
988
|
+
]);
|
|
612
989
|
|
|
613
990
|
const isHighlighted =
|
|
614
991
|
selectedItem.isJust && selectedItem.value === contextMenuTarget;
|
|
@@ -620,9 +997,14 @@ function Table({
|
|
|
620
997
|
{...tableSortProps}
|
|
621
998
|
tableBodyRef={tableBodyRef}
|
|
622
999
|
table={table}
|
|
1000
|
+
isLoading={useWorker && !workerReady}
|
|
623
1001
|
className={cl('flamegraph-table', {
|
|
624
1002
|
'flamegraph-table-doubles': isDoubles,
|
|
625
1003
|
})}
|
|
1004
|
+
virtualize={virtualize}
|
|
1005
|
+
virtualizeRowHeight={virtualizeRowHeight}
|
|
1006
|
+
virtualizeOverscan={virtualizeOverscan}
|
|
1007
|
+
scrollRef={scrollRef}
|
|
626
1008
|
/>
|
|
627
1009
|
|
|
628
1010
|
{/* 右键菜单 */}
|
|
@@ -670,8 +1052,10 @@ const ProfilerTable = React.memo(function ProfilerTable({
|
|
|
670
1052
|
}: Omit<ProfilerTableProps, 'tableBodyRef'>) {
|
|
671
1053
|
const tableBodyRef = useRef<HTMLTableSectionElement>(null);
|
|
672
1054
|
const wrapperRef = useRef<HTMLDivElement>(null);
|
|
1055
|
+
const scrollRef = useRef<HTMLDivElement>(null);
|
|
673
1056
|
const [scrollbarSize, setScrollbarSize] = useState(0);
|
|
674
1057
|
const isDoubles = flamebearer.format === 'double';
|
|
1058
|
+
const shouldVirtualize = true;
|
|
675
1059
|
|
|
676
1060
|
useEffect(() => {
|
|
677
1061
|
const bodyEl = tableBodyRef.current;
|
|
@@ -698,7 +1082,7 @@ const ProfilerTable = React.memo(function ProfilerTable({
|
|
|
698
1082
|
}
|
|
699
1083
|
>
|
|
700
1084
|
{/* 内部滚动容器:只控制表格本身的高度和滚动 */}
|
|
701
|
-
<div className="flamegraph-table-scroll">
|
|
1085
|
+
<div className="flamegraph-table-scroll" ref={scrollRef}>
|
|
702
1086
|
<Table
|
|
703
1087
|
tableBodyRef={tableBodyRef}
|
|
704
1088
|
flamebearer={flamebearer}
|
|
@@ -711,6 +1095,10 @@ const ProfilerTable = React.memo(function ProfilerTable({
|
|
|
711
1095
|
onFocusOnNode={onFocusOnNode}
|
|
712
1096
|
updateView={updateView}
|
|
713
1097
|
enableSandwichView={enableSandwichView}
|
|
1098
|
+
virtualize={shouldVirtualize}
|
|
1099
|
+
virtualizeRowHeight={26}
|
|
1100
|
+
virtualizeOverscan={8}
|
|
1101
|
+
scrollRef={scrollRef}
|
|
714
1102
|
/>
|
|
715
1103
|
</div>
|
|
716
1104
|
|
package/src/Tooltip/Tooltip.tsx
CHANGED
|
@@ -193,6 +193,8 @@ export function Tooltip({
|
|
|
193
193
|
}: TooltipProps) {
|
|
194
194
|
const tooltipRef = useRef<HTMLDivElement>(null);
|
|
195
195
|
const lastPosRef = useRef<{ x: number; y: number } | null>(null);
|
|
196
|
+
const rafRef = useRef<number | null>(null);
|
|
197
|
+
const lastMeasureRef = useRef<{ w: number; h: number } | null>(null);
|
|
196
198
|
|
|
197
199
|
const [content, setContent] = React.useState({
|
|
198
200
|
title: {
|
|
@@ -216,22 +218,29 @@ export function Tooltip({
|
|
|
216
218
|
|
|
217
219
|
const memoizedOnMouseMove = useCallback(
|
|
218
220
|
(e: MouseEvent) => {
|
|
219
|
-
if (
|
|
220
|
-
|
|
221
|
+
if (rafRef.current) {
|
|
222
|
+
cancelAnimationFrame(rafRef.current);
|
|
221
223
|
}
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
224
|
+
const x = e.clientX;
|
|
225
|
+
const y = e.clientY;
|
|
226
|
+
rafRef.current = requestAnimationFrame(() => {
|
|
227
|
+
if (!tooltipRef.current) {
|
|
228
|
+
throw new Error('Missing tooltipElement');
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// 记录最后鼠标位置(给 useLayoutEffect 二次校正用)
|
|
232
|
+
lastPosRef.current = { x, y };
|
|
233
|
+
|
|
234
|
+
// 先更新内容(tooltip 高度可能因此变化)
|
|
235
|
+
setTooltipContent(setContent, onMouseOut, e);
|
|
236
|
+
|
|
237
|
+
// 用当前已渲染的尺寸先算一次位置(内容变化后的真实尺寸会在 useLayoutEffect 再校正)
|
|
238
|
+
const w = tooltipRef.current.clientWidth || 0;
|
|
239
|
+
const h = tooltipRef.current.clientHeight || 0;
|
|
240
|
+
|
|
241
|
+
const { left, top } = computeSafeTooltipPos(x, y, w, h);
|
|
242
|
+
setStyle({ top, left, visibility: 'visible' });
|
|
243
|
+
});
|
|
235
244
|
},
|
|
236
245
|
[setTooltipContent]
|
|
237
246
|
);
|
|
@@ -241,12 +250,23 @@ export function Tooltip({
|
|
|
241
250
|
if (!dataSourceEl) {
|
|
242
251
|
return () => { };
|
|
243
252
|
}
|
|
253
|
+
const onMouseLeave = () => {
|
|
254
|
+
onMouseOut();
|
|
255
|
+
};
|
|
256
|
+
const onWindowMove = (e: MouseEvent) => {
|
|
257
|
+
if (!dataSourceEl.contains(e.target as Node)) {
|
|
258
|
+
onMouseOut();
|
|
259
|
+
}
|
|
260
|
+
};
|
|
244
261
|
|
|
245
262
|
dataSourceEl.addEventListener(
|
|
246
263
|
'mousemove',
|
|
247
264
|
memoizedOnMouseMove as EventListener
|
|
248
265
|
);
|
|
249
266
|
dataSourceEl.addEventListener('mouseout', onMouseOut);
|
|
267
|
+
dataSourceEl.addEventListener('mouseleave', onMouseLeave);
|
|
268
|
+
window.addEventListener('mousemove', onWindowMove, { passive: true });
|
|
269
|
+
window.addEventListener('blur', onMouseLeave);
|
|
250
270
|
|
|
251
271
|
return () => {
|
|
252
272
|
dataSourceEl.removeEventListener(
|
|
@@ -254,6 +274,12 @@ export function Tooltip({
|
|
|
254
274
|
memoizedOnMouseMove as EventListener
|
|
255
275
|
);
|
|
256
276
|
dataSourceEl.removeEventListener('mouseout', onMouseOut);
|
|
277
|
+
dataSourceEl.removeEventListener('mouseleave', onMouseLeave);
|
|
278
|
+
window.removeEventListener('mousemove', onWindowMove as EventListener);
|
|
279
|
+
window.removeEventListener('blur', onMouseLeave);
|
|
280
|
+
if (rafRef.current) {
|
|
281
|
+
cancelAnimationFrame(rafRef.current);
|
|
282
|
+
}
|
|
257
283
|
};
|
|
258
284
|
}, [dataSourceRef.current, memoizedOnMouseMove]);
|
|
259
285
|
|
|
@@ -265,7 +291,14 @@ export function Tooltip({
|
|
|
265
291
|
|
|
266
292
|
const { x, y } = lastPosRef.current;
|
|
267
293
|
const rect = tooltipRef.current.getBoundingClientRect();
|
|
268
|
-
const
|
|
294
|
+
const w = rect.width;
|
|
295
|
+
const h = rect.height;
|
|
296
|
+
const last = lastMeasureRef.current;
|
|
297
|
+
if (last && Math.abs(last.w - w) < 1 && Math.abs(last.h - h) < 1) {
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
lastMeasureRef.current = { w, h };
|
|
301
|
+
const { left, top } = computeSafeTooltipPos(x, y, w, h);
|
|
269
302
|
|
|
270
303
|
setStyle((prev) => {
|
|
271
304
|
if (prev && prev.left === left && prev.top === top && prev.visibility === 'visible') {
|