@kylincloud/flamegraph 0.35.21 → 0.35.23
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 +16 -0
- package/dist/FlameGraph/FlameGraphComponent/Flamegraph.d.ts +6 -1
- package/dist/FlameGraph/FlameGraphComponent/Flamegraph.d.ts.map +1 -1
- package/dist/FlameGraph/FlameGraphComponent/Flamegraph_render.d.ts +7 -0
- package/dist/FlameGraph/FlameGraphComponent/Flamegraph_render.d.ts.map +1 -1
- package/dist/FlameGraph/FlameGraphComponent/index.d.ts.map +1 -1
- package/dist/FlameGraph/FlameGraphRenderer.d.ts +4 -0
- package/dist/FlameGraph/FlameGraphRenderer.d.ts.map +1 -1
- package/dist/ProfilerTable.d.ts +4 -0
- package/dist/ProfilerTable.d.ts.map +1 -1
- package/dist/i18n.d.ts +5 -0
- package/dist/i18n.d.ts.map +1 -1
- package/dist/index.cjs.js +2 -2
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.esm.js +3 -3
- 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 +4 -2
- package/dist/shims/Table.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/FlameGraph/FlameGraphComponent/Flamegraph.ts +7 -2
- package/src/FlameGraph/FlameGraphComponent/Flamegraph_render.ts +31 -2
- package/src/FlameGraph/FlameGraphComponent/index.tsx +14 -3
- package/src/FlameGraph/FlameGraphRenderer.tsx +74 -6
- package/src/ProfilerTable.module.scss +42 -0
- package/src/ProfilerTable.tsx +234 -55
- package/src/i18n.tsx +27 -0
- package/src/shims/Table.module.scss +9 -2
- package/src/shims/Table.tsx +55 -36
package/dist/shims/Table.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { ReactNode, CSSProperties, RefObject } from 'react';
|
|
1
|
+
import React, { ReactNode, CSSProperties, RefObject } from 'react';
|
|
2
2
|
interface CustomProp {
|
|
3
|
-
[k: string]: string | CSSProperties | ReactNode | number | undefined;
|
|
3
|
+
[k: string]: string | CSSProperties | ReactNode | number | undefined | ((e: React.MouseEvent) => void) | (() => void);
|
|
4
4
|
}
|
|
5
5
|
export interface Cell extends CustomProp {
|
|
6
6
|
value: ReactNode | string;
|
|
@@ -18,6 +18,8 @@ export interface BodyRow {
|
|
|
18
18
|
isRowDisabled?: boolean;
|
|
19
19
|
cells: Cell[];
|
|
20
20
|
onClick?: () => void;
|
|
21
|
+
onDoubleClick?: () => void;
|
|
22
|
+
onContextMenu?: (e: React.MouseEvent) => void;
|
|
21
23
|
className?: string;
|
|
22
24
|
}
|
|
23
25
|
export type TableBodyType = {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Table.d.ts","sourceRoot":"","sources":["../../src/shims/Table.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"Table.d.ts","sourceRoot":"","sources":["../../src/shims/Table.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,EAAoB,SAAS,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAUrF,UAAU,UAAU;IAClB,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,aAAa,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,GAAG,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,UAAU,KAAK,IAAI,CAAC,GAAG,CAAC,MAAM,IAAI,CAAC,CAAC;CACvH;AAED,MAAM,WAAW,IAAK,SAAQ,UAAU;IACtC,KAAK,EAAE,SAAS,GAAG,MAAM,CAAC;IAC1B,KAAK,CAAC,EAAE,aAAa,CAAC;CACvB;AAED,UAAU,QAAS,SAAQ,UAAU;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,OAAO;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,KAAK,EAAE,IAAI,EAAE,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,aAAa,CAAC,EAAE,MAAM,IAAI,CAAC;IAC3B,aAAa,CAAC,EAAE,CAAC,CAAC,EAAE,KAAK,CAAC,UAAU,KAAK,IAAI,CAAC;IAC9C,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,MAAM,aAAa,GACrB;IACA,IAAI,EAAE,YAAY,CAAC;IACnB,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,GACC;IACA,IAAI,EAAE,QAAQ,CAAC;IACf,QAAQ,EAAE,OAAO,EAAE,CAAC;CACrB,CAAC;AAEJ,KAAK,KAAK,GAAG,aAAa,GAAG;IAC3B,OAAO,EAAE,QAAQ,EAAE,CAAC;CACrB,CAAC;AAEF,UAAU,cAAc;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,gBAAgB,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;IACtC,eAAe,EAAE,MAAM,GAAG,KAAK,CAAC;CACjC;AAED,eAAO,MAAM,YAAY,YAAa,QAAQ,EAAE,KAAG,cAsBlD,CAAC;AAEF,UAAU,UAAU;IAClB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,gBAAgB,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/C,KAAK,EAAE,KAAK,CAAC;IACb,YAAY,CAAC,EAAE,SAAS,CAAC,uBAAuB,CAAC,CAAC;IAClD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,OAAO,CAAC;IAEpB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AA0CD,iBAAS,KAAK,CAAC,EACb,eAAe,EACf,MAAM,EACN,gBAAgB,EAChB,KAAK,EACL,YAAY,EACZ,SAAS,EACT,SAAS,EACT,YAAY,GACb,EAAE,UAAU,2CAkEZ;AA2DD,eAAe,KAAK,CAAC"}
|
package/package.json
CHANGED
|
@@ -12,7 +12,7 @@ import { PX_PER_LEVEL, BAR_HEIGHT, COLLAPSE_THRESHOLD } from './constants';
|
|
|
12
12
|
import type { FlamegraphPalette } from './colorPalette';
|
|
13
13
|
// there's a dependency cycle here but it should be fine
|
|
14
14
|
/* eslint-disable-next-line import/no-cycle */
|
|
15
|
-
import RenderCanvas from './Flamegraph_render';
|
|
15
|
+
import RenderCanvas, { CanvasI18nMessages } from './Flamegraph_render';
|
|
16
16
|
|
|
17
17
|
/* eslint-disable no-useless-constructor */
|
|
18
18
|
|
|
@@ -47,7 +47,10 @@ export default class Flamegraph {
|
|
|
47
47
|
private readonly highlightQuery: string,
|
|
48
48
|
private zoom: Maybe<DeepReadonly<{ i: number; j: number }>>,
|
|
49
49
|
|
|
50
|
-
private palette: FlamegraphPalette
|
|
50
|
+
private palette: FlamegraphPalette,
|
|
51
|
+
|
|
52
|
+
/** i18n messages for canvas rendering */
|
|
53
|
+
private messages?: CanvasI18nMessages
|
|
51
54
|
) {
|
|
52
55
|
// TODO
|
|
53
56
|
// these were only added because storybook is not setting
|
|
@@ -59,6 +62,7 @@ export default class Flamegraph {
|
|
|
59
62
|
this.highlightQuery = highlightQuery;
|
|
60
63
|
this.ff = createFF(flamebearer.format);
|
|
61
64
|
this.palette = palette;
|
|
65
|
+
this.messages = messages;
|
|
62
66
|
|
|
63
67
|
// don't allow to have a zoom smaller than the focus
|
|
64
68
|
// since it does not make sense
|
|
@@ -96,6 +100,7 @@ export default class Flamegraph {
|
|
|
96
100
|
pxPerTick: this.pxPerTick(),
|
|
97
101
|
tickToX: this.tickToX,
|
|
98
102
|
palette: this.palette,
|
|
103
|
+
messages: this.messages,
|
|
99
104
|
};
|
|
100
105
|
|
|
101
106
|
const { format: viewType } = this.flamebearer;
|
|
@@ -49,6 +49,18 @@ import { isMatch } from '../../search';
|
|
|
49
49
|
/* eslint-disable-next-line import/no-cycle */
|
|
50
50
|
import Flamegraph from './Flamegraph';
|
|
51
51
|
|
|
52
|
+
/** i18n messages needed for canvas rendering */
|
|
53
|
+
export interface CanvasI18nMessages {
|
|
54
|
+
collapsedLevelsSingular: string;
|
|
55
|
+
collapsedLevelsPlural: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Default English messages (fallback) */
|
|
59
|
+
const defaultCanvasMessages: CanvasI18nMessages = {
|
|
60
|
+
collapsedLevelsSingular: 'total (1 level collapsed)',
|
|
61
|
+
collapsedLevelsPlural: 'total ({n} levels collapsed)',
|
|
62
|
+
};
|
|
63
|
+
|
|
52
64
|
type CanvasRendererConfig = Flamebearer & {
|
|
53
65
|
canvas: HTMLCanvasElement;
|
|
54
66
|
focusedNode: ConstructorParameters<typeof Flamegraph>[2];
|
|
@@ -73,13 +85,30 @@ type CanvasRendererConfig = Flamebearer & {
|
|
|
73
85
|
|
|
74
86
|
palette: FlamegraphPalette;
|
|
75
87
|
maxSelf?: number;
|
|
88
|
+
|
|
89
|
+
/** i18n messages for collapsed text */
|
|
90
|
+
messages?: CanvasI18nMessages;
|
|
76
91
|
};
|
|
77
92
|
|
|
93
|
+
/**
|
|
94
|
+
* 生成 collapsed 层级的显示文本
|
|
95
|
+
*/
|
|
96
|
+
function getCollapsedText(
|
|
97
|
+
levelCount: number,
|
|
98
|
+
messages: CanvasI18nMessages
|
|
99
|
+
): string {
|
|
100
|
+
if (levelCount === 1) {
|
|
101
|
+
return messages.collapsedLevelsSingular;
|
|
102
|
+
}
|
|
103
|
+
return messages.collapsedLevelsPlural.replace('{n}', String(levelCount));
|
|
104
|
+
}
|
|
105
|
+
|
|
78
106
|
export default function RenderCanvas(props: CanvasRendererConfig) {
|
|
79
107
|
const { canvas, fitMode, units, tickToX, levels, palette } = props;
|
|
80
108
|
const { numTicks, sampleRate, pxPerTick } = props;
|
|
81
109
|
const { rangeMin, rangeMax } = props;
|
|
82
110
|
const { focusedNode, zoom } = props;
|
|
111
|
+
const messages = props.messages || defaultCanvasMessages;
|
|
83
112
|
|
|
84
113
|
const graphWidth = getCanvasWidth(canvas);
|
|
85
114
|
// TODO: why is this needed? otherwise height is all messed up
|
|
@@ -134,10 +163,10 @@ export default function RenderCanvas(props: CanvasRendererConfig) {
|
|
|
134
163
|
ctx.fillStyle = colorGreyscale(200, 1).rgb().string();
|
|
135
164
|
ctx.fill();
|
|
136
165
|
|
|
137
|
-
//
|
|
166
|
+
// 使用 i18n 翻译的 collapsed 文本
|
|
138
167
|
const shortName = focusedNode.mapOrElse(
|
|
139
168
|
() => 'total',
|
|
140
|
-
(f) =>
|
|
169
|
+
(f) => getCollapsedText(f.i - 1, messages)
|
|
141
170
|
);
|
|
142
171
|
|
|
143
172
|
// Set the font syle
|
|
@@ -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 } from 'react';
|
|
3
|
+
import React, { useCallback, useRef, useMemo } 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';
|
|
@@ -63,6 +63,15 @@ export default function FlameGraphComponent(props: FlamegraphProps) {
|
|
|
63
63
|
const flamegraph = useRef<Flamegraph>();
|
|
64
64
|
const i18n = useFlamegraphI18n();
|
|
65
65
|
|
|
66
|
+
// ====== 新增:提取 canvas 渲染需要的 i18n messages ======
|
|
67
|
+
const canvasMessages = useMemo(
|
|
68
|
+
() => ({
|
|
69
|
+
collapsedLevelsSingular: i18n.collapsedLevelsSingular,
|
|
70
|
+
collapsedLevelsPlural: i18n.collapsedLevelsPlural,
|
|
71
|
+
}),
|
|
72
|
+
[i18n.collapsedLevelsSingular, i18n.collapsedLevelsPlural]
|
|
73
|
+
);
|
|
74
|
+
|
|
66
75
|
const [rightClickedNode, setRightClickedNode] = React.useState<
|
|
67
76
|
Maybe<{ top: number; left: number; width: number }>
|
|
68
77
|
>(Maybe.nothing());
|
|
@@ -291,17 +300,19 @@ export default function FlameGraphComponent(props: FlamegraphProps) {
|
|
|
291
300
|
fitMode,
|
|
292
301
|
highlightQuery,
|
|
293
302
|
zoom,
|
|
294
|
-
palette
|
|
303
|
+
palette,
|
|
304
|
+
canvasMessages // ====== 新增:传递 i18n messages ======
|
|
295
305
|
);
|
|
296
306
|
|
|
297
307
|
flamegraph.current = f;
|
|
298
308
|
}
|
|
299
309
|
};
|
|
300
310
|
|
|
311
|
+
// ====== 修改:添加 canvasMessages 依赖 ======
|
|
301
312
|
React.useEffect(() => {
|
|
302
313
|
constructCanvas();
|
|
303
314
|
renderCanvas();
|
|
304
|
-
}, [palette]);
|
|
315
|
+
}, [palette, canvasMessages]);
|
|
305
316
|
|
|
306
317
|
React.useEffect(() => {
|
|
307
318
|
constructCanvas();
|
|
@@ -275,6 +275,55 @@ class FlameGraphRenderer extends Component<
|
|
|
275
275
|
});
|
|
276
276
|
};
|
|
277
277
|
|
|
278
|
+
/**
|
|
279
|
+
* 检查指定名称的函数是否在当前聚焦节点的子树中
|
|
280
|
+
*/
|
|
281
|
+
isNameInFocusedSubtree = (name: string): boolean => {
|
|
282
|
+
const { focusedNode } = this.state.flamegraphConfigs;
|
|
283
|
+
|
|
284
|
+
// 如果没有聚焦节点,任何 name 都算"在子树中"
|
|
285
|
+
if (focusedNode.isNothing) {
|
|
286
|
+
return true;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const { flamebearer } = this.state;
|
|
290
|
+
if (!flamebearer || !flamebearer.levels) {
|
|
291
|
+
return true;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const { names, levels, format } = flamebearer;
|
|
295
|
+
const ff = createFF(format);
|
|
296
|
+
const focusedI = (focusedNode as any).value.i;
|
|
297
|
+
const focusedJ = (focusedNode as any).value.j;
|
|
298
|
+
|
|
299
|
+
// 获取聚焦节点的范围
|
|
300
|
+
const focusedLevel = levels[focusedI];
|
|
301
|
+
if (!focusedLevel) return true;
|
|
302
|
+
|
|
303
|
+
const focusedOffset = ff.getBarOffset(focusedLevel, focusedJ);
|
|
304
|
+
const focusedTotal = ff.getBarTotal(focusedLevel, focusedJ);
|
|
305
|
+
const focusedEnd = focusedOffset + focusedTotal;
|
|
306
|
+
|
|
307
|
+
// 遍历聚焦节点及其下面的所有层级,检查是否有匹配的函数名
|
|
308
|
+
for (let i = focusedI; i < levels.length; i++) {
|
|
309
|
+
const level = levels[i];
|
|
310
|
+
for (let j = 0; j < level.length; j += ff.jStep) {
|
|
311
|
+
const offset = ff.getBarOffset(level, j);
|
|
312
|
+
const total = ff.getBarTotal(level, j);
|
|
313
|
+
|
|
314
|
+
// 检查这个节点是否在聚焦范围内
|
|
315
|
+
if (offset >= focusedOffset && offset + total <= focusedEnd) {
|
|
316
|
+
const nameIndex = ff.getBarName(level, j);
|
|
317
|
+
if (names[nameIndex] === name) {
|
|
318
|
+
return true;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return false;
|
|
325
|
+
};
|
|
326
|
+
|
|
278
327
|
setActiveItem = (item: { name: string }) => {
|
|
279
328
|
const { name } = item;
|
|
280
329
|
|
|
@@ -288,10 +337,25 @@ class FlameGraphRenderer extends Component<
|
|
|
288
337
|
}
|
|
289
338
|
}
|
|
290
339
|
|
|
291
|
-
//
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
340
|
+
// 检查选中的 item 是否在当前聚焦的子树中
|
|
341
|
+
// 如果不在,需要重置聚焦状态,以便在火焰图中正确显示高亮
|
|
342
|
+
const isInSubtree = this.isNameInFocusedSubtree(name);
|
|
343
|
+
|
|
344
|
+
if (isInSubtree) {
|
|
345
|
+
// 在子树中,只更新选中项
|
|
346
|
+
this.setState({
|
|
347
|
+
selectedItem: Maybe.just(name),
|
|
348
|
+
});
|
|
349
|
+
} else {
|
|
350
|
+
// 不在子树中,重置聚焦并更新选中项
|
|
351
|
+
this.setState({
|
|
352
|
+
selectedItem: Maybe.just(name),
|
|
353
|
+
flamegraphConfigs: {
|
|
354
|
+
...this.state.flamegraphConfigs,
|
|
355
|
+
focusedNode: this.resetFlamegraphState.focusedNode,
|
|
356
|
+
},
|
|
357
|
+
});
|
|
358
|
+
}
|
|
295
359
|
};
|
|
296
360
|
|
|
297
361
|
getHighlightQuery = () => {
|
|
@@ -337,7 +401,7 @@ class FlameGraphRenderer extends Component<
|
|
|
337
401
|
return (
|
|
338
402
|
this.state.selectedItem.isJust ||
|
|
339
403
|
JSON.stringify(this.initialFlamegraphState) !==
|
|
340
|
-
|
|
404
|
+
JSON.stringify(this.state.flamegraphConfigs)
|
|
341
405
|
);
|
|
342
406
|
};
|
|
343
407
|
|
|
@@ -371,6 +435,10 @@ class FlameGraphRenderer extends Component<
|
|
|
371
435
|
selectedItem={this.state.selectedItem}
|
|
372
436
|
handleTableItemClick={this.setActiveItem}
|
|
373
437
|
palette={this.state.palette}
|
|
438
|
+
// 新增:传递聚焦和视图切换能力给表格
|
|
439
|
+
onFocusOnNode={this.onFocusOnNode}
|
|
440
|
+
updateView={this.props.onlyDisplay ? undefined : this.updateView}
|
|
441
|
+
enableSandwichView={this.props.enableSandwichView}
|
|
374
442
|
/>
|
|
375
443
|
</div>
|
|
376
444
|
);
|
|
@@ -603,4 +671,4 @@ function decidePanesOrder(
|
|
|
603
671
|
}
|
|
604
672
|
}
|
|
605
673
|
|
|
606
|
-
export default FlameGraphRenderer;
|
|
674
|
+
export default FlameGraphRenderer;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// src/ProfilerTable.module.scss
|
|
2
|
+
|
|
3
|
+
.tableContextMenu {
|
|
4
|
+
// 继承 react-menu 的基础样式
|
|
5
|
+
:global(.szh-menu) {
|
|
6
|
+
background-color: var(--ps-ui-background, #fff);
|
|
7
|
+
border: 1px solid var(--ps-ui-border, #ccc);
|
|
8
|
+
border-radius: 4px;
|
|
9
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
|
10
|
+
padding: 4px 0;
|
|
11
|
+
min-width: 180px;
|
|
12
|
+
z-index: 1000;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
:global(.szh-menu__item) {
|
|
16
|
+
display: flex;
|
|
17
|
+
align-items: center;
|
|
18
|
+
gap: 8px;
|
|
19
|
+
padding: 8px 12px;
|
|
20
|
+
cursor: pointer;
|
|
21
|
+
color: var(--ps-ui-foreground-text, #333);
|
|
22
|
+
font-size: 13px;
|
|
23
|
+
transition: background-color 0.15s;
|
|
24
|
+
|
|
25
|
+
&:hover {
|
|
26
|
+
background-color: var(--ps-ui-element-bg-highlight, #f0f0f0);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
svg {
|
|
30
|
+
width: 14px;
|
|
31
|
+
height: 14px;
|
|
32
|
+
flex-shrink: 0;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.sandwichItem {
|
|
38
|
+
svg {
|
|
39
|
+
width: 16px;
|
|
40
|
+
height: 16px;
|
|
41
|
+
}
|
|
42
|
+
}
|