@kylincloud/flamegraph 0.35.27 → 0.35.29
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 +35 -0
- package/dist/FlameGraph/FlameGraphComponent/DiffLegend.d.ts.map +1 -1
- package/dist/FlameGraph/FlameGraphComponent/DiffLegendPaletteDropdown.d.ts.map +1 -1
- 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/Icons.d.ts.map +1 -1
- package/dist/ProfilerTable.d.ts.map +1 -1
- package/dist/SharedQueryInput.d.ts.map +1 -1
- package/dist/Toolbar.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/shims/Tooltip.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/DiffLegend.module.css +8 -2
- package/src/FlameGraph/FlameGraphComponent/DiffLegend.tsx +12 -1
- package/src/FlameGraph/FlameGraphComponent/DiffLegendPaletteDropdown.module.css +93 -10
- package/src/FlameGraph/FlameGraphComponent/DiffLegendPaletteDropdown.tsx +9 -4
- 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 +208 -57
- package/src/FlameGraph/FlameGraphComponent/styles.module.scss +8 -0
- package/src/FlameGraph/normalize.ts +9 -7
- package/src/FlameGraph/uniqueness.ts +69 -59
- package/src/Icons.tsx +18 -9
- package/src/ProfilerTable.tsx +463 -33
- package/src/SharedQueryInput.module.scss +50 -0
- package/src/SharedQueryInput.tsx +18 -3
- package/src/Toolbar.module.scss +90 -0
- package/src/Toolbar.tsx +30 -16
- package/src/Tooltip/Tooltip.tsx +49 -16
- package/src/i18n.tsx +1 -1
- package/src/sass/_common.scss +22 -3
- package/src/sass/_css-variables.scss +5 -1
- package/src/sass/flamegraph.scss +26 -23
- package/src/shims/Table.module.scss +91 -13
- package/src/shims/Table.tsx +202 -7
- package/src/shims/Tooltip.module.scss +40 -0
- package/src/shims/Tooltip.tsx +31 -3
- package/src/workers/createFlamegraphRenderWorker.ts +7 -0
- package/src/workers/flamegraphRenderWorker.ts +198 -0
- package/src/workers/profilerTableWorker.ts +368 -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,29 +497,161 @@ 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
|
+
// eslint-disable-next-line no-console
|
|
544
|
+
console.debug('[profiler-table] worker created');
|
|
545
|
+
|
|
546
|
+
const handleMessage = (event: MessageEvent<ProfilerTableWorkerResponse>) => {
|
|
547
|
+
const msg = event.data;
|
|
548
|
+
if (!msg || !msg.type) {
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
switch (msg.type) {
|
|
552
|
+
case 'ready': {
|
|
553
|
+
setRowCount(msg.payload.rowCount);
|
|
554
|
+
setWorkerReady(true);
|
|
555
|
+
setDataVersion((prev) => prev + 1);
|
|
556
|
+
// eslint-disable-next-line no-console
|
|
557
|
+
console.debug('[profiler-table] worker ready', {
|
|
558
|
+
rowCount: msg.payload.rowCount,
|
|
559
|
+
});
|
|
560
|
+
break;
|
|
561
|
+
}
|
|
562
|
+
case 'range': {
|
|
563
|
+
if (msg.payload.requestId !== rangeRequestIdRef.current) {
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
setVisibleRowData(msg.payload.rows);
|
|
567
|
+
setVirtualRange({ start: msg.payload.start, end: msg.payload.end });
|
|
568
|
+
// eslint-disable-next-line no-console
|
|
569
|
+
console.debug('[profiler-table] range received', {
|
|
570
|
+
start: msg.payload.start,
|
|
571
|
+
end: msg.payload.end,
|
|
572
|
+
rows: msg.payload.rows.length,
|
|
573
|
+
});
|
|
574
|
+
break;
|
|
575
|
+
}
|
|
576
|
+
case 'findNode': {
|
|
577
|
+
const resolver = pendingFocusRef.current.get(msg.payload.requestId);
|
|
578
|
+
if (resolver) {
|
|
579
|
+
pendingFocusRef.current.delete(msg.payload.requestId);
|
|
580
|
+
resolver(msg.payload.node);
|
|
581
|
+
}
|
|
582
|
+
// eslint-disable-next-line no-console
|
|
583
|
+
console.debug('[profiler-table] findNode response', {
|
|
584
|
+
found: !!msg.payload.node,
|
|
585
|
+
});
|
|
586
|
+
break;
|
|
587
|
+
}
|
|
588
|
+
default:
|
|
589
|
+
break;
|
|
590
|
+
}
|
|
591
|
+
};
|
|
592
|
+
|
|
593
|
+
worker.addEventListener('message', handleMessage as EventListener);
|
|
594
|
+
return () => {
|
|
595
|
+
worker.removeEventListener('message', handleMessage as EventListener);
|
|
596
|
+
worker.terminate();
|
|
597
|
+
workerRef.current = null;
|
|
598
|
+
pendingFocusRef.current.clear();
|
|
599
|
+
// eslint-disable-next-line no-console
|
|
600
|
+
console.debug('[profiler-table] worker terminated');
|
|
601
|
+
};
|
|
602
|
+
}, [useWorker]);
|
|
603
|
+
|
|
604
|
+
const requestFocusNode = useCallback(
|
|
605
|
+
(name: string) => {
|
|
606
|
+
if (!useWorker || !workerRef.current) {
|
|
607
|
+
return Promise.resolve(findNodeByName(flamebearer, name));
|
|
608
|
+
}
|
|
609
|
+
const requestId = ++focusRequestIdRef.current;
|
|
610
|
+
// eslint-disable-next-line no-console
|
|
611
|
+
console.debug('[profiler-table] findNode request', { name, requestId });
|
|
612
|
+
return new Promise<{ i: number; j: number } | null>((resolve) => {
|
|
613
|
+
pendingFocusRef.current.set(requestId, resolve);
|
|
614
|
+
workerRef.current?.postMessage({
|
|
615
|
+
type: 'findNode',
|
|
616
|
+
payload: { name, requestId },
|
|
617
|
+
} satisfies ProfilerTableWorkerRequest);
|
|
618
|
+
});
|
|
619
|
+
},
|
|
620
|
+
[flamebearer, useWorker]
|
|
621
|
+
);
|
|
622
|
+
|
|
623
|
+
const focusByName = useCallback(
|
|
624
|
+
(name: string) => {
|
|
625
|
+
if (!onFocusOnNode) return;
|
|
626
|
+
requestFocusNode(name).then((node) => {
|
|
627
|
+
if (node) {
|
|
628
|
+
onFocusOnNode(node.i, node.j);
|
|
629
|
+
}
|
|
630
|
+
});
|
|
631
|
+
},
|
|
632
|
+
[onFocusOnNode, requestFocusNode]
|
|
633
|
+
);
|
|
476
634
|
|
|
477
635
|
// 双击处理:高亮 + 聚焦到火焰图
|
|
478
636
|
const handleDoubleClick = useCallback(
|
|
479
637
|
(name: string) => {
|
|
480
638
|
// 先确保该项被高亮(如果未高亮则高亮,如果已高亮则保持)
|
|
481
639
|
if (!selectedItem.isJust || selectedItem.value !== name) {
|
|
640
|
+
// eslint-disable-next-line no-console
|
|
641
|
+
console.time(`[profiler-table] dblclick highlight ${name}`);
|
|
482
642
|
handleTableItemClick({ name });
|
|
643
|
+
// eslint-disable-next-line no-console
|
|
644
|
+
console.timeEnd(`[profiler-table] dblclick highlight ${name}`);
|
|
483
645
|
}
|
|
484
646
|
|
|
485
647
|
// 然后聚焦到火焰图
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
}
|
|
648
|
+
// eslint-disable-next-line no-console
|
|
649
|
+
console.time(`[profiler-table] dblclick focus ${name}`);
|
|
650
|
+
focusByName(name);
|
|
651
|
+
// eslint-disable-next-line no-console
|
|
652
|
+
console.timeEnd(`[profiler-table] dblclick focus ${name}`);
|
|
492
653
|
},
|
|
493
|
-
[
|
|
654
|
+
[focusByName, selectedItem, handleTableItemClick]
|
|
494
655
|
);
|
|
495
656
|
|
|
496
657
|
// 右键菜单处理
|
|
@@ -511,14 +672,14 @@ function Table({
|
|
|
511
672
|
|
|
512
673
|
// 菜单项:聚焦到此函数
|
|
513
674
|
const handleFocusClick = useCallback(() => {
|
|
514
|
-
if (!contextMenuTarget
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
}
|
|
675
|
+
if (!contextMenuTarget) return;
|
|
676
|
+
// eslint-disable-next-line no-console
|
|
677
|
+
console.time(`[profiler-table] menu focus ${contextMenuTarget}`);
|
|
678
|
+
focusByName(contextMenuTarget);
|
|
679
|
+
// eslint-disable-next-line no-console
|
|
680
|
+
console.timeEnd(`[profiler-table] menu focus ${contextMenuTarget}`);
|
|
520
681
|
handleMenuClose();
|
|
521
|
-
}, [contextMenuTarget,
|
|
682
|
+
}, [contextMenuTarget, focusByName, handleMenuClose]);
|
|
522
683
|
|
|
523
684
|
// 菜单项:复制函数名
|
|
524
685
|
const handleCopyClick = useCallback(() => {
|
|
@@ -590,25 +751,283 @@ function Table({
|
|
|
590
751
|
);
|
|
591
752
|
|
|
592
753
|
const tableSortProps = useTableSort(tableFormat as any);
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
754
|
+
|
|
755
|
+
useEffect(() => {
|
|
756
|
+
if (!useWorker || !workerRef.current) {
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
const worker = workerRef.current;
|
|
760
|
+
const payload = {
|
|
597
761
|
sortBy: tableSortProps.sortBy,
|
|
598
762
|
sortByDirection: tableSortProps.sortByDirection,
|
|
599
|
-
fitMode,
|
|
600
|
-
handleTableItemClick,
|
|
601
763
|
highlightQuery,
|
|
764
|
+
};
|
|
765
|
+
|
|
766
|
+
if (flamebearerRef.current !== flamebearer) {
|
|
767
|
+
flamebearerRef.current = flamebearer;
|
|
768
|
+
setWorkerReady(false);
|
|
769
|
+
setRowCount(0);
|
|
770
|
+
setVisibleRowData([]);
|
|
771
|
+
worker.postMessage({
|
|
772
|
+
type: 'init',
|
|
773
|
+
payload: {
|
|
774
|
+
flamebearer,
|
|
775
|
+
...payload,
|
|
776
|
+
},
|
|
777
|
+
} satisfies ProfilerTableWorkerRequest);
|
|
778
|
+
} else {
|
|
779
|
+
setWorkerReady(false);
|
|
780
|
+
setVisibleRowData([]);
|
|
781
|
+
worker.postMessage({
|
|
782
|
+
type: 'update',
|
|
783
|
+
payload,
|
|
784
|
+
} satisfies ProfilerTableWorkerRequest);
|
|
785
|
+
}
|
|
786
|
+
}, [
|
|
787
|
+
flamebearer,
|
|
788
|
+
highlightQuery,
|
|
789
|
+
tableSortProps.sortBy,
|
|
790
|
+
tableSortProps.sortByDirection,
|
|
791
|
+
useWorker,
|
|
792
|
+
workerInstance,
|
|
793
|
+
]);
|
|
794
|
+
|
|
795
|
+
const handleRangeChange = useCallback((start: number, end: number) => {
|
|
796
|
+
// eslint-disable-next-line no-console
|
|
797
|
+
console.debug('[profiler-table] range requested', { start, end });
|
|
798
|
+
setRequestedRange((prev) => {
|
|
799
|
+
if (prev && prev.start === start && prev.end === end) {
|
|
800
|
+
return prev;
|
|
801
|
+
}
|
|
802
|
+
return { start, end };
|
|
803
|
+
});
|
|
804
|
+
}, []);
|
|
805
|
+
|
|
806
|
+
useEffect(() => {
|
|
807
|
+
if (!useWorker || !workerRef.current || !requestedRange) {
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
810
|
+
const requestId = ++rangeRequestIdRef.current;
|
|
811
|
+
workerRef.current.postMessage({
|
|
812
|
+
type: 'range',
|
|
813
|
+
payload: {
|
|
814
|
+
start: requestedRange.start,
|
|
815
|
+
end: requestedRange.end,
|
|
816
|
+
requestId,
|
|
817
|
+
},
|
|
818
|
+
} satisfies ProfilerTableWorkerRequest);
|
|
819
|
+
}, [requestedRange, dataVersion, useWorker]);
|
|
820
|
+
|
|
821
|
+
const { numTicks, maxSelf, sampleRate, spyName, units } = flamebearer;
|
|
822
|
+
const formatter = useMemo(
|
|
823
|
+
() => getFormatter(numTicks, sampleRate, units),
|
|
824
|
+
[numTicks, sampleRate, units]
|
|
825
|
+
);
|
|
826
|
+
const nameCellStyle = useMemo(() => fitIntoTableCell(fitMode), [fitMode]);
|
|
827
|
+
const isRowSelected = useCallback(
|
|
828
|
+
(name: string) => selectedItem.isJust && selectedItem.value === name,
|
|
829
|
+
[selectedItem]
|
|
830
|
+
);
|
|
831
|
+
const nameCell = useCallback(
|
|
832
|
+
(name: string) => (
|
|
833
|
+
<button className="table-item-button">
|
|
834
|
+
<div className="symbol-name" style={nameCellStyle}>
|
|
835
|
+
<bdi dir="ltr">{name}</bdi>
|
|
836
|
+
</div>
|
|
837
|
+
</button>
|
|
838
|
+
),
|
|
839
|
+
[nameCellStyle]
|
|
840
|
+
);
|
|
841
|
+
|
|
842
|
+
const buildSingleRow = useCallback(
|
|
843
|
+
(x: Extract<ProfilerTableWorkerRow, { type: 'single' }>): BodyRow => {
|
|
844
|
+
const pn = getPackageNameFromStackTrace(spyName, x.name);
|
|
845
|
+
const color = colorBasedOnPackageName(palette, pn);
|
|
846
|
+
return {
|
|
847
|
+
'data-row': `single;${x.name};${x.self};${x.total}`,
|
|
848
|
+
isRowSelected: isRowSelected(x.name),
|
|
849
|
+
onClick: () => {
|
|
850
|
+
// eslint-disable-next-line no-console
|
|
851
|
+
console.time(`[profiler-table] click ${x.name}`);
|
|
852
|
+
handleTableItemClick({ name: x.name });
|
|
853
|
+
// eslint-disable-next-line no-console
|
|
854
|
+
console.timeEnd(`[profiler-table] click ${x.name}`);
|
|
855
|
+
},
|
|
856
|
+
onContextMenu: (e: React.MouseEvent) => handleContextMenu(e, x.name),
|
|
857
|
+
onDoubleClick: () => handleDoubleClick(x.name),
|
|
858
|
+
cells: [
|
|
859
|
+
{ value: nameCell(x.name) },
|
|
860
|
+
{
|
|
861
|
+
value: formatter.format(x.self, sampleRate),
|
|
862
|
+
style: backgroundImageStyle(x.self, maxSelf, color),
|
|
863
|
+
},
|
|
864
|
+
{
|
|
865
|
+
value: formatter.format(x.total, sampleRate),
|
|
866
|
+
style: backgroundImageStyle(x.total, numTicks, color),
|
|
867
|
+
},
|
|
868
|
+
],
|
|
869
|
+
};
|
|
870
|
+
},
|
|
871
|
+
[
|
|
872
|
+
formatter,
|
|
873
|
+
handleContextMenu,
|
|
874
|
+
handleDoubleClick,
|
|
875
|
+
handleTableItemClick,
|
|
876
|
+
isRowSelected,
|
|
877
|
+
maxSelf,
|
|
878
|
+
nameCell,
|
|
879
|
+
numTicks,
|
|
602
880
|
palette,
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
881
|
+
sampleRate,
|
|
882
|
+
spyName,
|
|
883
|
+
]
|
|
884
|
+
);
|
|
885
|
+
|
|
886
|
+
const buildDoubleRow = useCallback(
|
|
887
|
+
(x: Extract<ProfilerTableWorkerRow, { type: 'double' }>): BodyRow => {
|
|
888
|
+
const leftPercent = ratioToPercent(
|
|
889
|
+
x.leftTicks > 0 ? x.totalLeft / x.leftTicks : 0
|
|
890
|
+
);
|
|
891
|
+
const rghtPercent = ratioToPercent(
|
|
892
|
+
x.rightTicks > 0 ? x.totalRght / x.rightTicks : 0
|
|
893
|
+
);
|
|
894
|
+
|
|
895
|
+
let totalDiff = diffPercent(leftPercent, rghtPercent);
|
|
896
|
+
if (!Number.isFinite(totalDiff)) {
|
|
897
|
+
totalDiff = 0;
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
let diffCellColor = '';
|
|
901
|
+
if (totalDiff > 0) {
|
|
902
|
+
diffCellColor = palette.badColor.rgb().string();
|
|
903
|
+
} else if (totalDiff < 0) {
|
|
904
|
+
diffCellColor = palette.goodColor.rgb().string();
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
let diffValue = '';
|
|
908
|
+
if (x.totalLeft === 0 && x.totalRght > 0) {
|
|
909
|
+
diffValue = i18n.diffNew;
|
|
910
|
+
} else if (x.totalRght === 0 && x.totalLeft > 0) {
|
|
911
|
+
diffValue = i18n.diffRemoved;
|
|
912
|
+
} else if (totalDiff === 0) {
|
|
913
|
+
diffValue = '0%';
|
|
914
|
+
} else if (totalDiff > 0) {
|
|
915
|
+
diffValue = `+${totalDiff.toFixed(2)}%`;
|
|
916
|
+
} else if (totalDiff < 0) {
|
|
917
|
+
diffValue = `${totalDiff.toFixed(2)}%`;
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
return {
|
|
921
|
+
'data-row': `double;${x.name};${x.totalLeft};${x.leftTicks};${x.totalRght};${x.rightTicks}`,
|
|
922
|
+
isRowSelected: isRowSelected(x.name),
|
|
923
|
+
onClick: () => {
|
|
924
|
+
// eslint-disable-next-line no-console
|
|
925
|
+
console.time(`[profiler-table] click ${x.name}`);
|
|
926
|
+
handleTableItemClick({ name: x.name });
|
|
927
|
+
// eslint-disable-next-line no-console
|
|
928
|
+
console.timeEnd(`[profiler-table] click ${x.name}`);
|
|
929
|
+
},
|
|
930
|
+
onContextMenu: (e: React.MouseEvent) => handleContextMenu(e, x.name),
|
|
931
|
+
onDoubleClick: () => handleDoubleClick(x.name),
|
|
932
|
+
cells: [
|
|
933
|
+
{ value: nameCell(x.name) },
|
|
934
|
+
{ value: `${leftPercent} %` },
|
|
935
|
+
{ value: `${rghtPercent} %` },
|
|
936
|
+
{
|
|
937
|
+
value: diffValue,
|
|
938
|
+
style: {
|
|
939
|
+
color: diffCellColor,
|
|
940
|
+
},
|
|
941
|
+
},
|
|
942
|
+
],
|
|
943
|
+
};
|
|
944
|
+
},
|
|
945
|
+
[
|
|
946
|
+
handleContextMenu,
|
|
947
|
+
handleDoubleClick,
|
|
948
|
+
handleTableItemClick,
|
|
949
|
+
i18n,
|
|
950
|
+
isRowSelected,
|
|
951
|
+
nameCell,
|
|
952
|
+
palette,
|
|
953
|
+
]
|
|
954
|
+
);
|
|
955
|
+
|
|
956
|
+
const visibleRows = useMemo(() => {
|
|
957
|
+
if (!useWorker) {
|
|
958
|
+
return [];
|
|
959
|
+
}
|
|
960
|
+
return visibleRowData.map((row) =>
|
|
961
|
+
row.type === 'double'
|
|
962
|
+
? buildDoubleRow(row)
|
|
963
|
+
: buildSingleRow(row)
|
|
964
|
+
);
|
|
965
|
+
}, [buildDoubleRow, buildSingleRow, useWorker, visibleRowData]);
|
|
966
|
+
|
|
967
|
+
const table = useMemo(() => {
|
|
968
|
+
if (useWorker) {
|
|
969
|
+
if (workerReady && rowCount === 0) {
|
|
970
|
+
return {
|
|
971
|
+
headRow: tableFormat as any,
|
|
972
|
+
type: 'not-filled' as const,
|
|
973
|
+
value: (
|
|
974
|
+
<div className="unsupported-format">{i18n.noItemsFound}</div>
|
|
975
|
+
),
|
|
976
|
+
};
|
|
977
|
+
}
|
|
978
|
+
return {
|
|
979
|
+
headRow: tableFormat as any,
|
|
980
|
+
type: 'virtual' as const,
|
|
981
|
+
rowCount,
|
|
982
|
+
rows: visibleRows,
|
|
983
|
+
range: virtualRange,
|
|
984
|
+
onRangeChange: handleRangeChange,
|
|
985
|
+
};
|
|
986
|
+
}
|
|
987
|
+
return {
|
|
988
|
+
headRow: tableFormat as any,
|
|
989
|
+
...getTableBody({
|
|
990
|
+
flamebearer,
|
|
991
|
+
sortBy: tableSortProps.sortBy,
|
|
992
|
+
sortByDirection: tableSortProps.sortByDirection,
|
|
993
|
+
fitMode,
|
|
994
|
+
handleTableItemClick,
|
|
995
|
+
highlightQuery,
|
|
996
|
+
palette,
|
|
997
|
+
selectedItem,
|
|
998
|
+
messages: i18n,
|
|
999
|
+
onRowContextMenu: handleContextMenu,
|
|
1000
|
+
onRowDoubleClick: handleDoubleClick,
|
|
1001
|
+
onFocusOnNode,
|
|
1002
|
+
updateView,
|
|
1003
|
+
enableSandwichView,
|
|
1004
|
+
}),
|
|
1005
|
+
};
|
|
1006
|
+
}, [
|
|
1007
|
+
buildDoubleRow,
|
|
1008
|
+
buildSingleRow,
|
|
1009
|
+
enableSandwichView,
|
|
1010
|
+
fitMode,
|
|
1011
|
+
flamebearer,
|
|
1012
|
+
handleContextMenu,
|
|
1013
|
+
handleDoubleClick,
|
|
1014
|
+
handleRangeChange,
|
|
1015
|
+
handleTableItemClick,
|
|
1016
|
+
highlightQuery,
|
|
1017
|
+
i18n,
|
|
1018
|
+
onFocusOnNode,
|
|
1019
|
+
palette,
|
|
1020
|
+
rowCount,
|
|
1021
|
+
selectedItem,
|
|
1022
|
+
tableFormat,
|
|
1023
|
+
tableSortProps.sortBy,
|
|
1024
|
+
tableSortProps.sortByDirection,
|
|
1025
|
+
updateView,
|
|
1026
|
+
useWorker,
|
|
1027
|
+
visibleRows,
|
|
1028
|
+
virtualRange,
|
|
1029
|
+
workerReady,
|
|
1030
|
+
]);
|
|
612
1031
|
|
|
613
1032
|
const isHighlighted =
|
|
614
1033
|
selectedItem.isJust && selectedItem.value === contextMenuTarget;
|
|
@@ -620,9 +1039,14 @@ function Table({
|
|
|
620
1039
|
{...tableSortProps}
|
|
621
1040
|
tableBodyRef={tableBodyRef}
|
|
622
1041
|
table={table}
|
|
1042
|
+
isLoading={useWorker && !workerReady}
|
|
623
1043
|
className={cl('flamegraph-table', {
|
|
624
1044
|
'flamegraph-table-doubles': isDoubles,
|
|
625
1045
|
})}
|
|
1046
|
+
virtualize={virtualize}
|
|
1047
|
+
virtualizeRowHeight={virtualizeRowHeight}
|
|
1048
|
+
virtualizeOverscan={virtualizeOverscan}
|
|
1049
|
+
scrollRef={scrollRef}
|
|
626
1050
|
/>
|
|
627
1051
|
|
|
628
1052
|
{/* 右键菜单 */}
|
|
@@ -670,8 +1094,10 @@ const ProfilerTable = React.memo(function ProfilerTable({
|
|
|
670
1094
|
}: Omit<ProfilerTableProps, 'tableBodyRef'>) {
|
|
671
1095
|
const tableBodyRef = useRef<HTMLTableSectionElement>(null);
|
|
672
1096
|
const wrapperRef = useRef<HTMLDivElement>(null);
|
|
1097
|
+
const scrollRef = useRef<HTMLDivElement>(null);
|
|
673
1098
|
const [scrollbarSize, setScrollbarSize] = useState(0);
|
|
674
1099
|
const isDoubles = flamebearer.format === 'double';
|
|
1100
|
+
const shouldVirtualize = true;
|
|
675
1101
|
|
|
676
1102
|
useEffect(() => {
|
|
677
1103
|
const bodyEl = tableBodyRef.current;
|
|
@@ -698,7 +1124,7 @@ const ProfilerTable = React.memo(function ProfilerTable({
|
|
|
698
1124
|
}
|
|
699
1125
|
>
|
|
700
1126
|
{/* 内部滚动容器:只控制表格本身的高度和滚动 */}
|
|
701
|
-
<div className="flamegraph-table-scroll">
|
|
1127
|
+
<div className="flamegraph-table-scroll" ref={scrollRef}>
|
|
702
1128
|
<Table
|
|
703
1129
|
tableBodyRef={tableBodyRef}
|
|
704
1130
|
flamebearer={flamebearer}
|
|
@@ -711,6 +1137,10 @@ const ProfilerTable = React.memo(function ProfilerTable({
|
|
|
711
1137
|
onFocusOnNode={onFocusOnNode}
|
|
712
1138
|
updateView={updateView}
|
|
713
1139
|
enableSandwichView={enableSandwichView}
|
|
1140
|
+
virtualize={shouldVirtualize}
|
|
1141
|
+
virtualizeRowHeight={26}
|
|
1142
|
+
virtualizeOverscan={8}
|
|
1143
|
+
scrollRef={scrollRef}
|
|
714
1144
|
/>
|
|
715
1145
|
</div>
|
|
716
1146
|
|
|
@@ -134,3 +134,53 @@
|
|
|
134
134
|
border-right-color: var(--ps-immutable-linked-border);
|
|
135
135
|
border-bottom-color: var(--ps-immutable-linked-border);
|
|
136
136
|
}
|
|
137
|
+
|
|
138
|
+
.searchIcon {
|
|
139
|
+
position: absolute;
|
|
140
|
+
left: 10px;
|
|
141
|
+
top: 50%;
|
|
142
|
+
transform: translateY(-50%);
|
|
143
|
+
color: var(--ps-toolbar-icon-color);
|
|
144
|
+
font-size: 12px;
|
|
145
|
+
pointer-events: none;
|
|
146
|
+
display: inline-flex;
|
|
147
|
+
align-items: center;
|
|
148
|
+
z-index: 2;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
:global([data-theme='kylin']) {
|
|
152
|
+
.search {
|
|
153
|
+
height: 28px;
|
|
154
|
+
border-radius: 4px;
|
|
155
|
+
padding: 0 10px 0 28px;
|
|
156
|
+
font-size: 12px;
|
|
157
|
+
color: #262626;
|
|
158
|
+
background: #ffffff;
|
|
159
|
+
border-color: #d9d9d9;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
.search::placeholder {
|
|
163
|
+
color: #8c8c8c;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
.search:hover {
|
|
167
|
+
border-color: #3c7150;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
.search:focus-visible,
|
|
171
|
+
.search:focus {
|
|
172
|
+
border-color: #3c7150;
|
|
173
|
+
box-shadow: 0 0 0 2px rgba(60, 113, 80, 0.2);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.searchIcon {
|
|
177
|
+
color: #8c8c8c;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
.sync,
|
|
181
|
+
.syncSelected {
|
|
182
|
+
height: 28px;
|
|
183
|
+
width: 28px;
|
|
184
|
+
border-radius: 4px;
|
|
185
|
+
}
|
|
186
|
+
}
|
package/src/SharedQueryInput.tsx
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/* eslint-disable no-unused-expressions */
|
|
2
|
-
import React, { useEffect, useMemo, ChangeEvent, useRef } from 'react';
|
|
2
|
+
import React, { useEffect, useMemo, ChangeEvent, useRef, useState } from 'react';
|
|
3
3
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|
4
4
|
import { faLink } from '@fortawesome/free-solid-svg-icons/faLink';
|
|
5
|
+
import { faSearch } from '@fortawesome/free-solid-svg-icons/faSearch';
|
|
5
6
|
import Input from './shims/Input';
|
|
6
7
|
import { Tooltip } from './shims/Tooltip';
|
|
7
8
|
import styles from './SharedQueryInput.module.scss';
|
|
@@ -33,6 +34,8 @@ const SharedQueryInput = ({
|
|
|
33
34
|
}: SharedQueryProps) => {
|
|
34
35
|
const messages = useFlamegraphI18n();
|
|
35
36
|
const prevSyncEnabled = usePreviousSyncEnabled(sharedQuery?.syncEnabled);
|
|
37
|
+
const wrapperRef = useRef<HTMLDivElement | null>(null);
|
|
38
|
+
const [isKylin, setIsKylin] = useState(false);
|
|
36
39
|
|
|
37
40
|
const onQueryChange = (e: ChangeEvent<HTMLInputElement>) => {
|
|
38
41
|
onHighlightChange(e.target.value);
|
|
@@ -58,6 +61,15 @@ const SharedQueryInput = ({
|
|
|
58
61
|
}
|
|
59
62
|
}, [sharedQuery?.searchQuery, sharedQuery?.syncEnabled]);
|
|
60
63
|
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
const el = wrapperRef.current;
|
|
66
|
+
if (!el) return;
|
|
67
|
+
const kylinEl = el.closest(
|
|
68
|
+
"[data-theme='kylin'], [data-flamegraph-color-mode='kylin']"
|
|
69
|
+
);
|
|
70
|
+
setIsKylin(!!kylinEl);
|
|
71
|
+
}, []);
|
|
72
|
+
|
|
61
73
|
const onToggleSync = () => {
|
|
62
74
|
const newValue = sharedQuery?.syncEnabled ? false : sharedQuery?.id;
|
|
63
75
|
sharedQuery?.toggleSync(newValue as string | false);
|
|
@@ -91,13 +103,16 @@ const SharedQueryInput = ({
|
|
|
91
103
|
: messages.syncSearchBars;
|
|
92
104
|
|
|
93
105
|
return (
|
|
94
|
-
<div className={styles.wrapper} style={{ width }}>
|
|
106
|
+
<div ref={wrapperRef} className={styles.wrapper} style={{ width }}>
|
|
107
|
+
<span className={styles.searchIcon}>
|
|
108
|
+
<FontAwesomeIcon icon={faSearch} />
|
|
109
|
+
</span>
|
|
95
110
|
<Input
|
|
96
111
|
testId="flamegraph-search"
|
|
97
112
|
className={inputClassName}
|
|
98
113
|
type="search"
|
|
99
114
|
name="flamegraph-search"
|
|
100
|
-
placeholder={messages.searchPlaceholder}
|
|
115
|
+
placeholder={isKylin ? '搜索函数 / 过滤' : messages.searchPlaceholder}
|
|
101
116
|
minLength={1}
|
|
102
117
|
debounceTimeout={100}
|
|
103
118
|
onChange={onQueryChange}
|