@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
|
@@ -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
|
-
...
|
|
19
|
-
flamebearer: { ...
|
|
19
|
+
...profile,
|
|
20
|
+
flamebearer: { ...profile.flamebearer },
|
|
20
21
|
};
|
|
21
22
|
|
|
22
|
-
//
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
const prevSig = getFlamebearerSignature(prevFlame);
|
|
8
|
+
const currSig = getFlamebearerSignature(currFlame);
|
|
9
|
+
return prevSig === currSig;
|
|
10
|
+
}
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
17
|
-
|
|
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
|
-
|
|
21
|
-
|
|
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
|
-
|
|
25
|
-
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
}
|
|
40
|
+
return hash >>> 0;
|
|
41
|
+
}
|
|
31
42
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
43
|
-
|
|
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
|
-
|
|
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
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
}
|
|
73
|
+
const extra = flamebearer as Flamebearer & {
|
|
74
|
+
leftTicks?: number;
|
|
75
|
+
rightTicks?: number;
|
|
76
|
+
};
|
|
74
77
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
92
|
+
(flamebearer as any).__kylinSignature = signature;
|
|
93
|
+
return signature;
|
|
84
94
|
}
|