@kylincloud/flamegraph 0.35.23 → 0.35.24
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 +13 -0
- 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 +1 -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 +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/package.json +1 -1
- package/src/FlameGraph/FlameGraphComponent/FlamegraphBreadcrumb.module.scss +106 -0
- package/src/FlameGraph/FlameGraphComponent/FlamegraphBreadcrumb.tsx +98 -0
- package/src/FlameGraph/FlameGraphComponent/Flamegraph_render.ts +23 -21
- package/src/FlameGraph/FlameGraphComponent/index.tsx +33 -1
- package/src/FlameGraph/FlameGraphRenderer.tsx +144 -3
- package/src/Tooltip/Tooltip.tsx +1 -75
- package/src/format/format.ts +98 -0
- package/src/i18n.tsx +27 -0
package/package.json
CHANGED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
.bar {
|
|
2
|
+
display: flex;
|
|
3
|
+
align-items: center;
|
|
4
|
+
justify-content: center;
|
|
5
|
+
width: 100%;
|
|
6
|
+
flex-wrap: wrap;
|
|
7
|
+
gap: 8px;
|
|
8
|
+
margin: 6px 0;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.chip {
|
|
12
|
+
display: inline-flex;
|
|
13
|
+
align-items: center;
|
|
14
|
+
gap: 6px;
|
|
15
|
+
padding: 4px 10px;
|
|
16
|
+
border-radius: 999px;
|
|
17
|
+
font-size: 12px;
|
|
18
|
+
line-height: 1.2;
|
|
19
|
+
background-color: var(--ps-ui-element-bg-primary);
|
|
20
|
+
border: 1px solid var(--ps-ui-border);
|
|
21
|
+
color: var(--ps-ui-foreground-text);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.focusTooltip {
|
|
25
|
+
position: relative;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.focusTooltip[data-tooltip]:hover::after,
|
|
29
|
+
.focusTooltip[data-tooltip]:hover::before {
|
|
30
|
+
opacity: 1;
|
|
31
|
+
transform: translate(-50%, -6px);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.focusTooltip[data-tooltip]::after {
|
|
35
|
+
content: attr(data-tooltip);
|
|
36
|
+
position: absolute;
|
|
37
|
+
left: 50%;
|
|
38
|
+
bottom: 100%;
|
|
39
|
+
transform: translate(-50%, -2px);
|
|
40
|
+
padding: 6px 8px;
|
|
41
|
+
border-radius: 6px;
|
|
42
|
+
background: var(--ps-ui-foreground);
|
|
43
|
+
color: var(--ps-ui-foreground-text);
|
|
44
|
+
border: 1px solid var(--ps-ui-border);
|
|
45
|
+
font-size: 12px;
|
|
46
|
+
white-space: nowrap;
|
|
47
|
+
pointer-events: none;
|
|
48
|
+
opacity: 0;
|
|
49
|
+
transition: opacity 120ms ease, transform 120ms ease;
|
|
50
|
+
z-index: 3;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.focusTooltip[data-tooltip]::before {
|
|
54
|
+
content: '';
|
|
55
|
+
position: absolute;
|
|
56
|
+
left: 50%;
|
|
57
|
+
bottom: 100%;
|
|
58
|
+
transform: translate(-50%, -2px);
|
|
59
|
+
border-width: 6px 6px 0 6px;
|
|
60
|
+
border-style: solid;
|
|
61
|
+
border-color: var(--ps-ui-border) transparent transparent transparent;
|
|
62
|
+
pointer-events: none;
|
|
63
|
+
opacity: 0;
|
|
64
|
+
transition: opacity 120ms ease, transform 120ms ease;
|
|
65
|
+
z-index: 2;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.separator {
|
|
69
|
+
opacity: 0.6;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.chipSeparator {
|
|
73
|
+
opacity: 0.6;
|
|
74
|
+
color: var(--ps-ui-foreground-text);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.unitLabel {
|
|
78
|
+
opacity: 0.8;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.focusIcon {
|
|
82
|
+
font-size: 12px;
|
|
83
|
+
display: inline-flex;
|
|
84
|
+
align-items: center;
|
|
85
|
+
color: currentColor;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.ofTotal {
|
|
89
|
+
opacity: 0.8;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.clearBtn {
|
|
93
|
+
margin-left: 4px;
|
|
94
|
+
padding: 0;
|
|
95
|
+
border: none;
|
|
96
|
+
background: transparent;
|
|
97
|
+
color: var(--ps-toolbar-icon-color);
|
|
98
|
+
cursor: pointer;
|
|
99
|
+
font-size: 14px;
|
|
100
|
+
line-height: 1;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.clearBtn:focus-visible {
|
|
104
|
+
outline: 1px solid var(--ps-ui-border);
|
|
105
|
+
border-radius: 4px;
|
|
106
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import styles from './FlamegraphBreadcrumb.module.scss';
|
|
3
|
+
|
|
4
|
+
type FlamegraphBreadcrumbProps = {
|
|
5
|
+
totalValueText: string;
|
|
6
|
+
totalSamplesText: string;
|
|
7
|
+
samplesLabel: string;
|
|
8
|
+
unitLabel: string;
|
|
9
|
+
hasFocus: boolean;
|
|
10
|
+
focusPercent?: number;
|
|
11
|
+
focusLabel?: string;
|
|
12
|
+
ofTotalPlacement?: 'before' | 'after';
|
|
13
|
+
ofTotalLabel: string;
|
|
14
|
+
clearFocusLabel: string;
|
|
15
|
+
onClearFocus: () => void;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export default function FlamegraphBreadcrumb(
|
|
19
|
+
props: FlamegraphBreadcrumbProps
|
|
20
|
+
) {
|
|
21
|
+
const {
|
|
22
|
+
totalValueText,
|
|
23
|
+
totalSamplesText,
|
|
24
|
+
samplesLabel,
|
|
25
|
+
unitLabel,
|
|
26
|
+
hasFocus,
|
|
27
|
+
focusPercent,
|
|
28
|
+
focusLabel,
|
|
29
|
+
ofTotalPlacement = 'after',
|
|
30
|
+
ofTotalLabel,
|
|
31
|
+
clearFocusLabel,
|
|
32
|
+
onClearFocus,
|
|
33
|
+
} = props;
|
|
34
|
+
|
|
35
|
+
const focusTooltip =
|
|
36
|
+
focusLabel && focusLabel.trim().length > 0 ? focusLabel : undefined;
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<div className={styles.bar} aria-live="polite">
|
|
40
|
+
<span className={styles.chip}>
|
|
41
|
+
<span>{totalValueText}</span>
|
|
42
|
+
<span className={styles.separator}>|</span>
|
|
43
|
+
<span>
|
|
44
|
+
{totalSamplesText} {samplesLabel}
|
|
45
|
+
</span>
|
|
46
|
+
<span className={styles.unitLabel}>({unitLabel})</span>
|
|
47
|
+
</span>
|
|
48
|
+
{hasFocus && focusPercent !== undefined && (
|
|
49
|
+
<>
|
|
50
|
+
<span className={styles.chipSeparator}>></span>
|
|
51
|
+
<span
|
|
52
|
+
className={`${styles.chip} ${styles.focusTooltip}`}
|
|
53
|
+
data-tooltip={focusTooltip}
|
|
54
|
+
>
|
|
55
|
+
<span className={styles.focusIcon} aria-hidden="true">
|
|
56
|
+
<svg
|
|
57
|
+
viewBox="0 0 24 24"
|
|
58
|
+
width="14"
|
|
59
|
+
height="14"
|
|
60
|
+
fill="none"
|
|
61
|
+
stroke="currentColor"
|
|
62
|
+
strokeWidth="1.6"
|
|
63
|
+
strokeLinecap="round"
|
|
64
|
+
strokeLinejoin="round"
|
|
65
|
+
>
|
|
66
|
+
<path d="M2 12s3.5-6 10-6 10 6 10 6-3.5 6-10 6-10-6-10-6Z" />
|
|
67
|
+
<circle cx="12" cy="12" r="3.2" />
|
|
68
|
+
</svg>
|
|
69
|
+
</span>
|
|
70
|
+
{ofTotalPlacement === 'before' ? (
|
|
71
|
+
<>
|
|
72
|
+
<span className={styles.ofTotal}>{ofTotalLabel}</span>
|
|
73
|
+
<span>{focusPercent.toFixed(2)}%</span>
|
|
74
|
+
</>
|
|
75
|
+
) : (
|
|
76
|
+
<>
|
|
77
|
+
<span>{focusPercent.toFixed(2)}%</span>
|
|
78
|
+
<span className={styles.ofTotal}>{ofTotalLabel}</span>
|
|
79
|
+
</>
|
|
80
|
+
)}
|
|
81
|
+
<button
|
|
82
|
+
type="button"
|
|
83
|
+
className={styles.clearBtn}
|
|
84
|
+
aria-label={clearFocusLabel}
|
|
85
|
+
onClick={(event) => {
|
|
86
|
+
event.preventDefault();
|
|
87
|
+
event.stopPropagation();
|
|
88
|
+
onClearFocus();
|
|
89
|
+
}}
|
|
90
|
+
>
|
|
91
|
+
×
|
|
92
|
+
</button>
|
|
93
|
+
</span>
|
|
94
|
+
</>
|
|
95
|
+
)}
|
|
96
|
+
</div>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
@@ -24,9 +24,9 @@ THIS SOFTWARE.
|
|
|
24
24
|
/* eslint-disable no-continue */
|
|
25
25
|
import { createFF, Flamebearer, SpyName } from '../../models';
|
|
26
26
|
import {
|
|
27
|
-
formatPercent,
|
|
28
27
|
getFormatter,
|
|
29
28
|
ratioToPercent,
|
|
29
|
+
localizeDurationString,
|
|
30
30
|
} from '../../format/format';
|
|
31
31
|
import { fitToCanvasRect } from '../../fitMode/fitMode';
|
|
32
32
|
import { getRatios } from './utils';
|
|
@@ -53,12 +53,14 @@ import Flamegraph from './Flamegraph';
|
|
|
53
53
|
export interface CanvasI18nMessages {
|
|
54
54
|
collapsedLevelsSingular: string;
|
|
55
55
|
collapsedLevelsPlural: string;
|
|
56
|
+
isZh?: boolean;
|
|
56
57
|
}
|
|
57
58
|
|
|
58
59
|
/** Default English messages (fallback) */
|
|
59
60
|
const defaultCanvasMessages: CanvasI18nMessages = {
|
|
60
61
|
collapsedLevelsSingular: 'total (1 level collapsed)',
|
|
61
62
|
collapsedLevelsPlural: 'total ({n} levels collapsed)',
|
|
63
|
+
isZh: false,
|
|
62
64
|
};
|
|
63
65
|
|
|
64
66
|
type CanvasRendererConfig = Flamebearer & {
|
|
@@ -230,16 +232,16 @@ export default function RenderCanvas(props: CanvasRendererConfig) {
|
|
|
230
232
|
j < level.length - ff.jStep &&
|
|
231
233
|
barIndex + numBarTicks === ff.getBarOffset(level, j + ff.jStep) &&
|
|
232
234
|
ff.getBarTotal(level, j + ff.jStep) * pxPerTick <=
|
|
233
|
-
|
|
235
|
+
COLLAPSE_THRESHOLD &&
|
|
234
236
|
isHighlighted ===
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
237
|
+
((props.highlightQuery &&
|
|
238
|
+
nodeIsInQuery(
|
|
239
|
+
j + ff.jStep + ff.jName,
|
|
240
|
+
level,
|
|
241
|
+
names,
|
|
242
|
+
props.highlightQuery
|
|
243
|
+
)) ||
|
|
244
|
+
false)
|
|
243
245
|
) {
|
|
244
246
|
j += ff.jStep;
|
|
245
247
|
numBarTicks += ff.getBarTotal(level, j);
|
|
@@ -315,9 +317,9 @@ export default function RenderCanvas(props: CanvasRendererConfig) {
|
|
|
315
317
|
const longName = getLongName(
|
|
316
318
|
shortName,
|
|
317
319
|
numBarTicks,
|
|
318
|
-
numTicks,
|
|
319
320
|
sampleRate,
|
|
320
|
-
formatter
|
|
321
|
+
formatter,
|
|
322
|
+
messages
|
|
321
323
|
);
|
|
322
324
|
|
|
323
325
|
// Set the font syle
|
|
@@ -372,20 +374,20 @@ function getFunctionName(
|
|
|
372
374
|
return shortName;
|
|
373
375
|
}
|
|
374
376
|
|
|
377
|
+
/**
|
|
378
|
+
* 生成条形文本的长名称
|
|
379
|
+
* 格式: functionName (value) - 对齐 Grafana 风格,不显示百分比
|
|
380
|
+
*/
|
|
375
381
|
function getLongName(
|
|
376
382
|
shortName: string,
|
|
377
383
|
numBarTicks: number,
|
|
378
|
-
numTicks: number,
|
|
379
384
|
sampleRate: number,
|
|
380
|
-
formatter: ReturnType<typeof getFormatter
|
|
385
|
+
formatter: ReturnType<typeof getFormatter>,
|
|
386
|
+
messages: CanvasI18nMessages
|
|
381
387
|
) {
|
|
382
|
-
const
|
|
383
|
-
const
|
|
384
|
-
|
|
385
|
-
const longName = `${shortName} (${percent}, ${formatter.format(
|
|
386
|
-
numBarTicks,
|
|
387
|
-
sampleRate
|
|
388
|
-
)})`;
|
|
388
|
+
const formatted = formatter.format(numBarTicks, sampleRate);
|
|
389
|
+
const localized = localizeDurationString(formatted, !!messages.isZh);
|
|
390
|
+
const longName = `${shortName} (${localized || formatted})`;
|
|
389
391
|
|
|
390
392
|
return longName;
|
|
391
393
|
}
|
|
@@ -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,6 +57,20 @@ 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) {
|
|
@@ -68,8 +83,9 @@ export default function FlameGraphComponent(props: FlamegraphProps) {
|
|
|
68
83
|
() => ({
|
|
69
84
|
collapsedLevelsSingular: i18n.collapsedLevelsSingular,
|
|
70
85
|
collapsedLevelsPlural: i18n.collapsedLevelsPlural,
|
|
86
|
+
isZh: i18n.location !== 'Location',
|
|
71
87
|
}),
|
|
72
|
-
[i18n.collapsedLevelsSingular, i18n.collapsedLevelsPlural]
|
|
88
|
+
[i18n.collapsedLevelsSingular, i18n.collapsedLevelsPlural, i18n.location]
|
|
73
89
|
);
|
|
74
90
|
|
|
75
91
|
const [rightClickedNode, setRightClickedNode] = React.useState<
|
|
@@ -92,6 +108,7 @@ export default function FlameGraphComponent(props: FlamegraphProps) {
|
|
|
92
108
|
selectedItem,
|
|
93
109
|
updateView,
|
|
94
110
|
enableSandwichView = true,
|
|
111
|
+
breadcrumb,
|
|
95
112
|
} = props;
|
|
96
113
|
|
|
97
114
|
const { onZoom, onReset, isDirty, onFocusOnNode } = props;
|
|
@@ -356,6 +373,21 @@ export default function FlameGraphComponent(props: FlamegraphProps) {
|
|
|
356
373
|
'vertical-orientation': flamebearer.format === 'double',
|
|
357
374
|
})}
|
|
358
375
|
>
|
|
376
|
+
{breadcrumb && (
|
|
377
|
+
<FlamegraphBreadcrumb
|
|
378
|
+
totalValueText={breadcrumb.totalValueText}
|
|
379
|
+
totalSamplesText={breadcrumb.totalSamplesText}
|
|
380
|
+
samplesLabel={breadcrumb.samplesLabel}
|
|
381
|
+
unitLabel={breadcrumb.unitLabel}
|
|
382
|
+
hasFocus={breadcrumb.hasFocus}
|
|
383
|
+
focusPercent={breadcrumb.focusPercent}
|
|
384
|
+
focusLabel={breadcrumb.focusLabel}
|
|
385
|
+
ofTotalPlacement={breadcrumb.ofTotalPlacement}
|
|
386
|
+
ofTotalLabel={breadcrumb.ofTotalLabel}
|
|
387
|
+
clearFocusLabel={breadcrumb.clearFocusLabel}
|
|
388
|
+
onClearFocus={breadcrumb.onClearFocus}
|
|
389
|
+
/>
|
|
390
|
+
)}
|
|
359
391
|
{/* Header 已简化,仅在 single 模式下显示标题 */}
|
|
360
392
|
{/* DiffLegend 已移到 Toolbar */}
|
|
361
393
|
{headerVisible && (
|
|
@@ -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>(),
|
|
@@ -395,6 +407,66 @@ class FlameGraphRenderer extends Component<
|
|
|
395
407
|
});
|
|
396
408
|
};
|
|
397
409
|
|
|
410
|
+
clearFocus = () => {
|
|
411
|
+
this.setState((prevState) => ({
|
|
412
|
+
...prevState,
|
|
413
|
+
flamegraphConfigs: {
|
|
414
|
+
...prevState.flamegraphConfigs,
|
|
415
|
+
...this.resetFlamegraphState,
|
|
416
|
+
},
|
|
417
|
+
}));
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
getActiveRange = () => {
|
|
421
|
+
const { flamebearer } = this.state;
|
|
422
|
+
const { zoom, focusedNode } = this.state.flamegraphConfigs;
|
|
423
|
+
const totalTicks = flamebearer.numTicks;
|
|
424
|
+
|
|
425
|
+
if (!totalTicks) {
|
|
426
|
+
return { rangeMin: 0, rangeMax: 1 };
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const ff = createFF(flamebearer.format);
|
|
430
|
+
|
|
431
|
+
const getRangeFromNode = (node: Node) => {
|
|
432
|
+
const level = flamebearer.levels[node.i];
|
|
433
|
+
if (!level) {
|
|
434
|
+
return { rangeMin: 0, rangeMax: 1 };
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const offset = ff.getBarOffset(level, node.j);
|
|
438
|
+
const total = ff.getBarTotal(level, node.j);
|
|
439
|
+
return {
|
|
440
|
+
rangeMin: offset / totalTicks,
|
|
441
|
+
rangeMax: (offset + total) / totalTicks,
|
|
442
|
+
};
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
return zoom.match({
|
|
446
|
+
Just: (z) => {
|
|
447
|
+
return focusedNode.match({
|
|
448
|
+
Just: (f) => {
|
|
449
|
+
const focusRange = getRangeFromNode(f);
|
|
450
|
+
const zoomRange = getRangeFromNode(z);
|
|
451
|
+
if (
|
|
452
|
+
focusRange.rangeMax - focusRange.rangeMin <
|
|
453
|
+
zoomRange.rangeMax - zoomRange.rangeMin
|
|
454
|
+
) {
|
|
455
|
+
return focusRange;
|
|
456
|
+
}
|
|
457
|
+
return zoomRange;
|
|
458
|
+
},
|
|
459
|
+
Nothing: () => getRangeFromNode(z),
|
|
460
|
+
});
|
|
461
|
+
},
|
|
462
|
+
Nothing: () =>
|
|
463
|
+
focusedNode.match({
|
|
464
|
+
Just: (f) => getRangeFromNode(f),
|
|
465
|
+
Nothing: () => ({ rangeMin: 0, rangeMax: 1 }),
|
|
466
|
+
}),
|
|
467
|
+
});
|
|
468
|
+
};
|
|
469
|
+
|
|
398
470
|
// used as a variable instead of keeping in the state
|
|
399
471
|
// so that the flamegraph doesn't rerender unnecessarily
|
|
400
472
|
isDirty = () => {
|
|
@@ -445,6 +517,60 @@ class FlameGraphRenderer extends Component<
|
|
|
445
517
|
|
|
446
518
|
const toolbarVisible = this.shouldShowToolbar();
|
|
447
519
|
|
|
520
|
+
const dataUnavailable =
|
|
521
|
+
!this.state.flamebearer || this.state.flamebearer.names.length <= 1;
|
|
522
|
+
const i18n = this.context;
|
|
523
|
+
const isZh = i18n.location !== 'Location';
|
|
524
|
+
const totalTicks = this.state.flamebearer.numTicks;
|
|
525
|
+
const formatter = getFormatter(
|
|
526
|
+
totalTicks,
|
|
527
|
+
this.state.flamebearer.sampleRate,
|
|
528
|
+
this.state.flamebearer.units
|
|
529
|
+
);
|
|
530
|
+
const totalValueRaw = formatter.format(
|
|
531
|
+
totalTicks,
|
|
532
|
+
this.state.flamebearer.sampleRate
|
|
533
|
+
);
|
|
534
|
+
const totalValueText =
|
|
535
|
+
localizeDurationString(totalValueRaw, isZh) || totalValueRaw;
|
|
536
|
+
const totalSamplesText = formatSampleCount(
|
|
537
|
+
totalTicks,
|
|
538
|
+
i18n.sampleCountFormat
|
|
539
|
+
);
|
|
540
|
+
const samplesLabel = i18n.tooltipSamples.replace(/[::]\s*$/, '');
|
|
541
|
+
const unitLabel =
|
|
542
|
+
i18n.tooltipUnitTitles[this.state.flamebearer.units]?.formattedValue ||
|
|
543
|
+
i18n.tooltipUnitTitles.unknown.formattedValue;
|
|
544
|
+
const activeRange = this.getActiveRange();
|
|
545
|
+
const hasFocus =
|
|
546
|
+
activeRange.rangeMin > 0 || activeRange.rangeMax < 1;
|
|
547
|
+
const activeRangeTicks = Math.max(
|
|
548
|
+
0,
|
|
549
|
+
Math.min(
|
|
550
|
+
totalTicks,
|
|
551
|
+
(activeRange.rangeMax - activeRange.rangeMin) * totalTicks
|
|
552
|
+
)
|
|
553
|
+
);
|
|
554
|
+
const focusPercent =
|
|
555
|
+
totalTicks > 0 ? (activeRangeTicks / totalTicks) * 100 : 0;
|
|
556
|
+
const focusLabel = (() => {
|
|
557
|
+
const ff = createFF(this.state.flamebearer.format);
|
|
558
|
+
const focusNode = this.state.flamegraphConfigs.focusedNode.mapOrElse(
|
|
559
|
+
() =>
|
|
560
|
+
this.state.flamegraphConfigs.zoom.mapOrElse(() => null, (z) => z),
|
|
561
|
+
(f) => f
|
|
562
|
+
);
|
|
563
|
+
if (!focusNode) {
|
|
564
|
+
return '';
|
|
565
|
+
}
|
|
566
|
+
const level = this.state.flamebearer.levels[focusNode.i];
|
|
567
|
+
if (!level) {
|
|
568
|
+
return '';
|
|
569
|
+
}
|
|
570
|
+
const nameIndex = ff.getBarName(level, focusNode.j);
|
|
571
|
+
return this.state.flamebearer.names[nameIndex] || '';
|
|
572
|
+
})();
|
|
573
|
+
|
|
448
574
|
const flameGraphPane = (
|
|
449
575
|
<Graph
|
|
450
576
|
key="flamegraph-pane"
|
|
@@ -467,6 +593,23 @@ class FlameGraphRenderer extends Component<
|
|
|
467
593
|
toolbarVisible={toolbarVisible}
|
|
468
594
|
setPalette={this.handleSetPalette}
|
|
469
595
|
enableSandwichView={this.props.enableSandwichView}
|
|
596
|
+
breadcrumb={
|
|
597
|
+
dataUnavailable
|
|
598
|
+
? undefined
|
|
599
|
+
: {
|
|
600
|
+
totalValueText,
|
|
601
|
+
totalSamplesText,
|
|
602
|
+
samplesLabel,
|
|
603
|
+
unitLabel,
|
|
604
|
+
hasFocus,
|
|
605
|
+
focusPercent,
|
|
606
|
+
focusLabel,
|
|
607
|
+
ofTotalPlacement: isZh ? 'before' : 'after',
|
|
608
|
+
ofTotalLabel: i18n.ofTotal,
|
|
609
|
+
clearFocusLabel: i18n.clearFocus,
|
|
610
|
+
onClearFocus: this.clearFocus,
|
|
611
|
+
}
|
|
612
|
+
}
|
|
470
613
|
/>
|
|
471
614
|
);
|
|
472
615
|
|
|
@@ -574,8 +717,6 @@ class FlameGraphRenderer extends Component<
|
|
|
574
717
|
// // rightTicks?: number;
|
|
575
718
|
// } & addTicks;
|
|
576
719
|
|
|
577
|
-
const dataUnavailable =
|
|
578
|
-
!this.state.flamebearer || this.state.flamebearer.names.length <= 1;
|
|
579
720
|
const panes = decidePanesOrder(
|
|
580
721
|
this.state.view,
|
|
581
722
|
flameGraphPane,
|
|
@@ -671,4 +812,4 @@ function decidePanesOrder(
|
|
|
671
812
|
}
|
|
672
813
|
}
|
|
673
814
|
|
|
674
|
-
export default FlameGraphRenderer;
|
|
815
|
+
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,
|