@kylincloud/flamegraph 0.35.28 → 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 (43) hide show
  1. package/CHANGELOG.md +18 -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 +150 -1
  35. package/src/FlameGraph/normalize.ts +9 -7
  36. package/src/FlameGraph/uniqueness.ts +69 -59
  37. package/src/ProfilerTable.tsx +463 -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 +7 -0
  42. package/src/workers/flamegraphRenderWorker.ts +198 -0
  43. package/src/workers/profilerTableWorker.ts +368 -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,20 @@ 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 renderSeq = useRef(0);
84
+ const renderWorkerRef = useRef<Worker | null>(null);
85
+ const rectOffscreenRef = useRef<OffscreenCanvas | null>(null);
86
+ const textOffscreenRef = useRef<OffscreenCanvas | null>(null);
87
+ const workerReadyRef = useRef({ rect: false, text: false });
81
88
  const i18n = useFlamegraphI18n();
82
89
  const resizeLogRef = useRef({
83
90
  lastWidth: 0,
84
91
  lastHeight: 0,
85
92
  });
93
+ const useRenderWorker =
94
+ typeof window !== 'undefined' &&
95
+ typeof OffscreenCanvas !== 'undefined' &&
96
+ 'transferControlToOffscreen' in HTMLCanvasElement.prototype;
86
97
 
87
98
  // ====== 新增:提取 canvas 渲染需要的 i18n messages ======
