@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.
- package/CHANGELOG.md +18 -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 +150 -1
- package/src/FlameGraph/normalize.ts +9 -7
- package/src/FlameGraph/uniqueness.ts +69 -59
- package/src/ProfilerTable.tsx +463 -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 +7 -0
- package/src/workers/flamegraphRenderWorker.ts +198 -0
- 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
|
-
...
|
|
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
|
}
|