@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.
Files changed (65) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/dist/FlameGraph/FlameGraphComponent/DiffLegend.d.ts.map +1 -1
  3. package/dist/FlameGraph/FlameGraphComponent/DiffLegendPaletteDropdown.d.ts.map +1 -1
  4. package/dist/FlameGraph/FlameGraphComponent/Flamegraph.d.ts +16 -2
  5. package/dist/FlameGraph/FlameGraphComponent/Flamegraph.d.ts.map +1 -1
  6. package/dist/FlameGraph/FlameGraphComponent/Flamegraph_render.d.ts +15 -2
  7. package/dist/FlameGraph/FlameGraphComponent/Flamegraph_render.d.ts.map +1 -1
  8. package/dist/FlameGraph/FlameGraphComponent/Highlight.d.ts.map +1 -1
  9. package/dist/FlameGraph/FlameGraphComponent/index.d.ts.map +1 -1
  10. package/dist/FlameGraph/normalize.d.ts.map +1 -1
  11. package/dist/FlameGraph/uniqueness.d.ts.map +1 -1
  12. package/dist/Icons.d.ts.map +1 -1
  13. package/dist/ProfilerTable.d.ts.map +1 -1
  14. package/dist/SharedQueryInput.d.ts.map +1 -1
  15. package/dist/Toolbar.d.ts.map +1 -1
  16. package/dist/Tooltip/Tooltip.d.ts.map +1 -1
  17. package/dist/flamegraphRenderWorker.js +2 -0
  18. package/dist/flamegraphRenderWorker.js.map +1 -0
  19. package/dist/index.cjs.js +4 -4
  20. package/dist/index.cjs.js.map +1 -1
  21. package/dist/index.esm.js +4 -4
  22. package/dist/index.esm.js.map +1 -1
  23. package/dist/index.node.cjs.js +4 -4
  24. package/dist/index.node.cjs.js.map +1 -1
  25. package/dist/index.node.esm.js +4 -4
  26. package/dist/index.node.esm.js.map +1 -1
  27. package/dist/shims/Table.d.ts +15 -1
  28. package/dist/shims/Table.d.ts.map +1 -1
  29. package/dist/shims/Tooltip.d.ts.map +1 -1
  30. package/dist/workers/createFlamegraphRenderWorker.d.ts +2 -0
  31. package/dist/workers/createFlamegraphRenderWorker.d.ts.map +1 -0
  32. package/dist/workers/flamegraphRenderWorker.d.ts +2 -0
  33. package/dist/workers/flamegraphRenderWorker.d.ts.map +1 -0
  34. package/dist/workers/profilerTableWorker.d.ts +73 -0
  35. package/dist/workers/profilerTableWorker.d.ts.map +1 -0
  36. package/package.json +1 -1
  37. package/src/FlameGraph/FlameGraphComponent/DiffLegend.module.css +8 -2
  38. package/src/FlameGraph/FlameGraphComponent/DiffLegend.tsx +12 -1
  39. package/src/FlameGraph/FlameGraphComponent/DiffLegendPaletteDropdown.module.css +93 -10
  40. package/src/FlameGraph/FlameGraphComponent/DiffLegendPaletteDropdown.tsx +9 -4
  41. package/src/FlameGraph/FlameGraphComponent/Flamegraph.ts +33 -8
  42. package/src/FlameGraph/FlameGraphComponent/Flamegraph_render.ts +289 -85
  43. package/src/FlameGraph/FlameGraphComponent/Highlight.tsx +43 -17
  44. package/src/FlameGraph/FlameGraphComponent/index.tsx +208 -57
  45. package/src/FlameGraph/FlameGraphComponent/styles.module.scss +8 -0
  46. package/src/FlameGraph/normalize.ts +9 -7
  47. package/src/FlameGraph/uniqueness.ts +69 -59
  48. package/src/Icons.tsx +18 -9
  49. package/src/ProfilerTable.tsx +463 -33
  50. package/src/SharedQueryInput.module.scss +50 -0
  51. package/src/SharedQueryInput.tsx +18 -3
  52. package/src/Toolbar.module.scss +90 -0
  53. package/src/Toolbar.tsx +30 -16
  54. package/src/Tooltip/Tooltip.tsx +49 -16
  55. package/src/i18n.tsx +1 -1
  56. package/src/sass/_common.scss +22 -3
  57. package/src/sass/_css-variables.scss +5 -1
  58. package/src/sass/flamegraph.scss +26 -23
  59. package/src/shims/Table.module.scss +91 -13
  60. package/src/shims/Table.tsx +202 -7
  61. package/src/shims/Tooltip.module.scss +40 -0
  62. package/src/shims/Tooltip.tsx +31 -3
  63. package/src/workers/createFlamegraphRenderWorker.ts +7 -0
  64. package/src/workers/flamegraphRenderWorker.ts +198 -0
  65. package/src/workers/profilerTableWorker.ts +368 -0
@@ -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 = generateTable(flamebearer).sort(
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
- }: ProfilerTableProps & { isDoubles: boolean }) {
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
- if (!onFocusOnNode) return;
487
-
488
- const node = findNodeByName(flamebearer, name);
489
- if (node) {
490
- onFocusOnNode(node.i, node.j);
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
- [flamebearer, onFocusOnNode, selectedItem, handleTableItemClick]
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 || !onFocusOnNode) return;
515
-
516
- const node = findNodeByName(flamebearer, contextMenuTarget);
517
- if (node) {
518
- onFocusOnNode(node.i, node.j);
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, flamebearer, onFocusOnNode, handleMenuClose]);
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
- const table = {
594
- headRow: tableFormat as any,
595
- ...getTableBody({
596
- flamebearer,
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
- selectedItem,
604
- messages: i18n,
605
- onRowContextMenu: handleContextMenu,
606
- onRowDoubleClick: handleDoubleClick,
607
- onFocusOnNode,
608
- updateView,
609
- enableSandwichView,
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
+ }
@@ -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}