@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.
Files changed (43) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/dist/FlameGraph/FlameGraphComponent/Flamegraph.d.ts +16 -2
  3. package/dist/FlameGraph/FlameGraphComponent/Flamegraph.d.ts.map +1 -1
  4. package/dist/FlameGraph/FlameGraphComponent/Flamegraph_render.d.ts +15 -2
  5. package/dist/FlameGraph/FlameGraphComponent/Flamegraph_render.d.ts.map +1 -1
  6. package/dist/FlameGraph/FlameGraphComponent/Highlight.d.ts.map +1 -1
  7. package/dist/FlameGraph/FlameGraphComponent/index.d.ts.map +1 -1
  8. package/dist/FlameGraph/normalize.d.ts.map +1 -1
  9. package/dist/FlameGraph/uniqueness.d.ts.map +1 -1
  10. package/dist/ProfilerTable.d.ts.map +1 -1
  11. package/dist/Tooltip/Tooltip.d.ts.map +1 -1
  12. package/dist/flamegraphRenderWorker.js +2 -0
  13. package/dist/flamegraphRenderWorker.js.map +1 -0
  14. package/dist/index.cjs.js +4 -4
  15. package/dist/index.cjs.js.map +1 -1
  16. package/dist/index.esm.js +4 -4
  17. package/dist/index.esm.js.map +1 -1
  18. package/dist/index.node.cjs.js +4 -4
  19. package/dist/index.node.cjs.js.map +1 -1
  20. package/dist/index.node.esm.js +4 -4
  21. package/dist/index.node.esm.js.map +1 -1
  22. package/dist/shims/Table.d.ts +15 -1
  23. package/dist/shims/Table.d.ts.map +1 -1
  24. package/dist/workers/createFlamegraphRenderWorker.d.ts +2 -0
  25. package/dist/workers/createFlamegraphRenderWorker.d.ts.map +1 -0
  26. package/dist/workers/flamegraphRenderWorker.d.ts +2 -0
  27. package/dist/workers/flamegraphRenderWorker.d.ts.map +1 -0
  28. package/dist/workers/profilerTableWorker.d.ts +73 -0
  29. package/dist/workers/profilerTableWorker.d.ts.map +1 -0
  30. package/package.json +1 -1
  31. package/src/FlameGraph/FlameGraphComponent/Flamegraph.ts +33 -8
  32. package/src/FlameGraph/FlameGraphComponent/Flamegraph_render.ts +289 -85
  33. package/src/FlameGraph/FlameGraphComponent/Highlight.tsx +43 -17
  34. package/src/FlameGraph/FlameGraphComponent/index.tsx +119 -1
  35. package/src/FlameGraph/normalize.ts +9 -7
  36. package/src/FlameGraph/uniqueness.ts +69 -59
  37. package/src/ProfilerTable.tsx +421 -33
  38. package/src/Tooltip/Tooltip.tsx +49 -16
  39. package/src/shims/Table.module.scss +5 -0
  40. package/src/shims/Table.tsx +195 -5
  41. package/src/workers/createFlamegraphRenderWorker.ts +23 -0
  42. package/src/workers/flamegraphRenderWorker.ts +192 -0
  43. package/src/workers/profilerTableWorker.ts +342 -0
@@ -1,6 +1,6 @@
1
1
  // src/FlameGraph/FlameGraphComponent/index.tsx
2
2
  /* eslint-disable no-unused-expressions, import/no-extraneous-dependencies */
3
- import React, { useCallback, useRef, useMemo } from 'react';
3
+ import React, { useCallback, useRef, useMemo, useEffect } from 'react';
4
4
  import clsx from 'clsx';
5
5
  import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
6
6
  import { faRedo } from '@fortawesome/free-solid-svg-icons/faRedo';
@@ -28,6 +28,8 @@ import type { ViewTypes } from './viewTypes';
28
28
  import { FitModes, HeadMode, TailMode } from '../../fitMode/fitMode';
29
29
  import indexStyles from './styles.module.scss';
30
30
  import { useFlamegraphI18n } from '../../i18n';
31
+ import { createFlamegraphRenderWorker } from '../../workers/createFlamegraphRenderWorker';
32
+ import type { CanvasI18nMessages } from './Flamegraph_render';
31
33
 
