@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.
Files changed (35) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/dist/FlameGraph/FlameGraphComponent/Flamegraph.d.ts +7 -1
  3. package/dist/FlameGraph/FlameGraphComponent/Flamegraph.d.ts.map +1 -1
  4. package/dist/FlameGraph/FlameGraphComponent/FlamegraphBreadcrumb.d.ts +16 -0
  5. package/dist/FlameGraph/FlameGraphComponent/FlamegraphBreadcrumb.d.ts.map +1 -0
  6. package/dist/FlameGraph/FlameGraphComponent/Flamegraph_render.d.ts +3 -0
  7. package/dist/FlameGraph/FlameGraphComponent/Flamegraph_render.d.ts.map +1 -1
  8. package/dist/FlameGraph/FlameGraphComponent/index.d.ts +13 -0
  9. package/dist/FlameGraph/FlameGraphComponent/index.d.ts.map +1 -1
  10. package/dist/FlameGraph/FlameGraphRenderer.d.ts +9 -1
  11. package/dist/FlameGraph/FlameGraphRenderer.d.ts.map +1 -1
  12. package/dist/Tooltip/Tooltip.d.ts.map +1 -1
  13. package/dist/format/format.d.ts +14 -0
  14. package/dist/format/format.d.ts.map +1 -1
  15. package/dist/i18n.d.ts +7 -0
  16. package/dist/i18n.d.ts.map +1 -1
  17. package/dist/index.cjs.js +3 -3
  18. package/dist/index.cjs.js.map +1 -1
  19. package/dist/index.esm.js +3 -3
  20. package/dist/index.esm.js.map +1 -1
  21. package/dist/index.node.cjs.js +4 -4
  22. package/dist/index.node.cjs.js.map +1 -1
  23. package/dist/index.node.esm.js +4 -4
  24. package/dist/index.node.esm.js.map +1 -1
  25. package/package.json +1 -1
  26. package/src/FlameGraph/FlameGraphComponent/Flamegraph.ts +34 -8
  27. package/src/FlameGraph/FlameGraphComponent/FlamegraphBreadcrumb.module.scss +106 -0
  28. package/src/FlameGraph/FlameGraphComponent/FlamegraphBreadcrumb.tsx +98 -0
  29. package/src/FlameGraph/FlameGraphComponent/Flamegraph_render.ts +134 -85
  30. package/src/FlameGraph/FlameGraphComponent/canvas.module.css +9 -0
  31. package/src/FlameGraph/FlameGraphComponent/index.tsx +105 -21
  32. package/src/FlameGraph/FlameGraphRenderer.tsx +169 -21
  33. package/src/Tooltip/Tooltip.tsx +1 -75
  34. package/src/format/format.ts +98 -0
  35. package/src/i18n.tsx +27 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kylincloud/flamegraph",
3
- "version": "0.35.23",
3
+ "version": "0.35.25",
4
4
  "description": "KylinCloud flamegraph renderer (Pyroscope-based)",
5
5
  "license": "Apache-2.0",
6
6
  "main": "dist/index.node.cjs.js",
@@ -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.getRange();
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: this.pxPerTick(),
101
- tickToX: this.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
- return graphWidth / this.flamebearer.numTicks / (rangeMax - rangeMin);
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 { rangeMin } = this.getRange();
137
- return (i - this.flamebearer.numTicks * rangeMin) * this.pxPerTick();
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}>&gt;</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
- ctx.beginPath();
160
- ctx.rect(0, 0, numTicks * pxPerTick, BAR_HEIGHT);
161
- // TODO find a neutral color
162
- // TODO use getColor ?
163
- ctx.fillStyle = colorGreyscale(200, 1).rgb().string();
164
- ctx.fill();
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
- ctx.save();
193
- ctx.clip();
194
- ctx.fillStyle = 'black';
195
- const namePosX = Math.round(Math.max(x, 0));
196
- ctx.fillText(fitCalc.text, namePosX + fitCalc.marginLeft, y + sh / 2 + 1);
197
- ctx.restore();
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 + (isFocused ? BAR_HEIGHT : 0);
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
- COLLAPSE_THRESHOLD &&
267
+ COLLAPSE_THRESHOLD &&
234
268
  isHighlighted ===
235
- ((props.highlightQuery &&
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
- ctx.fillStyle = color.string();
300
- ctx.fill();
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
- const ratio = numBarTicks / numTicks;
383
- const percent = formatPercent(ratio);
384
-
385
- const longName = `${shortName} (${percent}, ${formatter.format(
386
- numBarTicks,
387
- sampleRate
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
- return colorBasedOnDiffPercent(cfg.palette, leftPercent, rightPercent).alpha(
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
- const l2 = names[l];
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
+ }
@@ -4,3 +4,12 @@
4
4
  position: relative;
5
5
  z-index: 1;
6
6
  }
7
+
8
+ .textCanvas {
9
+ position: absolute;
10
+ left: 0;
11
+ top: 0;
12
+ width: 100%;
13
+ pointer-events: none;
14
+ z-index: 2;
15
+ }