88
99
  const canvasMessages = useMemo(
@@ -94,6 +105,19 @@ export default function FlameGraphComponent(props: FlamegraphProps) {
94
105
  [i18n.collapsedLevelsSingular, i18n.collapsedLevelsPlural, i18n.location]
95
106
  );
96
107
 
108
+ const serializePalette = useCallback(
109
+ (p: FlamegraphPalette) => ({
110
+ name: p.name,
111
+ goodColor: p.goodColor.rgb().array() as [number, number, number],
112
+ neutralColor: p.neutralColor.rgb().array() as [number, number, number],
113
+ badColor: p.badColor.rgb().array() as [number, number, number],
114
+ colors: p.colors.map(
115
+ (c) => c.rgb().array() as [number, number, number]
116
+ ),
117
+ }),
118
+ []
119
+ );
120
+
97
121
  const [rightClickedNode, setRightClickedNode] = React.useState<
98
122
  Maybe<{ top: number; left: number; width: number }>
99
123
  >(Maybe.nothing());
@@ -129,6 +153,29 @@ export default function FlameGraphComponent(props: FlamegraphProps) {
129
153
  []
130
154
  );
131
155
 
156
+ useEffect(() => {
157
+ if (!useRenderWorker) {
158
+ return () => {};
159
+ }
160
+ const worker = createFlamegraphRenderWorker();
161
+ renderWorkerRef.current = worker;
162
+ // eslint-disable-next-line no-console
163
+ console.debug('[flamegraph] render worker created');
164
+ setTimeout(() => {
165
+ renderRectCanvas();
166
+ renderTextCanvas();
167
+ }, 0);
168
+ return () => {
169
+ worker.terminate();
170
+ renderWorkerRef.current = null;
171
+ rectOffscreenRef.current = null;
172
+ textOffscreenRef.current = null;
173
+ workerReadyRef.current = { rect: false, text: false };
174
+ // eslint-disable-next-line no-console
175
+ console.debug('[flamegraph] render worker terminated');
176
+ };
177
+ }, [useRenderWorker]);
178
+
132
179
  useResizeObserver(canvasRef, () => {
133
180
  if (flamegraph) {
134
181
  if (canvasRef.current) {
@@ -328,6 +375,10 @@ export default function FlameGraphComponent(props: FlamegraphProps) {
328
375
  );
329
376
 
330
377
  const constructCanvas = () => {
378
+ const seq = renderSeq.current + 1;
379
+ renderSeq.current = seq;
380
+ // eslint-disable-next-line no-console
381
+ console.time(`[flamegraph] constructCanvas #${seq}`);
331
382
  if (canvasRef.current) {
332
383
  const f = new Flamegraph(
333
384
  flamebearer,
@@ -355,8 +406,49 @@ export default function FlameGraphComponent(props: FlamegraphProps) {
355
406
  );
356
407
  flamegraphText.current = f;
357
408
  }
409
+ // eslint-disable-next-line no-console
410
+ console.timeEnd(`[flamegraph] constructCanvas #${seq}`);
358
411
  };
359
412
 
413
+ const ensureRenderWorkerCanvases = useCallback(() => {
414
+ if (!useRenderWorker || !renderWorkerRef.current) {
415
+ return false;
416
+ }
417
+ const worker = renderWorkerRef.current;
418
+
419
+ if (canvasRef.current && !rectOffscreenRef.current) {
420
+ try {
421
+ const offscreen = canvasRef.current.transferControlToOffscreen();
422
+ rectOffscreenRef.current = offscreen;
423
+ worker.postMessage(
424
+ { type: 'init', payload: { kind: 'rect', canvas: offscreen } },
425
+ [offscreen]
426
+ );
427
+ workerReadyRef.current.rect = true;
428
+ } catch (err) {
429
+ // eslint-disable-next-line no-console
430
+ console.debug('[flamegraph] rect transfer failed', err);
431
+ }
432
+ }
433
+
434
+ if (textCanvasRef.current && !textOffscreenRef.current) {
435
+ try {
436
+ const offscreen = textCanvasRef.current.transferControlToOffscreen();
437
+ textOffscreenRef.current = offscreen;
438
+ worker.postMessage(
439
+ { type: 'init', payload: { kind: 'text', canvas: offscreen } },
440
+ [offscreen]
441
+ );
442
+ workerReadyRef.current.text = true;
443
+ } catch (err) {
444
+ // eslint-disable-next-line no-console
445
+ console.debug('[flamegraph] text transfer failed', err);
446
+ }
447
+ }
448
+
449
+ return true;
450
+ }, [useRenderWorker]);
451
+
360
452
  // ====== 修改:添加 canvasMessages 依赖 ======
361
453
  React.useEffect(() => {
362
454
  constructCanvas();
@@ -382,16 +474,73 @@ export default function FlameGraphComponent(props: FlamegraphProps) {
382
474
  renderTextCanvas();
383
475
  }, [highlightQuery]);
384
476
 
477
+ const postWorkerRender = (
478
+ kind: 'rect' | 'text',
479
+ options: { renderRects?: boolean; renderText?: boolean }
480
+ ) => {
481
+ if (!useRenderWorker) {
482
+ return false;
483
+ }
484
+ if (!renderWorkerRef.current) {
485
+ // eslint-disable-next-line no-console
486
+ console.debug('[flamegraph] worker not ready yet');
487
+ return true;
488
+ }
489
+ if (!ensureRenderWorkerCanvases()) {
490
+ return true;
491
+ }
492
+ const canvasEl =
493
+ kind === 'rect' ? canvasRef.current : textCanvasRef.current;
494
+ const width = canvasEl?.clientWidth || canvasEl?.width || 0;
495
+ const payload = {
496
+ kind,
497
+ flamebearer,
498
+ focusedNode: focusedNode.isJust ? focusedNode.value : null,
499
+ fitMode,
500
+ highlightQuery,
501
+ zoom: zoom.isJust ? zoom.value : null,
502
+ palette: serializePalette(palette),
503
+ messages: canvasMessages as CanvasI18nMessages,
504
+ renderRects: options.renderRects,
505
+ renderText: options.renderText,
506
+ width,
507
+ devicePixelRatio:
508
+ typeof window !== 'undefined' ? window.devicePixelRatio || 1 : 1,
509
+ };
510
+ renderWorkerRef.current.postMessage({ type: 'render', payload });
511
+ return true;
512
+ };
513
+
385
514
  const renderRectCanvas = () => {
515
+ const seq = renderSeq.current;
516
+ if (postWorkerRender('rect', { renderText: false })) {
517
+ // eslint-disable-next-line no-console
518
+ console.debug(`[flamegraph] renderRects via worker #${seq}`);
519
+ return;
520
+ }
521
+ // eslint-disable-next-line no-console
522
+ console.time(`[flamegraph] renderRects #${seq}`);
386
523
  canvasRef?.current?.setAttribute('data-state', 'rendering');
387
524
  flamegraph?.current?.render({ renderText: false });
388
525
  canvasRef?.current?.setAttribute('data-state', 'rendered');
526
+ // eslint-disable-next-line no-console
527
+ console.timeEnd(`[flamegraph] renderRects #${seq}`);
389
528
  };
390
529
 
391
530
  const renderTextCanvas = () => {
531
+ const seq = renderSeq.current;
532
+ if (postWorkerRender('text', { renderRects: false })) {
533
+ // eslint-disable-next-line no-console
534
+ console.debug(`[flamegraph] renderText via worker #${seq}`);
535
+ return;
536
+ }
537
+ // eslint-disable-next-line no-console
538
+ console.time(`[flamegraph] renderText #${seq}`);
392
539
  textCanvasRef?.current?.setAttribute('data-state', 'rendering');
393
540
  flamegraphText?.current?.render({ renderRects: false });
394
541
  textCanvasRef?.current?.setAttribute('data-state', 'rendered');
542
+ // eslint-disable-next-line no-console
543
+ console.timeEnd(`[flamegraph] renderText #${seq}`);
395
544
  };
396
545
 
397
546
  const dataUnavailable =
@@ -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
  }