@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
package/package.json
CHANGED
|
@@ -23,6 +23,8 @@ type XYWithinBounds = { x: number; y: number } & { __brand: 'XYWithinBounds' };
|
|
|
23
23
|
|
|
24
24
|
export default class Flamegraph {
|
|
25
25
|
private ff: ReturnType<typeof createFF>;
|
|
26
|
+
private cachedRange: { rangeMin: number; rangeMax: number } | null = null;
|
|
27
|
+
private cachedPxPerTick: number | null = null;
|
|
26
28
|
|
|
27
29
|
constructor(
|
|
28
30
|
private readonly flamebearer: Flamebearer,
|
|
@@ -73,8 +75,11 @@ export default class Flamegraph {
|
|
|
73
75
|
}
|
|
74
76
|
}
|
|
75
77
|
|
|
76
|
-
public render() {
|
|
77
|
-
const { rangeMin, rangeMax } = this.
|
|
78
|
+
public render(options?: { renderRects?: boolean; renderText?: boolean }) {
|
|
79
|
+
const { rangeMin, rangeMax, pxPerTick } = this.updateCachedMetrics();
|
|
80
|
+
const tickToX = (i: number) => {
|
|
81
|
+
return (i - this.flamebearer.numTicks * rangeMin) * pxPerTick;
|
|
82
|
+
};
|
|
78
83
|
|
|
79
84
|
const props = {
|
|
80
85
|
canvas: this.canvas,
|
|
@@ -97,10 +102,12 @@ export default class Flamegraph {
|
|
|
97
102
|
highlightQuery: this.highlightQuery,
|
|
98
103
|
zoom: this.zoom,
|
|
99
104
|
focusedNode: this.focusedNode,
|
|
100
|
-
pxPerTick
|
|
101
|
-
tickToX
|
|
105
|
+
pxPerTick,
|
|
106
|
+
tickToX,
|
|
102
107
|
palette: this.palette,
|
|
103
108
|
messages: this.messages,
|
|
109
|
+
renderRects: options?.renderRects,
|
|
110
|
+
renderText: options?.renderText,
|
|
104
111
|
};
|
|
105
112
|
|
|
106
113
|
const { format: viewType } = this.flamebearer;
|
|
@@ -125,18 +132,37 @@ export default class Flamegraph {
|
|
|
125
132
|
}
|
|
126
133
|
|
|
127
134
|
private pxPerTick() {
|
|
135
|
+
if (this.cachedPxPerTick !== null) {
|
|
136
|
+
return this.cachedPxPerTick;
|
|
137
|
+
}
|
|
128
138
|
const { rangeMin, rangeMax } = this.getRange();
|
|
129
139
|
// const graphWidth = this.canvas.width;
|
|
130
140
|
const graphWidth = this.getCanvasWidth();
|
|
131
|
-
|
|
132
|
-
|
|
141
|
+
const pxPerTick =
|
|
142
|
+
graphWidth / this.flamebearer.numTicks / (rangeMax - rangeMin);
|
|
143
|
+
this.cachedRange = { rangeMin, rangeMax };
|
|
144
|
+
this.cachedPxPerTick = pxPerTick;
|
|
145
|
+
return pxPerTick;
|
|
133
146
|
}
|
|
134
147
|
|
|
135
148
|
private tickToX = (i: number) => {
|
|
136
|
-
const
|
|
137
|
-
|
|
149
|
+
const rangeMin = this.cachedRange
|
|
150
|
+
? this.cachedRange.rangeMin
|
|
151
|
+
: this.getRange().rangeMin;
|
|
152
|
+
const pxPerTick = this.cachedPxPerTick ?? this.pxPerTick();
|
|
153
|
+
return (i - this.flamebearer.numTicks * rangeMin) * pxPerTick;
|
|
138
154
|
};
|
|
139
155
|
|
|
156
|
+
private updateCachedMetrics() {
|
|
157
|
+
const { rangeMin, rangeMax } = this.getRange();
|
|
158
|
+
const graphWidth = this.getCanvasWidth();
|
|
159
|
+
const pxPerTick =
|
|
160
|
+
graphWidth / this.flamebearer.numTicks / (rangeMax - rangeMin);
|
|
161
|
+
this.cachedRange = { rangeMin, rangeMax };
|
|
162
|
+
this.cachedPxPerTick = pxPerTick;
|
|
163
|
+
return { rangeMin, rangeMax, pxPerTick };
|
|
164
|
+
}
|
|
165
|
+
|
|
140
166
|
private getRange() {
|
|
141
167
|
const { ff } = this;
|
|
142
168
|
|
|
@@ -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';
|
|
@@ -41,10 +41,11 @@ import {
|
|
|
41
41
|
colorBasedOnDiffPercent,
|
|
42
42
|
colorBasedOnPackageName,
|
|
43
43
|
colorGreyscale,
|
|
44
|
+
diffPercent,
|
|
44
45
|
getPackageNameFromStackTrace,
|
|
46
|
+
NewDiffColor,
|
|
45
47
|
} from './color';
|
|
46
48
|
import type { FlamegraphPalette } from './colorPalette';
|
|
47
|
-
import { isMatch } from '../../search';
|
|
48
49
|
// there's a dependency cycle here but it should be fine
|
|
49
50
|
/* eslint-disable-next-line import/no-cycle */
|
|
50
51
|
import Flamegraph from './Flamegraph';
|
|
@@ -53,12 +54,14 @@ import Flamegraph from './Flamegraph';
|
|
|
53
54
|
export interface CanvasI18nMessages {
|
|
54
55
|
collapsedLevelsSingular: string;
|
|
55
56
|
collapsedLevelsPlural: string;
|
|
57
|
+
isZh?: boolean;
|
|
56
58
|
}
|
|
57
59
|
|
|
58
60
|
/** Default English messages (fallback) */
|
|
59
61
|
const defaultCanvasMessages: CanvasI18nMessages = {
|
|
60
62
|
collapsedLevelsSingular: 'total (1 level collapsed)',
|
|
61
63
|
collapsedLevelsPlural: 'total ({n} levels collapsed)',
|
|
64
|
+
isZh: false,
|
|
62
65
|
};
|
|
63
66
|
|
|
64
67
|
type CanvasRendererConfig = Flamebearer & {
|
|
@@ -67,6 +70,8 @@ type CanvasRendererConfig = Flamebearer & {
|
|
|
67
70
|
fitMode: ConstructorParameters<typeof Flamegraph>[3];
|
|
68
71
|
highlightQuery: ConstructorParameters<typeof Flamegraph>[4];
|
|
69
72
|
zoom: ConstructorParameters<typeof Flamegraph>[5];
|
|
73
|
+
renderRects?: boolean;
|
|
74
|
+
renderText?: boolean;
|
|
70
75
|
|
|
71
76
|
/**
|
|
72
77
|
* Used when zooming, values between 0 and 1.
|
|
@@ -104,6 +109,8 @@ function getCollapsedText(
|
|
|
104
109
|
}
|
|
105
110
|
|
|
106
111
|
export default function RenderCanvas(props: CanvasRendererConfig) {
|
|
112
|
+
const renderRects = props.renderRects !== false;
|
|
113
|
+
const renderText = props.renderText !== false;
|
|
107
114
|
const { canvas, fitMode, units, tickToX, levels, palette } = props;
|
|
108
115
|
const { numTicks, sampleRate, pxPerTick } = props;
|
|
109
116
|
const { rangeMin, rangeMax } = props;
|
|
@@ -137,6 +144,9 @@ export default function RenderCanvas(props: CanvasRendererConfig) {
|
|
|
137
144
|
() => 0,
|
|
138
145
|
(f) => f.i
|
|
139
146
|
);
|
|
147
|
+
const focusOffset = isFocused ? BAR_HEIGHT : 0;
|
|
148
|
+
const highlightModeOn =
|
|
149
|
+
!!props.highlightQuery && props.highlightQuery.length > 0;
|
|
140
150
|
|
|
141
151
|
const canvasHeight =
|
|
142
152
|
PX_PER_LEVEL * (levels.length - topLevel) + (isFocused ? BAR_HEIGHT : 0);
|
|
@@ -150,18 +160,50 @@ export default function RenderCanvas(props: CanvasRendererConfig) {
|
|
|
150
160
|
ctx.scale(2, 2);
|
|
151
161
|
}
|
|
152
162
|
|
|
163
|
+
ctx.textBaseline = 'middle';
|
|
164
|
+
ctx.font =
|
|
165
|
+
'400 11.5px SFMono-Regular, Consolas, Liberation Mono, Menlo, monospace';
|
|
166
|
+
// Since this is a monospaced font any character would do
|
|
167
|
+
const characterSize = ctx.measureText('a').width;
|
|
168
|
+
|
|
153
169
|
const { names } = props;
|
|
170
|
+
const formattedValueCache = new Map<number, string>();
|
|
171
|
+
const highlightByNameIndex = highlightModeOn
|
|
172
|
+
? buildHighlightByNameIndex(names, props.highlightQuery)
|
|
173
|
+
: null;
|
|
174
|
+
const colorByNameIndex =
|
|
175
|
+
props.format === 'single'
|
|
176
|
+
? buildColorByNameIndex(names, props.spyName as SpyName, palette)
|
|
177
|
+
: null;
|
|
178
|
+
const diffColorFn =
|
|
179
|
+
props.format === 'double'
|
|
180
|
+
? NewDiffColor({
|
|
181
|
+
name: palette.name,
|
|
182
|
+
goodColor: palette.goodColor,
|
|
183
|
+
neutralColor: palette.neutralColor,
|
|
184
|
+
badColor: palette.badColor,
|
|
185
|
+
})
|
|
186
|
+
: null;
|
|
187
|
+
const isHighlightedAt = (level: number[], j: number) => {
|
|
188
|
+
const nameIndex = level[j + ff.jName];
|
|
189
|
+
if (!nameIndex || nameIndex < 0 || !highlightByNameIndex) {
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
return highlightByNameIndex[nameIndex] === 1;
|
|
193
|
+
};
|
|
154
194
|
// are we focused?
|
|
155
195
|
// if so, add an initial bar telling it's a collapsed one
|
|
156
196
|
// TODO clean this up
|
|
157
197
|
if (isFocused) {
|
|
158
198
|
const width = numTicks * pxPerTick;
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
199
|
+
if (renderRects) {
|
|
200
|
+
ctx.beginPath();
|
|
201
|
+
ctx.rect(0, 0, numTicks * pxPerTick, BAR_HEIGHT);
|
|
202
|
+
// TODO find a neutral color
|
|
203
|
+
// TODO use getColor ?
|
|
204
|
+
ctx.fillStyle = colorGreyscale(200, 1).rgb().string();
|
|
205
|
+
ctx.fill();
|
|
206
|
+
}
|
|
165
207
|
|
|
166
208
|
// 使用 i18n 翻译的 collapsed 文本
|
|
167
209
|
const shortName = focusedNode.mapOrElse(
|
|
@@ -169,14 +211,6 @@ export default function RenderCanvas(props: CanvasRendererConfig) {
|
|
|
169
211
|
(f) => getCollapsedText(f.i - 1, messages)
|
|
170
212
|
);
|
|
171
213
|
|
|
172
|
-
// Set the font syle
|
|
173
|
-
// It's important to set the font BEFORE calculating 'characterSize'
|
|
174
|
-
// Since it will be used to calculate how many characters can fit
|
|
175
|
-
ctx.textBaseline = 'middle';
|
|
176
|
-
ctx.font =
|
|
177
|
-
'400 11.5px SFMono-Regular, Consolas, Liberation Mono, Menlo, monospace';
|
|
178
|
-
// Since this is a monospaced font any character would do
|
|
179
|
-
const characterSize = ctx.measureText('a').width;
|
|
180
214
|
const fitCalc = fitToCanvasRect({
|
|
181
215
|
mode: fitMode,
|
|
182
216
|
charSize: characterSize,
|
|
@@ -189,12 +223,20 @@ export default function RenderCanvas(props: CanvasRendererConfig) {
|
|
|
189
223
|
const y = 0;
|
|
190
224
|
const sh = BAR_HEIGHT;
|
|
191
225
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
226
|
+
if (renderText) {
|
|
227
|
+
ctx.beginPath();
|
|
228
|
+
ctx.rect(0, 0, width, BAR_HEIGHT);
|
|
229
|
+
ctx.save();
|
|
230
|
+
ctx.clip();
|
|
231
|
+
ctx.fillStyle = 'black';
|
|
232
|
+
const namePosX = Math.round(Math.max(x, 0));
|
|
233
|
+
ctx.fillText(
|
|
234
|
+
fitCalc.text,
|
|
235
|
+
namePosX + fitCalc.marginLeft,
|
|
236
|
+
y + sh / 2 + 1
|
|
237
|
+
);
|
|
238
|
+
ctx.restore();
|
|
239
|
+
}
|
|
198
240
|
}
|
|
199
241
|
|
|
200
242
|
for (let i = 0; i < levels.length - topLevel; i += 1) {
|
|
@@ -206,19 +248,11 @@ export default function RenderCanvas(props: CanvasRendererConfig) {
|
|
|
206
248
|
for (let j = 0; j < level.length; j += ff.jStep) {
|
|
207
249
|
const barIndex = ff.getBarOffset(level, j);
|
|
208
250
|
const x = tickToX(barIndex);
|
|
209
|
-
const y = i * PX_PER_LEVEL +
|
|
251
|
+
const y = i * PX_PER_LEVEL + focusOffset;
|
|
210
252
|
|
|
211
253
|
const sh = BAR_HEIGHT;
|
|
212
254
|
|
|
213
|
-
const highlightModeOn
|
|
214
|
-
!!props.highlightQuery && props.highlightQuery.length > 0;
|
|
215
|
-
|
|
216
|
-
const isHighlighted = nodeIsInQuery(
|
|
217
|
-
j + ff.jName,
|
|
218
|
-
level,
|
|
219
|
-
names,
|
|
220
|
-
props.highlightQuery
|
|
221
|
-
);
|
|
255
|
+
const isHighlighted = highlightModeOn && isHighlightedAt(level, j);
|
|
222
256
|
|
|
223
257
|
let numBarTicks = ff.getBarTotal(level, j);
|
|
224
258
|
|
|
@@ -230,16 +264,9 @@ export default function RenderCanvas(props: CanvasRendererConfig) {
|
|
|
230
264
|
j < level.length - ff.jStep &&
|
|
231
265
|
barIndex + numBarTicks === ff.getBarOffset(level, j + ff.jStep) &&
|
|
232
266
|
ff.getBarTotal(level, j + ff.jStep) * pxPerTick <=
|
|
233
|
-
|
|
267
|
+
COLLAPSE_THRESHOLD &&
|
|
234
268
|
isHighlighted ===
|
|
235
|
-
|
|
236
|
-
nodeIsInQuery(
|
|
237
|
-
j + ff.jStep + ff.jName,
|
|
238
|
-
level,
|
|
239
|
-
names,
|
|
240
|
-
props.highlightQuery
|
|
241
|
-
)) ||
|
|
242
|
-
false)
|
|
269
|
+
(highlightModeOn && isHighlightedAt(level, j + ff.jStep))
|
|
243
270
|
) {
|
|
244
271
|
j += ff.jStep;
|
|
245
272
|
numBarTicks += ff.getBarTotal(level, j);
|
|
@@ -273,6 +300,8 @@ export default function RenderCanvas(props: CanvasRendererConfig) {
|
|
|
273
300
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
|
|
274
301
|
spyName: spyName as SpyName,
|
|
275
302
|
palette,
|
|
303
|
+
colorByNameIndex,
|
|
304
|
+
diffColorFn,
|
|
276
305
|
};
|
|
277
306
|
|
|
278
307
|
switch (format) {
|
|
@@ -292,18 +321,19 @@ export default function RenderCanvas(props: CanvasRendererConfig) {
|
|
|
292
321
|
}
|
|
293
322
|
};
|
|
294
323
|
|
|
295
|
-
const color = getColor();
|
|
296
|
-
|
|
297
324
|
ctx.beginPath();
|
|
298
325
|
ctx.rect(x, y, sw, sh);
|
|
299
|
-
|
|
300
|
-
|
|
326
|
+
if (renderRects) {
|
|
327
|
+
const color = getColor();
|
|
328
|
+
ctx.fillStyle = color.string();
|
|
329
|
+
ctx.fill();
|
|
330
|
+
}
|
|
301
331
|
|
|
302
332
|
/*******************************/
|
|
303
333
|
/* D r a w T e x t */
|
|
304
334
|
/*******************************/
|
|
305
335
|
// don't write text if there's not enough space for a single letter
|
|
306
|
-
if (collapsed) {
|
|
336
|
+
if (!renderText || collapsed) {
|
|
307
337
|
continue;
|
|
308
338
|
}
|
|
309
339
|
|
|
@@ -315,19 +345,12 @@ export default function RenderCanvas(props: CanvasRendererConfig) {
|
|
|
315
345
|
const longName = getLongName(
|
|
316
346
|
shortName,
|
|
317
347
|
numBarTicks,
|
|
318
|
-
numTicks,
|
|
319
348
|
sampleRate,
|
|
320
|
-
formatter
|
|
349
|
+
formatter,
|
|
350
|
+
messages,
|
|
351
|
+
formattedValueCache
|
|
321
352
|
);
|
|
322
353
|
|
|
323
|
-
// Set the font syle
|
|
324
|
-
// It's important to set the font BEFORE calculating 'characterSize'
|
|
325
|
-
// Since it will be used to calculate how many characters can fit
|
|
326
|
-
ctx.textBaseline = 'middle';
|
|
327
|
-
ctx.font =
|
|
328
|
-
'400 11.5px SFMono-Regular, Consolas, Liberation Mono, Menlo, monospace';
|
|
329
|
-
// Since this is a monospaced font any character would do
|
|
330
|
-
const characterSize = ctx.measureText('a').width;
|
|
331
354
|
const fitCalc = fitToCanvasRect({
|
|
332
355
|
mode: fitMode,
|
|
333
356
|
charSize: characterSize,
|
|
@@ -372,20 +395,26 @@ function getFunctionName(
|
|
|
372
395
|
return shortName;
|
|
373
396
|
}
|
|
374
397
|
|
|
398
|
+
/**
|
|
399
|
+
* 生成条形文本的长名称
|
|
400
|
+
* 格式: functionName (value) - 对齐 Grafana 风格,不显示百分比
|
|
401
|
+
*/
|
|
375
402
|
function getLongName(
|
|
376
403
|
shortName: string,
|
|
377
404
|
numBarTicks: number,
|
|
378
|
-
numTicks: number,
|
|
379
405
|
sampleRate: number,
|
|
380
|
-
formatter: ReturnType<typeof getFormatter
|
|
406
|
+
formatter: ReturnType<typeof getFormatter>,
|
|
407
|
+
messages: CanvasI18nMessages,
|
|
408
|
+
formattedValueCache?: Map<number, string>
|
|
381
409
|
) {
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
410
|
+
let cachedValue = formattedValueCache?.get(numBarTicks);
|
|
411
|
+
if (!cachedValue) {
|
|
412
|
+
const formatted = formatter.format(numBarTicks, sampleRate);
|
|
413
|
+
const localized = localizeDurationString(formatted, !!messages.isZh);
|
|
414
|
+
cachedValue = localized || formatted;
|
|
415
|
+
formattedValueCache?.set(numBarTicks, cachedValue);
|
|
416
|
+
}
|
|
417
|
+
const longName = `${shortName} (${cachedValue})`;
|
|
389
418
|
|
|
390
419
|
return longName;
|
|
391
420
|
}
|
|
@@ -401,6 +430,8 @@ type getColorCfg = {
|
|
|
401
430
|
names: string[];
|
|
402
431
|
spyName: SpyName;
|
|
403
432
|
palette: FlamegraphPalette;
|
|
433
|
+
colorByNameIndex: ReturnType<typeof colorBasedOnPackageName>[] | null;
|
|
434
|
+
diffColorFn: ReturnType<typeof NewDiffColor> | null;
|
|
404
435
|
};
|
|
405
436
|
|
|
406
437
|
function getColorCommon({
|
|
@@ -448,6 +479,10 @@ function getColorSingle(cfg: getColorCfg) {
|
|
|
448
479
|
});
|
|
449
480
|
l = -1;
|
|
450
481
|
}
|
|
482
|
+
if (cfg.colorByNameIndex && l >= 0 && cfg.colorByNameIndex[l]) {
|
|
483
|
+
return cfg.colorByNameIndex[l].alpha(a);
|
|
484
|
+
}
|
|
485
|
+
|
|
451
486
|
const name = cfg.names[l] || '';
|
|
452
487
|
const packageName = getPackageNameFromStackTrace(cfg.spyName, name) || '';
|
|
453
488
|
|
|
@@ -475,28 +510,11 @@ function getColorDouble(
|
|
|
475
510
|
const leftPercent = ratioToPercent(leftRatio);
|
|
476
511
|
const rightPercent = ratioToPercent(rightRatio);
|
|
477
512
|
|
|
478
|
-
|
|
479
|
-
a
|
|
480
|
-
);
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
function nodeIsInQuery(
|
|
484
|
-
index: number,
|
|
485
|
-
level: number[],
|
|
486
|
-
names: string[],
|
|
487
|
-
query: string
|
|
488
|
-
) {
|
|
489
|
-
const l = level[index];
|
|
490
|
-
if (!l) {
|
|
491
|
-
return false;
|
|
513
|
+
if (cfg.diffColorFn) {
|
|
514
|
+
return cfg.diffColorFn(diffPercent(leftPercent, rightPercent)).alpha(a);
|
|
492
515
|
}
|
|
493
516
|
|
|
494
|
-
|
|
495
|
-
if (!l2) {
|
|
496
|
-
return false;
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
return isMatch(query, l2);
|
|
517
|
+
return colorBasedOnDiffPercent(cfg.palette, leftPercent, rightPercent).alpha(a);
|
|
500
518
|
}
|
|
501
519
|
|
|
502
520
|
function getCanvasWidth(canvas: HTMLCanvasElement) {
|
|
@@ -505,3 +523,34 @@ function getCanvasWidth(canvas: HTMLCanvasElement) {
|
|
|
505
523
|
// so we also fallback to canvas.width
|
|
506
524
|
return canvas.clientWidth || canvas.width;
|
|
507
525
|
}
|
|
526
|
+
|
|
527
|
+
function buildHighlightByNameIndex(names: string[], query: string) {
|
|
528
|
+
const queryLower = query.toLowerCase();
|
|
529
|
+
const matches = new Uint8Array(names.length);
|
|
530
|
+
for (let i = 1; i < names.length; i += 1) {
|
|
531
|
+
const name = names[i];
|
|
532
|
+
if (!name) {
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
535
|
+
if (name.toLowerCase().includes(queryLower)) {
|
|
536
|
+
matches[i] = 1;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
return matches;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function buildColorByNameIndex(
|
|
543
|
+
names: string[],
|
|
544
|
+
spyName: SpyName,
|
|
545
|
+
palette: FlamegraphPalette
|
|
546
|
+
) {
|
|
547
|
+
const colors = new Array<ReturnType<typeof colorBasedOnPackageName>>(
|
|
548
|
+
names.length
|
|
549
|
+
);
|
|
550
|
+
for (let i = 0; i < names.length; i += 1) {
|
|
551
|
+
const name = names[i] || '';
|
|
552
|
+
const packageName = getPackageNameFromStackTrace(spyName, name) || '';
|
|
553
|
+
colors[i] = colorBasedOnPackageName(palette, packageName);
|
|
554
|
+
}
|
|
555
|
+
return colors;
|
|
556
|
+
}
|