32
34
  interface FlamegraphProps {
33
35
  flamebearer: Flamebearer;
@@ -78,11 +80,19 @@ export default function FlameGraphComponent(props: FlamegraphProps) {
78
80
  const textCanvasRef = React.useRef<HTMLCanvasElement>(null);
79
81
  const flamegraph = useRef<Flamegraph>();
80
82
  const flamegraphText = useRef<Flamegraph>();
83
+ const renderWorkerRef = useRef<Worker | null>(null);
84
+ const rectOffscreenRef = useRef<OffscreenCanvas | null>(null);
85
+ const textOffscreenRef = useRef<OffscreenCanvas | null>(null);
86
+ const workerReadyRef = useRef({ rect: false, text: false });
81
87
  const i18n = useFlamegraphI18n();
82
88
  const resizeLogRef = useRef({
83
89
  lastWidth: 0,
84
90
  lastHeight: 0,
85
91
  });
92
+ const useRenderWorker =
93
+ typeof window !== 'undefined' &&
94
+ typeof OffscreenCanvas !== 'undefined' &&
95
+ 'transferControlToOffscreen' in HTMLCanvasElement.prototype;
86
96
 
87
97
  // ====== 新增:提取 canvas 渲染需要的 i18n messages ======
88
98
  const canvasMessages = useMemo(
@@ -94,6 +104,19 @@ export default function FlameGraphComponent(props: FlamegraphProps) {
94
104
  [i18n.collapsedLevelsSingular, i18n.collapsedLevelsPlural, i18n.location]
95
105
  );
96
106
 
107
+ const serializePalette = useCallback(
108
+ (p: FlamegraphPalette) => ({
109
+ name: p.name,
110
+ goodColor: p.goodColor.rgb().array() as [number, number, number],
111
+ neutralColor: p.neutralColor.rgb().array() as [number, number, number],
112
+ badColor: p.badColor.rgb().array() as [number, number, number],
113
+ colors: p.colors.map(
114
+ (c) => c.rgb().array() as [number, number, number]
115
+ ),
116
+ }),
117
+ []
118
+ );
119
+
97
120
  const [rightClickedNode, setRightClickedNode] = React.useState<
98
121
  Maybe<{ top: number; left: number; width: number }>
99
122
  >(Maybe.nothing());
@@ -129,6 +152,25 @@ export default function FlameGraphComponent(props: FlamegraphProps) {
129
152
  []
130
153
  );
131
154
 
155
+ useEffect(() => {
156
+ if (!useRenderWorker) {
157
+ return () => {};
158
+ }
159
+ const worker = createFlamegraphRenderWorker();
160
+ renderWorkerRef.current = worker;
161
+ setTimeout(() => {
162
+ renderRectCanvas();
163
+ renderTextCanvas();
164
+ }, 0);
165
+ return () => {
166
+ worker.terminate();
167
+ renderWorkerRef.current = null;
168
+ rectOffscreenRef.current = null;
169
+ textOffscreenRef.current = null;
170
+ workerReadyRef.current = { rect: false, text: false };
171
+ };
172
+ }, [useRenderWorker]);
173
+
132
174
  useResizeObserver(canvasRef, () => {
133
175
  if (flamegraph) {
134
176
  if (canvasRef.current) {
@@ -357,6 +399,41 @@ export default function FlameGraphComponent(props: FlamegraphProps) {
357
399
  }
358
400
  };
359
401
 
402
+ const ensureRenderWorkerCanvases = useCallback(() => {
403
+ if (!useRenderWorker || !renderWorkerRef.current) {
404
+ return false;
405
+ }
406
+ const worker = renderWorkerRef.current;
407
+
408
+ if (canvasRef.current && !rectOffscreenRef.current) {
409
+ try {
410
+ const offscreen = canvasRef.current.transferControlToOffscreen();
411
+ rectOffscreenRef.current = offscreen;
412
+ worker.postMessage(
413
+ { type: 'init', payload: { kind: 'rect', canvas: offscreen } },
414
+ [offscreen]
415
+ );
416
+ workerReadyRef.current.rect = true;
417
+ } catch (err) {
418
+ }
419
+ }
420
+
421
+ if (textCanvasRef.current && !textOffscreenRef.current) {
422
+ try {
423
+ const offscreen = textCanvasRef.current.transferControlToOffscreen();
424
+ textOffscreenRef.current = offscreen;
425
+ worker.postMessage(
426
+ { type: 'init', payload: { kind: 'text', canvas: offscreen } },
427
+ [offscreen]
428
+ );
429
+ workerReadyRef.current.text = true;
430
+ } catch (err) {
431
+ }
432
+ }
433
+
434
+ return true;
435
+ }, [useRenderWorker]);
436
+
360
437
  // ====== 修改:添加 canvasMessages 依赖 ======
361
438
  React.useEffect(() => {
362
439
  constructCanvas();
@@ -382,13 +459,54 @@ export default function FlameGraphComponent(props: FlamegraphProps) {
382
459
  renderTextCanvas();
383
460
  }, [highlightQuery]);
384
461
 
462
+ const postWorkerRender = (
463
+ kind: 'rect' | 'text',
464
+ options: { renderRects?: boolean; renderText?: boolean }
465
+ ) => {
466
+ if (!useRenderWorker) {
467
+ return false;
468
+ }
469
+ if (!renderWorkerRef.current) {
470
+ return true;
471
+ }
472
+ if (!ensureRenderWorkerCanvases()) {
473
+ return true;
474
+ }
475
+ const canvasEl =
476
+ kind === 'rect' ? canvasRef.current : textCanvasRef.current;
477
+ const width = canvasEl?.clientWidth || canvasEl?.width || 0;
478
+ const payload = {
479
+ kind,
480
+ flamebearer,
481
+ focusedNode: focusedNode.isJust ? focusedNode.value : null,
482
+ fitMode,
483
+ highlightQuery,
484
+ zoom: zoom.isJust ? zoom.value : null,
485
+ palette: serializePalette(palette),
486
+ messages: canvasMessages as CanvasI18nMessages,
487
+ renderRects: options.renderRects,
488
+ renderText: options.renderText,
489
+ width,
490
+ devicePixelRatio:
491
+ typeof window !== 'undefined' ? window.devicePixelRatio || 1 : 1,
492
+ };
493
+ renderWorkerRef.current.postMessage({ type: 'render', payload });
494
+ return true;
495
+ };
496
+
385
497
  const renderRectCanvas = () => {
498
+ if (postWorkerRender('rect', { renderText: false })) {
499
+ return;
500
+ }
386
501
  canvasRef?.current?.setAttribute('data-state', 'rendering');
387
502
  flamegraph?.current?.render({ renderText: false });
388
503
  canvasRef?.current?.setAttribute('data-state', 'rendered');
389
504
  };
390
505
 
391
506
  const renderTextCanvas = () => {
507
+ if (postWorkerRender('text', { renderRects: false })) {
508
+ return;
509
+ }
392
510
  textCanvasRef?.current?.setAttribute('data-state', 'rendering');
393
511
  flamegraphText?.current?.render({ renderRects: false });
394
512
  textCanvasRef?.current?.setAttribute('data-state', 'rendered');
@@ -14,16 +14,18 @@ export function normalize(p: {
14
14
  }
15
15
 
16
16
  if (p.profile) {
17
+ const profile = p.profile as Profile & { __kylinDecoded?: boolean };
17
18
  const copy = {
18
- ...p.profile,
19
- flamebearer: { ...p.profile.flamebearer },
19
+ ...profile,
20
+ flamebearer: { ...profile.flamebearer },
20
21
  };
21
22
 
22
- // TODO: copy levels, since that's modified by decode
23
- copy.flamebearer.levels = JSON.parse(
24
- JSON.stringify(copy.flamebearer.levels)
25
- );
26
- decodeFlamebearer(copy);
23
+ // Avoid deep-cloning huge levels arrays; decode once in-place.
24
+ if (!profile.__kylinDecoded) {
25
+ decodeFlamebearer(copy);
26
+ profile.__kylinDecoded = true;
27
+ (copy as Profile & { __kylinDecoded?: boolean }).__kylinDecoded = true;
28
+ }
27
29
 
28
30
  const p2 = {
29
31
  ...copy,
@@ -4,81 +4,91 @@ export function isSameFlamebearer(
4
4
  prevFlame: Flamebearer,
5
5
  currFlame: Flamebearer
6
6
  ) {
7
- // We first compare simple fields, since they are faster
8
- if (prevFlame.format !== currFlame.format) {
9
- return false;
10
- }
7
+ const prevSig = getFlamebearerSignature(prevFlame);
8
+ const currSig = getFlamebearerSignature(currFlame);
9
+ return prevSig === currSig;
10
+ }
11
11
 
12
- if (prevFlame.numTicks !== currFlame.numTicks) {
13
- return false;
12
+ function hashNumbersSample(levels: Flamebearer['levels']) {
13
+ let hash = 2166136261;
14
+ const totalLevels = levels.length;
15
+ if (totalLevels === 0) {
16
+ return hash >>> 0;
14
17
  }
15
18
 
16
- if (prevFlame.sampleRate !== currFlame.sampleRate) {
17
- return false;
18
- }
19
+ const sampleLevels = [0, Math.floor(totalLevels / 2), totalLevels - 1];
20
+ for (let s = 0; s < sampleLevels.length; s += 1) {
21
+ const idx = sampleLevels[s];
22
+ const level = levels[idx];
23
+ if (!level || level.length === 0) {
24
+ continue;
25
+ }
19
26
 
20
- if (prevFlame.units !== currFlame.units) {
21
- return false;
22
- }
27
+ const take = Math.min(8, level.length);
28
+ for (let i = 0; i < take; i += 1) {
29
+ hash ^= level[i] | 0;
30
+ hash = Math.imul(hash, 16777619);
31
+ }
23
32
 
24
- if (prevFlame.names?.length !== currFlame?.names.length) {
25
- return false;
33
+ const tailStart = Math.max(0, level.length - take);
34
+ for (let i = tailStart; i < level.length; i += 1) {
35
+ hash ^= level[i] | 0;
36
+ hash = Math.imul(hash, 16777619);
37
+ }
26
38
  }
27
39
 
28
- if (prevFlame.levels.length !== currFlame.levels.length) {
29
- return false;
30
- }
40
+ return hash >>> 0;
41
+ }
31
42
 
32
- // Most likely names is smaller, so let's start with it
33
- // Are all names the same?
34
- if (
35
- !prevFlame.names.every((a, i) => {
36
- return a === currFlame.names[i];
37
- })
38
- ) {
39
- return false;
43
+ function hashStringsSample(names: string[]) {
44
+ let hash = 2166136261;
45
+ const count = names.length;
46
+ if (count === 0) {
47
+ return hash >>> 0;
40
48
  }
41
49
 
42
- if (!areLevelsTheSame(prevFlame.levels, currFlame.levels)) {
43
- return false;
50
+ const take = Math.min(32, count);
51
+ const sampleIndices = [
52
+ ...Array.from({ length: take }, (_, i) => i),
53
+ ...Array.from({ length: take }, (_, i) => count - take + i),
54
+ ];
55
+
56
+ for (let i = 0; i < sampleIndices.length; i += 1) {
57
+ const name = names[sampleIndices[i]] || '';
58
+ for (let j = 0; j < name.length; j += 1) {
59
+ hash ^= name.charCodeAt(j);
60
+ hash = Math.imul(hash, 16777619);
61
+ }
44
62
  }
45
63
 
46
- // Fallback in case new fields are added
47
- return (
48
- JSON.stringify({
49
- ...prevFlame,
50
- levels: undefined,
51
- names: undefined,
52
- }) ===
53
- JSON.stringify({
54
- ...currFlame,
55
- levels: undefined,
56
- names: undefined,
57
- })
58
- );
64
+ return hash >>> 0;
59
65
  }
60
66
 
61
- function areLevelsTheSame(
62
- l1: Flamebearer['levels'],
63
- l2: Flamebearer['levels']
64
- ) {
65
- if (l1.length !== l2.length) {
66
- return false;
67
+ function getFlamebearerSignature(flamebearer: Flamebearer) {
68
+ const cached = (flamebearer as any).__kylinSignature;
69
+ if (cached) {
70
+ return cached;
67
71
  }
68
72
 
69
- // eslint-disable-next-line no-plusplus
70
- for (let i = 0; i < l1.length; i++) {
71
- if (l1[i].length !== l2[i].length) {
72
- return false;
73
- }
73
+ const extra = flamebearer as Flamebearer & {
74
+ leftTicks?: number;
75
+ rightTicks?: number;
76
+ };
74
77
 
75
- // eslint-disable-next-line no-plusplus
76
- for (let j = 0; j < l1[i].length; j++) {
77
- if (l1[i][j] !== l2[i][j]) {
78
- return false;
79
- }
80
- }
81
- }
78
+ const signature = [
79
+ flamebearer.format,
80
+ flamebearer.numTicks,
81
+ flamebearer.sampleRate,
82
+ flamebearer.units,
83
+ flamebearer.maxSelf,
84
+ flamebearer.names?.length ?? 0,
85
+ flamebearer.levels?.length ?? 0,
86
+ extra.leftTicks ?? 'na',
87
+ extra.rightTicks ?? 'na',
88
+ hashNumbersSample(flamebearer.levels),
89
+ hashStringsSample(flamebearer.names || []),
90
+ ].join('|');
82
91
 
83
- return true;
92
+ (flamebearer as any).__kylinSignature = signature;
93
+ return signature;
84
94
  }