@kylincloud/flamegraph 0.35.23 → 0.35.25
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 +19 -0
- package/dist/FlameGraph/FlameGraphComponent/Flamegraph.d.ts +7 -1
- package/dist/FlameGraph/FlameGraphComponent/Flamegraph.d.ts.map +1 -1
- package/dist/FlameGraph/FlameGraphComponent/FlamegraphBreadcrumb.d.ts +16 -0
- package/dist/FlameGraph/FlameGraphComponent/FlamegraphBreadcrumb.d.ts.map +1 -0
- package/dist/FlameGraph/FlameGraphComponent/Flamegraph_render.d.ts +3 -0
- package/dist/FlameGraph/FlameGraphComponent/Flamegraph_render.d.ts.map +1 -1
- package/dist/FlameGraph/FlameGraphComponent/index.d.ts +13 -0
- package/dist/FlameGraph/FlameGraphComponent/index.d.ts.map +1 -1
- package/dist/FlameGraph/FlameGraphRenderer.d.ts +9 -1
- package/dist/FlameGraph/FlameGraphRenderer.d.ts.map +1 -1
- package/dist/Tooltip/Tooltip.d.ts.map +1 -1
- package/dist/format/format.d.ts +14 -0
- package/dist/format/format.d.ts.map +1 -1
- package/dist/i18n.d.ts +7 -0
- package/dist/i18n.d.ts.map +1 -1
- package/dist/index.cjs.js +3 -3
- 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/package.json +1 -1
- package/src/FlameGraph/FlameGraphComponent/Flamegraph.ts +34 -8
- package/src/FlameGraph/FlameGraphComponent/FlamegraphBreadcrumb.module.scss +106 -0
- package/src/FlameGraph/FlameGraphComponent/FlamegraphBreadcrumb.tsx +98 -0
- package/src/FlameGraph/FlameGraphComponent/Flamegraph_render.ts +134 -85
- package/src/FlameGraph/FlameGraphComponent/canvas.module.css +9 -0
- package/src/FlameGraph/FlameGraphComponent/index.tsx +105 -21
- package/src/FlameGraph/FlameGraphRenderer.tsx +169 -21
- package/src/Tooltip/Tooltip.tsx +1 -75
- package/src/format/format.ts +98 -0
- package/src/i18n.tsx +27 -0
|
@@ -23,6 +23,7 @@ import { SandwichIcon, HeadFirstIcon, TailFirstIcon } from '../../Icons';
|
|
|
23
23
|
import { PX_PER_LEVEL } from './constants';
|
|
24
24
|
import Header from './Header';
|
|
25
25
|
import { FlamegraphPalette } from './colorPalette';
|
|
26
|
+
import FlamegraphBreadcrumb from './FlamegraphBreadcrumb';
|
|
26
27
|
import type { ViewTypes } from './viewTypes';
|
|
27
28
|
import { FitModes, HeadMode, TailMode } from '../../fitMode/fitMode';
|
|
28
29
|
import indexStyles from './styles.module.scss';
|
|
@@ -56,20 +57,41 @@ interface FlamegraphProps {
|
|
|
56
57
|
|
|
57
58
|
/** 是否显示右键菜单中的「打开 Sandwich 视图」项,默认 true */
|
|
58
59
|
enableSandwichView?: boolean;
|
|
60
|
+
|
|
61
|
+
breadcrumb?: {
|
|
62
|
+
totalValueText: string;
|
|
63
|
+
totalSamplesText: string;
|
|
64
|
+
samplesLabel: string;
|
|
65
|
+
unitLabel: string;
|
|
66
|
+
hasFocus: boolean;
|
|
67
|
+
focusPercent?: number;
|
|
68
|
+
focusLabel?: string;
|
|
69
|
+
ofTotalPlacement?: 'before' | 'after';
|
|
70
|
+
ofTotalLabel: string;
|
|
71
|
+
clearFocusLabel: string;
|
|
72
|
+
onClearFocus: () => void;
|
|
73
|
+
};
|
|
59
74
|
}
|
|
60
75
|
|
|
61
76
|
export default function FlameGraphComponent(props: FlamegraphProps) {
|
|
62
77
|
const canvasRef = React.useRef<HTMLCanvasElement>(null);
|
|
78
|
+
const textCanvasRef = React.useRef<HTMLCanvasElement>(null);
|
|
63
79
|
const flamegraph = useRef<Flamegraph>();
|
|
80
|
+
const flamegraphText = useRef<Flamegraph>();
|
|
64
81
|
const i18n = useFlamegraphI18n();
|
|
82
|
+
const resizeLogRef = useRef({
|
|
83
|
+
lastWidth: 0,
|
|
84
|
+
lastHeight: 0,
|
|
85
|
+
});
|
|
65
86
|
|
|
66
87
|
// ====== 新增:提取 canvas 渲染需要的 i18n messages ======
|
|
67
88
|
const canvasMessages = useMemo(
|
|
68
89
|
() => ({
|
|
69
90
|
collapsedLevelsSingular: i18n.collapsedLevelsSingular,
|
|
70
91
|
collapsedLevelsPlural: i18n.collapsedLevelsPlural,
|
|
92
|
+
isZh: i18n.location !== 'Location',
|
|
71
93
|
}),
|
|
72
|
-
[i18n.collapsedLevelsSingular, i18n.collapsedLevelsPlural]
|
|
94
|
+
[i18n.collapsedLevelsSingular, i18n.collapsedLevelsPlural, i18n.location]
|
|
73
95
|
);
|
|
74
96
|
|
|
75
97
|
const [rightClickedNode, setRightClickedNode] = React.useState<
|
|
@@ -92,6 +114,7 @@ export default function FlameGraphComponent(props: FlamegraphProps) {
|
|
|
92
114
|
selectedItem,
|
|
93
115
|
updateView,
|
|
94
116
|
enableSandwichView = true,
|
|
117
|
+
breadcrumb,
|
|
95
118
|
} = props;
|
|
96
119
|
|
|
97
120
|
const { onZoom, onReset, isDirty, onFocusOnNode } = props;
|
|
@@ -100,13 +123,26 @@ export default function FlameGraphComponent(props: FlamegraphProps) {
|
|
|
100
123
|
|
|
101
124
|
const debouncedRenderCanvas = useCallback(
|
|
102
125
|
debounce(() => {
|
|
103
|
-
|
|
126
|
+
renderRectCanvas();
|
|
127
|
+
renderTextCanvas();
|
|
104
128
|
}, 50),
|
|
105
129
|
[]
|
|
106
130
|
);
|
|
107
131
|
|
|
108
132
|
useResizeObserver(canvasRef, () => {
|
|
109
133
|
if (flamegraph) {
|
|
134
|
+
if (canvasRef.current) {
|
|
135
|
+
const width = canvasRef.current.clientWidth;
|
|
136
|
+
const height = canvasRef.current.clientHeight;
|
|
137
|
+
const info = resizeLogRef.current;
|
|
138
|
+
const widthDelta = Math.abs(width - info.lastWidth);
|
|
139
|
+
const widthChanged = widthDelta >= 2;
|
|
140
|
+
if (!widthChanged) {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
info.lastWidth = width;
|
|
144
|
+
info.lastHeight = height;
|
|
145
|
+
}
|
|
110
146
|
debouncedRenderCanvas();
|
|
111
147
|
}
|
|
112
148
|
});
|
|
@@ -306,32 +342,58 @@ export default function FlameGraphComponent(props: FlamegraphProps) {
|
|
|
306
342
|
|
|
307
343
|
flamegraph.current = f;
|
|
308
344
|
}
|
|
345
|
+
if (textCanvasRef.current) {
|
|
346
|
+
const f = new Flamegraph(
|
|
347
|
+
flamebearer,
|
|
348
|
+
textCanvasRef.current,
|
|
349
|
+
focusedNode,
|
|
350
|
+
fitMode,
|
|
351
|
+
highlightQuery,
|
|
352
|
+
zoom,
|
|
353
|
+
palette,
|
|
354
|
+
canvasMessages
|
|
355
|
+
);
|
|
356
|
+
flamegraphText.current = f;
|
|
357
|
+
}
|
|
309
358
|
};
|
|
310
359
|
|
|
311
360
|
// ====== 修改:添加 canvasMessages 依赖 ======
|
|
312
361
|
React.useEffect(() => {
|
|
313
362
|
constructCanvas();
|
|
314
|
-
|
|
363
|
+
renderRectCanvas();
|
|
364
|
+
renderTextCanvas();
|
|
315
365
|
}, [palette, canvasMessages]);
|
|
316
366
|
|
|
317
367
|
React.useEffect(() => {
|
|
318
368
|
constructCanvas();
|
|
319
|
-
|
|
369
|
+
renderRectCanvas();
|
|
370
|
+
renderTextCanvas();
|
|
320
371
|
}, [
|
|
321
372
|
canvasRef.current,
|
|
322
373
|
flamebearer,
|
|
323
374
|
focusedNode,
|
|
324
375
|
fitMode,
|
|
325
|
-
highlightQuery,
|
|
326
376
|
zoom,
|
|
327
377
|
]);
|
|
328
378
|
|
|
329
|
-
|
|
379
|
+
React.useEffect(() => {
|
|
380
|
+
constructCanvas();
|
|
381
|
+
renderRectCanvas();
|
|
382
|
+
renderTextCanvas();
|
|
383
|
+
}, [highlightQuery]);
|
|
384
|
+
|
|
385
|
+
const renderRectCanvas = () => {
|
|
330
386
|
canvasRef?.current?.setAttribute('data-state', 'rendering');
|
|
331
|
-
flamegraph?.current?.render();
|
|
387
|
+
flamegraph?.current?.render({ renderText: false });
|
|
332
388
|
canvasRef?.current?.setAttribute('data-state', 'rendered');
|
|
333
389
|
};
|
|
334
390
|
|
|
391
|
+
const renderTextCanvas = () => {
|
|
392
|
+
textCanvasRef?.current?.setAttribute('data-state', 'rendering');
|
|
393
|
+
flamegraphText?.current?.render({ renderRects: false });
|
|
394
|
+
textCanvasRef?.current?.setAttribute('data-state', 'rendered');
|
|
395
|
+
};
|
|
396
|
+
|
|
335
397
|
const dataUnavailable =
|
|
336
398
|
!flamebearer || (flamebearer && flamebearer.names.length <= 1);
|
|
337
399
|
|
|
@@ -356,6 +418,21 @@ export default function FlameGraphComponent(props: FlamegraphProps) {
|
|
|
356
418
|
'vertical-orientation': flamebearer.format === 'double',
|
|
357
419
|
})}
|
|
358
420
|
>
|
|
421
|
+
{breadcrumb && (
|
|
422
|
+
<FlamegraphBreadcrumb
|
|
423
|
+
totalValueText={breadcrumb.totalValueText}
|
|
424
|
+
totalSamplesText={breadcrumb.totalSamplesText}
|
|
425
|
+
samplesLabel={breadcrumb.samplesLabel}
|
|
426
|
+
unitLabel={breadcrumb.unitLabel}
|
|
427
|
+
hasFocus={breadcrumb.hasFocus}
|
|
428
|
+
focusPercent={breadcrumb.focusPercent}
|
|
429
|
+
focusLabel={breadcrumb.focusLabel}
|
|
430
|
+
ofTotalPlacement={breadcrumb.ofTotalPlacement}
|
|
431
|
+
ofTotalLabel={breadcrumb.ofTotalLabel}
|
|
432
|
+
clearFocusLabel={breadcrumb.clearFocusLabel}
|
|
433
|
+
onClearFocus={breadcrumb.onClearFocus}
|
|
434
|
+
/>
|
|
435
|
+
)}
|
|
359
436
|
{/* Header 已简化,仅在 single 模式下显示标题 */}
|
|
360
437
|
{/* DiffLegend 已移到 Toolbar */}
|
|
361
438
|
{headerVisible && (
|
|
@@ -368,6 +445,7 @@ export default function FlameGraphComponent(props: FlamegraphProps) {
|
|
|
368
445
|
<div
|
|
369
446
|
data-testid={dataTestId}
|
|
370
447
|
style={{
|
|
448
|
+
position: 'relative',
|
|
371
449
|
opacity: dataUnavailable && !showSingleLevel ? 0 : 1,
|
|
372
450
|
}}
|
|
373
451
|
>
|
|
@@ -379,22 +457,28 @@ export default function FlameGraphComponent(props: FlamegraphProps) {
|
|
|
379
457
|
ref={canvasRef}
|
|
380
458
|
onClick={!disableClick ? onClick : undefined}
|
|
381
459
|
/>
|
|
460
|
+
<canvas
|
|
461
|
+
height="0"
|
|
462
|
+
data-testid="flamegraph-text-canvas"
|
|
463
|
+
className={clsx('flamegraph-text-canvas', styles.textCanvas)}
|
|
464
|
+
ref={textCanvasRef}
|
|
465
|
+
/>
|
|
466
|
+
{flamegraph && canvasRef && (
|
|
467
|
+
<Highlight
|
|
468
|
+
barHeight={PX_PER_LEVEL}
|
|
469
|
+
canvasRef={canvasRef}
|
|
470
|
+
zoom={zoom}
|
|
471
|
+
xyToHighlightData={xyToHighlightData}
|
|
472
|
+
/>
|
|
473
|
+
)}
|
|
474
|
+
{flamegraph && (
|
|
475
|
+
<ContextMenuHighlight
|
|
476
|
+
barHeight={PX_PER_LEVEL}
|
|
477
|
+
node={rightClickedNode}
|
|
478
|
+
/>
|
|
479
|
+
)}
|
|
382
480
|
</div>
|
|
383
481
|
{showCredit ? <LogoLink /> : ''}
|
|
384
|
-
{flamegraph && canvasRef && (
|
|
385
|
-
<Highlight
|
|
386
|
-
barHeight={PX_PER_LEVEL}
|
|
387
|
-
canvasRef={canvasRef}
|
|
388
|
-
zoom={zoom}
|
|
389
|
-
xyToHighlightData={xyToHighlightData}
|
|
390
|
-
/>
|
|
391
|
-
)}
|
|
392
|
-
{flamegraph && (
|
|
393
|
-
<ContextMenuHighlight
|
|
394
|
-
barHeight={PX_PER_LEVEL}
|
|
395
|
-
node={rightClickedNode}
|
|
396
|
-
/>
|
|
397
|
-
)}
|
|
398
482
|
{flamegraph && (
|
|
399
483
|
<FlamegraphTooltip
|
|
400
484
|
format={flamebearer.format}
|
|
@@ -30,6 +30,15 @@ import { ViewTypes } from './FlameGraphComponent/viewTypes';
|
|
|
30
30
|
import { GraphVizPane } from './FlameGraphComponent/GraphVizPane';
|
|
31
31
|
import { isSameFlamebearer } from './uniqueness';
|
|
32
32
|
import { normalize } from './normalize';
|
|
33
|
+
import {
|
|
34
|
+
formatSampleCount,
|
|
35
|
+
getFormatter,
|
|
36
|
+
localizeDurationString,
|
|
37
|
+
} from '../format/format';
|
|
38
|
+
import {
|
|
39
|
+
FlamegraphI18nContext,
|
|
40
|
+
type FlamegraphMessages,
|
|
41
|
+
} from '../i18n';
|
|
33
42
|
|
|
34
43
|
// Refers to a node in the flamegraph
|
|
35
44
|
interface Node {
|
|
@@ -92,6 +101,9 @@ class FlameGraphRenderer extends Component<
|
|
|
92
101
|
FlamegraphRendererProps,
|
|
93
102
|
FlamegraphRendererState
|
|
94
103
|
> {
|
|
104
|
+
static contextType = FlamegraphI18nContext;
|
|
105
|
+
declare context: FlamegraphMessages;
|
|
106
|
+
|
|
95
107
|
resetFlamegraphState = {
|
|
96
108
|
focusedNode: Maybe.nothing<Node>(),
|
|
97
109
|
zoom: Maybe.nothing<Node>(),
|
|
@@ -135,24 +147,28 @@ class FlameGraphRenderer extends Component<
|
|
|
135
147
|
prevProps: FlamegraphRendererProps,
|
|
136
148
|
prevState: FlamegraphRendererState
|
|
137
149
|
) {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
150
|
+
const propsChanged =
|
|
151
|
+
prevProps.profile !== this.props.profile ||
|
|
152
|
+
prevProps.flamebearer !== this.props.flamebearer;
|
|
153
|
+
if (propsChanged) {
|
|
154
|
+
const prevFlame = prevState.flamebearer;
|
|
155
|
+
const currFlame = normalize(this.props);
|
|
156
|
+
|
|
157
|
+
if (!this.isSameFlamebearer(prevFlame, currFlame)) {
|
|
158
|
+
const newConfigs = this.calcNewConfigs(prevFlame, currFlame);
|
|
159
|
+
|
|
160
|
+
// Batch these updates to not do unnecessary work
|
|
161
|
+
// eslint-disable-next-line react/no-did-update-set-state
|
|
162
|
+
this.setState({
|
|
163
|
+
flamebearer: currFlame,
|
|
164
|
+
flamegraphConfigs: {
|
|
165
|
+
...this.state.flamegraphConfigs,
|
|
166
|
+
...newConfigs,
|
|
167
|
+
},
|
|
168
|
+
selectedItem: Maybe.nothing(),
|
|
169
|
+
});
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
156
172
|
}
|
|
157
173
|
|
|
158
174
|
// flamegraph configs changed
|
|
@@ -395,6 +411,66 @@ class FlameGraphRenderer extends Component<
|
|
|
395
411
|
});
|
|
396
412
|
};
|
|
397
413
|
|
|
414
|
+
clearFocus = () => {
|
|
415
|
+
this.setState((prevState) => ({
|
|
416
|
+
...prevState,
|
|
417
|
+
flamegraphConfigs: {
|
|
418
|
+
...prevState.flamegraphConfigs,
|
|
419
|
+
...this.resetFlamegraphState,
|
|
420
|
+
},
|
|
421
|
+
}));
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
getActiveRange = () => {
|
|
425
|
+
const { flamebearer } = this.state;
|
|
426
|
+
const { zoom, focusedNode } = this.state.flamegraphConfigs;
|
|
427
|
+
const totalTicks = flamebearer.numTicks;
|
|
428
|
+
|
|
429
|
+
if (!totalTicks) {
|
|
430
|
+
return { rangeMin: 0, rangeMax: 1 };
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const ff = createFF(flamebearer.format);
|
|
434
|
+
|
|
435
|
+
const getRangeFromNode = (node: Node) => {
|
|
436
|
+
const level = flamebearer.levels[node.i];
|
|
437
|
+
if (!level) {
|
|
438
|
+
return { rangeMin: 0, rangeMax: 1 };
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const offset = ff.getBarOffset(level, node.j);
|
|
442
|
+
const total = ff.getBarTotal(level, node.j);
|
|
443
|
+
return {
|
|
444
|
+
rangeMin: offset / totalTicks,
|
|
445
|
+
rangeMax: (offset + total) / totalTicks,
|
|
446
|
+
};
|
|
447
|
+
};
|
|
448
|
+
|
|
449
|
+
return zoom.match({
|
|
450
|
+
Just: (z) => {
|
|
451
|
+
return focusedNode.match({
|
|
452
|
+
Just: (f) => {
|
|
453
|
+
const focusRange = getRangeFromNode(f);
|
|
454
|
+
const zoomRange = getRangeFromNode(z);
|
|
455
|
+
if (
|
|
456
|
+
focusRange.rangeMax - focusRange.rangeMin <
|
|
457
|
+
zoomRange.rangeMax - zoomRange.rangeMin
|
|
458
|
+
) {
|
|
459
|
+
return focusRange;
|
|
460
|
+
}
|
|
461
|
+
return zoomRange;
|
|
462
|
+
},
|
|
463
|
+
Nothing: () => getRangeFromNode(z),
|
|
464
|
+
});
|
|
465
|
+
},
|
|
466
|
+
Nothing: () =>
|
|
467
|
+
focusedNode.match({
|
|
468
|
+
Just: (f) => getRangeFromNode(f),
|
|
469
|
+
Nothing: () => ({ rangeMin: 0, rangeMax: 1 }),
|
|
470
|
+
}),
|
|
471
|
+
});
|
|
472
|
+
};
|
|
473
|
+
|
|
398
474
|
// used as a variable instead of keeping in the state
|
|
399
475
|
// so that the flamegraph doesn't rerender unnecessarily
|
|
400
476
|
isDirty = () => {
|
|
@@ -445,6 +521,60 @@ class FlameGraphRenderer extends Component<
|
|
|
445
521
|
|
|
446
522
|
const toolbarVisible = this.shouldShowToolbar();
|
|
447
523
|
|
|
524
|
+
const dataUnavailable =
|
|
525
|
+
!this.state.flamebearer || this.state.flamebearer.names.length <= 1;
|
|
526
|
+
const i18n = this.context;
|
|
527
|
+
const isZh = i18n.location !== 'Location';
|
|
528
|
+
const totalTicks = this.state.flamebearer.numTicks;
|
|
529
|
+
const formatter = getFormatter(
|
|
530
|
+
totalTicks,
|
|
531
|
+
this.state.flamebearer.sampleRate,
|
|
532
|
+
this.state.flamebearer.units
|
|
533
|
+
);
|
|
534
|
+
const totalValueRaw = formatter.format(
|
|
535
|
+
totalTicks,
|
|
536
|
+
this.state.flamebearer.sampleRate
|
|
537
|
+
);
|
|
538
|
+
const totalValueText =
|
|
539
|
+
localizeDurationString(totalValueRaw, isZh) || totalValueRaw;
|
|
540
|
+
const totalSamplesText = formatSampleCount(
|
|
541
|
+
totalTicks,
|
|
542
|
+
i18n.sampleCountFormat
|
|
543
|
+
);
|
|
544
|
+
const samplesLabel = i18n.tooltipSamples.replace(/[::]\s*$/, '');
|
|
545
|
+
const unitLabel =
|
|
546
|
+
i18n.tooltipUnitTitles[this.state.flamebearer.units]?.formattedValue ||
|
|
547
|
+
i18n.tooltipUnitTitles.unknown.formattedValue;
|
|
548
|
+
const activeRange = this.getActiveRange();
|
|
549
|
+
const hasFocus =
|
|
550
|
+
activeRange.rangeMin > 0 || activeRange.rangeMax < 1;
|
|
551
|
+
const activeRangeTicks = Math.max(
|
|
552
|
+
0,
|
|
553
|
+
Math.min(
|
|
554
|
+
totalTicks,
|
|
555
|
+
(activeRange.rangeMax - activeRange.rangeMin) * totalTicks
|
|
556
|
+
)
|
|
557
|
+
);
|
|
558
|
+
const focusPercent =
|
|
559
|
+
totalTicks > 0 ? (activeRangeTicks / totalTicks) * 100 : 0;
|
|
560
|
+
const focusLabel = (() => {
|
|
561
|
+
const ff = createFF(this.state.flamebearer.format);
|
|
562
|
+
const focusNode = this.state.flamegraphConfigs.focusedNode.mapOrElse(
|
|
563
|
+
() =>
|
|
564
|
+
this.state.flamegraphConfigs.zoom.mapOrElse(() => null, (z) => z),
|
|
565
|
+
(f) => f
|
|
566
|
+
);
|
|
567
|
+
if (!focusNode) {
|
|
568
|
+
return '';
|
|
569
|
+
}
|
|
570
|
+
const level = this.state.flamebearer.levels[focusNode.i];
|
|
571
|
+
if (!level) {
|
|
572
|
+
return '';
|
|
573
|
+
}
|
|
574
|
+
const nameIndex = ff.getBarName(level, focusNode.j);
|
|
575
|
+
return this.state.flamebearer.names[nameIndex] || '';
|
|
576
|
+
})();
|
|
577
|
+
|
|
448
578
|
const flameGraphPane = (
|
|
449
579
|
<Graph
|
|
450
580
|
key="flamegraph-pane"
|
|
@@ -467,10 +597,30 @@ class FlameGraphRenderer extends Component<
|
|
|
467
597
|
toolbarVisible={toolbarVisible}
|
|
468
598
|
setPalette={this.handleSetPalette}
|
|
469
599
|
enableSandwichView={this.props.enableSandwichView}
|
|
600
|
+
breadcrumb={
|
|
601
|
+
dataUnavailable
|
|
602
|
+
? undefined
|
|
603
|
+
: {
|
|
604
|
+
totalValueText,
|
|
605
|
+
totalSamplesText,
|
|
606
|
+
samplesLabel,
|
|
607
|
+
unitLabel,
|
|
608
|
+
hasFocus,
|
|
609
|
+
focusPercent,
|
|
610
|
+
focusLabel,
|
|
611
|
+
ofTotalPlacement: isZh ? 'before' : 'after',
|
|
612
|
+
ofTotalLabel: i18n.ofTotal,
|
|
613
|
+
clearFocusLabel: i18n.clearFocus,
|
|
614
|
+
onClearFocus: this.clearFocus,
|
|
615
|
+
}
|
|
616
|
+
}
|
|
470
617
|
/>
|
|
471
618
|
);
|
|
472
619
|
|
|
473
620
|
const sandwichPane = (() => {
|
|
621
|
+
if (this.state.view !== 'sandwich') {
|
|
622
|
+
return <div className={styles.sandwichPane} key="sandwich-pane" />;
|
|
623
|
+
}
|
|
474
624
|
if (this.state.selectedItem.isNothing) {
|
|
475
625
|
return (
|
|
476
626
|
<div className={styles.sandwichPane} key="sandwich-pane">
|
|
@@ -574,8 +724,6 @@ class FlameGraphRenderer extends Component<
|
|
|
574
724
|
// // rightTicks?: number;
|
|
575
725
|
// } & addTicks;
|
|
576
726
|
|
|
577
|
-
const dataUnavailable =
|
|
578
|
-
!this.state.flamebearer || this.state.flamebearer.names.length <= 1;
|
|
579
727
|
const panes = decidePanesOrder(
|
|
580
728
|
this.state.view,
|
|
581
729
|
flameGraphPane,
|
|
@@ -671,4 +819,4 @@ function decidePanesOrder(
|
|
|
671
819
|
}
|
|
672
820
|
}
|
|
673
821
|
|
|
674
|
-
export default FlameGraphRenderer;
|
|
822
|
+
export default FlameGraphRenderer;
|
package/src/Tooltip/Tooltip.tsx
CHANGED
|
@@ -20,6 +20,7 @@ import LeftClickIcon from './LeftClickIcon';
|
|
|
20
20
|
import styles from './Tooltip.module.scss';
|
|
21
21
|
import { useFlamegraphI18n } from '../i18n';
|
|
22
22
|
import type { FlamegraphPalette } from '../FlameGraph/FlameGraphComponent/colorPalette';
|
|
23
|
+
import { localizeDurationString, parseNumericWithUnit } from '../format/format';
|
|
23
24
|
|
|
24
25
|
export type TooltipData = {
|
|
25
26
|
units: Units;
|
|
@@ -56,81 +57,6 @@ export interface TooltipProps {
|
|
|
56
57
|
) => void;
|
|
57
58
|
}
|
|
58
59
|
|
|
59
|
-
/** 解析各种带单位的字符串,例如:
|
|
60
|
-
* "0.63 seconds" / "0.63 分钟" / "< 1 ms" / "+0.24minutes"
|
|
61
|
-
*/
|
|
62
|
-
function parseNumericWithUnit(
|
|
63
|
-
input?: string | number
|
|
64
|
-
): { value: number; unit: string; hasLessThan: boolean; sign: string } | null {
|
|
65
|
-
if (input == null) return null;
|
|
66
|
-
|
|
67
|
-
if (typeof input === 'number') {
|
|
68
|
-
return { value: input, unit: '', hasLessThan: false, sign: '' };
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// 统一 Unicode 减号
|
|
72
|
-
let s = String(input).trim().replace(/\u2212/g, '-');
|
|
73
|
-
if (!s) return null;
|
|
74
|
-
|
|
75
|
-
let hasLessThan = false;
|
|
76
|
-
if (s.startsWith('<')) {
|
|
77
|
-
hasLessThan = true;
|
|
78
|
-
s = s.slice(1).trim();
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const m = s.match(/^([+\-]?)(\d*\.?\d+)\s*(.*)$/);
|
|
82
|
-
if (!m) return null;
|
|
83
|
-
|
|
84
|
-
const sign = m[1] || '';
|
|
85
|
-
const numStr = m[2];
|
|
86
|
-
const unit = (m[3] || '').trim();
|
|
87
|
-
|
|
88
|
-
const value = parseFloat(numStr);
|
|
89
|
-
if (!Number.isFinite(value)) return null;
|
|
90
|
-
|
|
91
|
-
return { value, unit, hasLessThan, sign };
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/** 把 DurationFormatter 输出的英文单位翻译成中文,并保证数字和单位之间有空格 */
|
|
95
|
-
function localizeDurationString(
|
|
96
|
-
input?: string,
|
|
97
|
-
isZh = false
|
|
98
|
-
): string | undefined {
|
|
99
|
-
if (!input || !isZh) return input;
|
|
100
|
-
|
|
101
|
-
const parsed = parseNumericWithUnit(input);
|
|
102
|
-
if (!parsed) return input;
|
|
103
|
-
|
|
104
|
-
const { hasLessThan, unit, sign, value } = parsed;
|
|
105
|
-
|
|
106
|
-
let unitZh = unit;
|
|
107
|
-
const u = unit.toLowerCase();
|
|
108
|
-
|
|
109
|
-
if (u === 'ms') unitZh = '毫秒';
|
|
110
|
-
else if (u === 'μs' || u === 'µs' || u === 'us') unitZh = '微秒';
|
|
111
|
-
else if (u === 'second' || u === 'seconds') unitZh = '秒';
|
|
112
|
-
else if (u === 'minute' || u === 'minutes' || u === 'min' || u === 'mins')
|
|
113
|
-
unitZh = '分钟';
|
|
114
|
-
else if (u === 'hour' || u === 'hours') unitZh = '小时';
|
|
115
|
-
else if (u === 'day' || u === 'days') unitZh = '天';
|
|
116
|
-
else if (u === 'month' || u === 'months') unitZh = '月';
|
|
117
|
-
else if (u === 'year' || u === 'years') unitZh = '年';
|
|
118
|
-
|
|
119
|
-
// 尽量保留原来的数值部分
|
|
120
|
-
const m = String(input)
|
|
121
|
-
.trim()
|
|
122
|
-
.replace(/\u2212/g, '-')
|
|
123
|
-
.match(/^<?\s*([+\-]?)(\d*\.?\d+)/);
|
|
124
|
-
const valueStr =
|
|
125
|
-
m && m[2] ? `${m[1] || ''}${m[2]}` : `${sign}${Math.abs(value)}`;
|
|
126
|
-
|
|
127
|
-
const prefix = hasLessThan ? '< ' : '';
|
|
128
|
-
if (!unitZh) {
|
|
129
|
-
return `${prefix}${valueStr}`.trim();
|
|
130
|
-
}
|
|
131
|
-
return `${prefix}${valueStr} ${unitZh}`.trim();
|
|
132
|
-
}
|
|
133
|
-
|
|
134
60
|
/** CPU 时间差异:构造 "+0.24 minutes" / "-0.12 s" 这种文本(注意带空格) */
|
|
135
61
|
function formatTimeDiff(
|
|
136
62
|
baselineFormatted?: string,
|
package/src/format/format.ts
CHANGED
|
@@ -5,6 +5,104 @@ export function numberWithCommas(x: number): string {
|
|
|
5
5
|
return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
+
type NumericWithUnit = {
|
|
9
|
+
value: number;
|
|
10
|
+
unit: string;
|
|
11
|
+
hasLessThan: boolean;
|
|
12
|
+
sign: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export function parseNumericWithUnit(
|
|
16
|
+
input?: string | number
|
|
17
|
+
): NumericWithUnit | null {
|
|
18
|
+
if (input == null) return null;
|
|
19
|
+
|
|
20
|
+
if (typeof input === 'number') {
|
|
21
|
+
return { value: input, unit: '', hasLessThan: false, sign: '' };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let s = String(input).trim().replace(/\u2212/g, '-');
|
|
25
|
+
if (!s) return null;
|
|
26
|
+
|
|
27
|
+
let hasLessThan = false;
|
|
28
|
+
if (s.startsWith('<')) {
|
|
29
|
+
hasLessThan = true;
|
|
30
|
+
s = s.slice(1).trim();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const m = s.match(/^([+\-]?)(\d*\.?\d+)\s*(.*)$/);
|
|
34
|
+
if (!m) return null;
|
|
35
|
+
|
|
36
|
+
const sign = m[1] || '';
|
|
37
|
+
const numStr = m[2];
|
|
38
|
+
const unit = (m[3] || '').trim();
|
|
39
|
+
|
|
40
|
+
const value = parseFloat(numStr);
|
|
41
|
+
if (!Number.isFinite(value)) return null;
|
|
42
|
+
|
|
43
|
+
return { value, unit, hasLessThan, sign };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function localizeDurationString(
|
|
47
|
+
input?: string,
|
|
48
|
+
isZh = false
|
|
49
|
+
): string | undefined {
|
|
50
|
+
if (!input || !isZh) return input;
|
|
51
|
+
|
|
52
|
+
const parsed = parseNumericWithUnit(input);
|
|
53
|
+
if (!parsed) return input;
|
|
54
|
+
|
|
55
|
+
const { unit } = parsed;
|
|
56
|
+
|
|
57
|
+
let unitZh = unit;
|
|
58
|
+
const u = unit.toLowerCase();
|
|
59
|
+
|
|
60
|
+
if (u === 'ms') unitZh = '毫秒';
|
|
61
|
+
else if (u === 'μs' || u === 'µs' || u === 'us') unitZh = '微秒';
|
|
62
|
+
else if (u === 'second' || u === 'seconds') unitZh = '秒';
|
|
63
|
+
else if (u === 'minute' || u === 'minutes' || u === 'min' || u === 'mins')
|
|
64
|
+
unitZh = '分钟';
|
|
65
|
+
else if (u === 'hour' || u === 'hours') unitZh = '小时';
|
|
66
|
+
else if (u === 'day' || u === 'days') unitZh = '天';
|
|
67
|
+
else if (u === 'month' || u === 'months') unitZh = '月';
|
|
68
|
+
else if (u === 'year' || u === 'years') unitZh = '年';
|
|
69
|
+
|
|
70
|
+
const m = String(input).match(/^([<\s]*)([+\-]?\d*\.?\d+)(.*)$/);
|
|
71
|
+
if (!m) return input;
|
|
72
|
+
|
|
73
|
+
const prefix = m[1] || '';
|
|
74
|
+
const numberPart = m[2] || '';
|
|
75
|
+
|
|
76
|
+
return `${prefix}${numberPart} ${unitZh}`.trim();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export type SampleCountFormat = Array<{ value: number; label: string }>;
|
|
80
|
+
|
|
81
|
+
const defaultSampleCountFormat: SampleCountFormat = [
|
|
82
|
+
{ value: 1e15, label: 'Quad' },
|
|
83
|
+
{ value: 1e12, label: 'Tri' },
|
|
84
|
+
{ value: 1e9, label: 'B' },
|
|
85
|
+
{ value: 1e6, label: 'M' },
|
|
86
|
+
{ value: 1e3, label: 'K' },
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
export function formatSampleCount(
|
|
90
|
+
value: number,
|
|
91
|
+
format: SampleCountFormat = defaultSampleCountFormat
|
|
92
|
+
): string {
|
|
93
|
+
const absValue = Math.abs(value);
|
|
94
|
+
for (const { value: threshold, label } of format) {
|
|
95
|
+
if (absValue >= threshold) {
|
|
96
|
+
const scaled = value / threshold;
|
|
97
|
+
const fixed = scaled.toFixed(2);
|
|
98
|
+
const trimmed = fixed.replace(/\.?0+$/, '');
|
|
99
|
+
return `${trimmed} ${label}`;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return numberWithCommas(value);
|
|
104
|
+
}
|
|
105
|
+
|
|
8
106
|
export function formatPercent(ratio: number) {
|
|
9
107
|
const percent = ratioToPercent(ratio);
|
|
10
108
|
return `${percent}%`;
|