@kylincloud/flamegraph 0.35.24 → 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 +6 -0
- package/dist/FlameGraph/FlameGraphComponent/Flamegraph.d.ts +7 -1
- package/dist/FlameGraph/FlameGraphComponent/Flamegraph.d.ts.map +1 -1
- package/dist/FlameGraph/FlameGraphComponent/Flamegraph_render.d.ts +2 -0
- package/dist/FlameGraph/FlameGraphComponent/Flamegraph_render.d.ts.map +1 -1
- package/dist/FlameGraph/FlameGraphComponent/index.d.ts.map +1 -1
- package/dist/FlameGraph/FlameGraphRenderer.d.ts.map +1 -1
- package/dist/index.cjs.js +2 -2
- 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 +2 -2
- 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/Flamegraph_render.ts +124 -77
- package/src/FlameGraph/FlameGraphComponent/canvas.module.css +9 -0
- package/src/FlameGraph/FlameGraphComponent/index.tsx +72 -20
- package/src/FlameGraph/FlameGraphRenderer.tsx +25 -18
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
|
|
|
@@ -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';
|
|
@@ -69,6 +70,8 @@ type CanvasRendererConfig = Flamebearer & {
|
|
|
69
70
|
fitMode: ConstructorParameters<typeof Flamegraph>[3];
|
|
70
71
|
highlightQuery: ConstructorParameters<typeof Flamegraph>[4];
|
|
71
72
|
zoom: ConstructorParameters<typeof Flamegraph>[5];
|
|
73
|
+
renderRects?: boolean;
|
|
74
|
+
renderText?: boolean;
|
|
72
75
|
|
|
73
76
|
/**
|
|
74
77
|
* Used when zooming, values between 0 and 1.
|
|
@@ -106,6 +109,8 @@ function getCollapsedText(
|
|
|
106
109
|
}
|
|
107
110
|
|
|
108
111
|
export default function RenderCanvas(props: CanvasRendererConfig) {
|
|
112
|
+
const renderRects = props.renderRects !== false;
|
|
113
|
+
const renderText = props.renderText !== false;
|
|
109
114
|
const { canvas, fitMode, units, tickToX, levels, palette } = props;
|
|
110
115
|
const { numTicks, sampleRate, pxPerTick } = props;
|
|
111
116
|
const { rangeMin, rangeMax } = props;
|
|
@@ -139,6 +144,9 @@ export default function RenderCanvas(props: CanvasRendererConfig) {
|
|
|
139
144
|
() => 0,
|
|
140
145
|
(f) => f.i
|
|
141
146
|
);
|
|
147
|
+
const focusOffset = isFocused ? BAR_HEIGHT : 0;
|
|
148
|
+
const highlightModeOn =
|
|
149
|
+
!!props.highlightQuery && props.highlightQuery.length > 0;
|
|
142
150
|
|
|
143
151
|
const canvasHeight =
|
|
144
152
|
PX_PER_LEVEL * (levels.length - topLevel) + (isFocused ? BAR_HEIGHT : 0);
|
|
@@ -152,18 +160,50 @@ export default function RenderCanvas(props: CanvasRendererConfig) {
|
|
|
152
160
|
ctx.scale(2, 2);
|
|
153
161
|
}
|
|
154
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
|
+
|
|
155
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
|
+
};
|
|
156
194
|
// are we focused?
|
|
157
195
|
// if so, add an initial bar telling it's a collapsed one
|
|
158
196
|
// TODO clean this up
|
|
159
197
|
if (isFocused) {
|
|
160
198
|
const width = numTicks * pxPerTick;
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
+
}
|
|
167
207
|
|
|
168
208
|
// 使用 i18n 翻译的 collapsed 文本
|
|
169
209
|
const shortName = focusedNode.mapOrElse(
|
|
@@ -171,14 +211,6 @@ export default function RenderCanvas(props: CanvasRendererConfig) {
|
|
|
171
211
|
(f) => getCollapsedText(f.i - 1, messages)
|
|
172
212
|
);
|
|
173
213
|
|
|
174
|
-
// Set the font syle
|
|
175
|
-
// It's important to set the font BEFORE calculating 'characterSize'
|
|
176
|
-
// Since it will be used to calculate how many characters can fit
|
|
177
|
-
ctx.textBaseline = 'middle';
|
|
178
|
-
ctx.font =
|
|
179
|
-
'400 11.5px SFMono-Regular, Consolas, Liberation Mono, Menlo, monospace';
|
|
180
|
-
// Since this is a monospaced font any character would do
|
|
181
|
-
const characterSize = ctx.measureText('a').width;
|
|
182
214
|
const fitCalc = fitToCanvasRect({
|
|
183
215
|
mode: fitMode,
|
|
184
216
|
charSize: characterSize,
|
|
@@ -191,12 +223,20 @@ export default function RenderCanvas(props: CanvasRendererConfig) {
|
|
|
191
223
|
const y = 0;
|
|
192
224
|
const sh = BAR_HEIGHT;
|
|
193
225
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
+
}
|
|
200
240
|
}
|
|
201
241
|
|
|
202
242
|
for (let i = 0; i < levels.length - topLevel; i += 1) {
|
|
@@ -208,19 +248,11 @@ export default function RenderCanvas(props: CanvasRendererConfig) {
|
|
|
208
248
|
for (let j = 0; j < level.length; j += ff.jStep) {
|
|
209
249
|
const barIndex = ff.getBarOffset(level, j);
|
|
210
250
|
const x = tickToX(barIndex);
|
|
211
|
-
const y = i * PX_PER_LEVEL +
|
|
251
|
+
const y = i * PX_PER_LEVEL + focusOffset;
|
|
212
252
|
|
|
213
253
|
const sh = BAR_HEIGHT;
|
|
214
254
|
|
|
215
|
-
const highlightModeOn
|
|
216
|
-
!!props.highlightQuery && props.highlightQuery.length > 0;
|
|
217
|
-
|
|
218
|
-
const isHighlighted = nodeIsInQuery(
|
|
219
|
-
j + ff.jName,
|
|
220
|
-
level,
|
|
221
|
-
names,
|
|
222
|
-
props.highlightQuery
|
|
223
|
-
);
|
|
255
|
+
const isHighlighted = highlightModeOn && isHighlightedAt(level, j);
|
|
224
256
|
|
|
225
257
|
let numBarTicks = ff.getBarTotal(level, j);
|
|
226
258
|
|
|
@@ -234,14 +266,7 @@ export default function RenderCanvas(props: CanvasRendererConfig) {
|
|
|
234
266
|
ff.getBarTotal(level, j + ff.jStep) * pxPerTick <=
|
|
235
267
|
COLLAPSE_THRESHOLD &&
|
|
236
268
|
isHighlighted ===
|
|
237
|
-
((
|
|
238
|
-
nodeIsInQuery(
|
|
239
|
-
j + ff.jStep + ff.jName,
|
|
240
|
-
level,
|
|
241
|
-
names,
|
|
242
|
-
props.highlightQuery
|
|
243
|
-
)) ||
|
|
244
|
-
false)
|
|
269
|
+
(highlightModeOn && isHighlightedAt(level, j + ff.jStep))
|
|
245
270
|
) {
|
|
246
271
|
j += ff.jStep;
|
|
247
272
|
numBarTicks += ff.getBarTotal(level, j);
|
|
@@ -275,6 +300,8 @@ export default function RenderCanvas(props: CanvasRendererConfig) {
|
|
|
275
300
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
|
|
276
301
|
spyName: spyName as SpyName,
|
|
277
302
|
palette,
|
|
303
|
+
colorByNameIndex,
|
|
304
|
+
diffColorFn,
|
|
278
305
|
};
|
|
279
306
|
|
|
280
307
|
switch (format) {
|
|
@@ -294,18 +321,19 @@ export default function RenderCanvas(props: CanvasRendererConfig) {
|
|
|
294
321
|
}
|
|
295
322
|
};
|
|
296
323
|
|
|
297
|
-
const color = getColor();
|
|
298
|
-
|
|
299
324
|
ctx.beginPath();
|
|
300
325
|
ctx.rect(x, y, sw, sh);
|
|
301
|
-
|
|
302
|
-
|
|
326
|
+
if (renderRects) {
|
|
327
|
+
const color = getColor();
|
|
328
|
+
ctx.fillStyle = color.string();
|
|
329
|
+
ctx.fill();
|
|
330
|
+
}
|
|
303
331
|
|
|
304
332
|
/*******************************/
|
|
305
333
|
/* D r a w T e x t */
|
|
306
334
|
/*******************************/
|
|
307
335
|
// don't write text if there's not enough space for a single letter
|
|
308
|
-
if (collapsed) {
|
|
336
|
+
if (!renderText || collapsed) {
|
|
309
337
|
continue;
|
|
310
338
|
}
|
|
311
339
|
|
|
@@ -319,17 +347,10 @@ export default function RenderCanvas(props: CanvasRendererConfig) {
|
|
|
319
347
|
numBarTicks,
|
|
320
348
|
sampleRate,
|
|
321
349
|
formatter,
|
|
322
|
-
messages
|
|
350
|
+
messages,
|
|
351
|
+
formattedValueCache
|
|
323
352
|
);
|
|
324
353
|
|
|
325
|
-
// Set the font syle
|
|
326
|
-
// It's important to set the font BEFORE calculating 'characterSize'
|
|
327
|
-
// Since it will be used to calculate how many characters can fit
|
|
328
|
-
ctx.textBaseline = 'middle';
|
|
329
|
-
ctx.font =
|
|
330
|
-
'400 11.5px SFMono-Regular, Consolas, Liberation Mono, Menlo, monospace';
|
|
331
|
-
// Since this is a monospaced font any character would do
|
|
332
|
-
const characterSize = ctx.measureText('a').width;
|
|
333
354
|
const fitCalc = fitToCanvasRect({
|
|
334
355
|
mode: fitMode,
|
|
335
356
|
charSize: characterSize,
|
|
@@ -383,11 +404,17 @@ function getLongName(
|
|
|
383
404
|
numBarTicks: number,
|
|
384
405
|
sampleRate: number,
|
|
385
406
|
formatter: ReturnType<typeof getFormatter>,
|
|
386
|
-
messages: CanvasI18nMessages
|
|
407
|
+
messages: CanvasI18nMessages,
|
|
408
|
+
formattedValueCache?: Map<number, string>
|
|
387
409
|
) {
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
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})`;
|
|
391
418
|
|
|
392
419
|
return longName;
|
|
393
420
|
}
|
|
@@ -403,6 +430,8 @@ type getColorCfg = {
|
|
|
403
430
|
names: string[];
|
|
404
431
|
spyName: SpyName;
|
|
405
432
|
palette: FlamegraphPalette;
|
|
433
|
+
colorByNameIndex: ReturnType<typeof colorBasedOnPackageName>[] | null;
|
|
434
|
+
diffColorFn: ReturnType<typeof NewDiffColor> | null;
|
|
406
435
|
};
|
|
407
436
|
|
|
408
437
|
function getColorCommon({
|
|
@@ -450,6 +479,10 @@ function getColorSingle(cfg: getColorCfg) {
|
|
|
450
479
|
});
|
|
451
480
|
l = -1;
|
|
452
481
|
}
|
|
482
|
+
if (cfg.colorByNameIndex && l >= 0 && cfg.colorByNameIndex[l]) {
|
|
483
|
+
return cfg.colorByNameIndex[l].alpha(a);
|
|
484
|
+
}
|
|
485
|
+
|
|
453
486
|
const name = cfg.names[l] || '';
|
|
454
487
|
const packageName = getPackageNameFromStackTrace(cfg.spyName, name) || '';
|
|
455
488
|
|
|
@@ -477,28 +510,11 @@ function getColorDouble(
|
|
|
477
510
|
const leftPercent = ratioToPercent(leftRatio);
|
|
478
511
|
const rightPercent = ratioToPercent(rightRatio);
|
|
479
512
|
|
|
480
|
-
|
|
481
|
-
a
|
|
482
|
-
);
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
function nodeIsInQuery(
|
|
486
|
-
index: number,
|
|
487
|
-
level: number[],
|
|
488
|
-
names: string[],
|
|
489
|
-
query: string
|
|
490
|
-
) {
|
|
491
|
-
const l = level[index];
|
|
492
|
-
if (!l) {
|
|
493
|
-
return false;
|
|
513
|
+
if (cfg.diffColorFn) {
|
|
514
|
+
return cfg.diffColorFn(diffPercent(leftPercent, rightPercent)).alpha(a);
|
|
494
515
|
}
|
|
495
516
|
|
|
496
|
-
|
|
497
|
-
if (!l2) {
|
|
498
|
-
return false;
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
return isMatch(query, l2);
|
|
517
|
+
return colorBasedOnDiffPercent(cfg.palette, leftPercent, rightPercent).alpha(a);
|
|
502
518
|
}
|
|
503
519
|
|
|
504
520
|
function getCanvasWidth(canvas: HTMLCanvasElement) {
|
|
@@ -507,3 +523,34 @@ function getCanvasWidth(canvas: HTMLCanvasElement) {
|
|
|
507
523
|
// so we also fallback to canvas.width
|
|
508
524
|
return canvas.clientWidth || canvas.width;
|
|
509
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
|
+
}
|
|
@@ -75,8 +75,14 @@ interface FlamegraphProps {
|
|
|
75
75
|
|
|
76
76
|
export default function FlameGraphComponent(props: FlamegraphProps) {
|
|
77
77
|
const canvasRef = React.useRef<HTMLCanvasElement>(null);
|
|
78
|
+
const textCanvasRef = React.useRef<HTMLCanvasElement>(null);
|
|
78
79
|
const flamegraph = useRef<Flamegraph>();
|
|
80
|
+
const flamegraphText = useRef<Flamegraph>();
|
|
79
81
|
const i18n = useFlamegraphI18n();
|
|
82
|
+
const resizeLogRef = useRef({
|
|
83
|
+
lastWidth: 0,
|
|
84
|
+
lastHeight: 0,
|
|
85
|
+
});
|
|
80
86
|
|
|
81
87
|
// ====== 新增:提取 canvas 渲染需要的 i18n messages ======
|
|
82
88
|
const canvasMessages = useMemo(
|
|
@@ -117,13 +123,26 @@ export default function FlameGraphComponent(props: FlamegraphProps) {
|
|
|
117
123
|
|
|
118
124
|
const debouncedRenderCanvas = useCallback(
|
|
119
125
|
debounce(() => {
|
|
120
|
-
|
|
126
|
+
renderRectCanvas();
|
|
127
|
+
renderTextCanvas();
|
|
121
128
|
}, 50),
|
|
122
129
|
[]
|
|
123
130
|
);
|
|
124
131
|
|
|
125
132
|
useResizeObserver(canvasRef, () => {
|
|
126
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
|
+
}
|
|
127
146
|
debouncedRenderCanvas();
|
|
128
147
|
}
|
|
129
148
|
});
|
|
@@ -323,32 +342,58 @@ export default function FlameGraphComponent(props: FlamegraphProps) {
|
|
|
323
342
|
|
|
324
343
|
flamegraph.current = f;
|
|
325
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
|
+
}
|
|
326
358
|
};
|
|
327
359
|
|
|
328
360
|
// ====== 修改:添加 canvasMessages 依赖 ======
|
|
329
361
|
React.useEffect(() => {
|
|
330
362
|
constructCanvas();
|
|
331
|
-
|
|
363
|
+
renderRectCanvas();
|
|
364
|
+
renderTextCanvas();
|
|
332
365
|
}, [palette, canvasMessages]);
|
|
333
366
|
|
|
334
367
|
React.useEffect(() => {
|
|
335
368
|
constructCanvas();
|
|
336
|
-
|
|
369
|
+
renderRectCanvas();
|
|
370
|
+
renderTextCanvas();
|
|
337
371
|
}, [
|
|
338
372
|
canvasRef.current,
|
|
339
373
|
flamebearer,
|
|
340
374
|
focusedNode,
|
|
341
375
|
fitMode,
|
|
342
|
-
highlightQuery,
|
|
343
376
|
zoom,
|
|
344
377
|
]);
|
|
345
378
|
|
|
346
|
-
|
|
379
|
+
React.useEffect(() => {
|
|
380
|
+
constructCanvas();
|
|
381
|
+
renderRectCanvas();
|
|
382
|
+
renderTextCanvas();
|
|
383
|
+
}, [highlightQuery]);
|
|
384
|
+
|
|
385
|
+
const renderRectCanvas = () => {
|
|
347
386
|
canvasRef?.current?.setAttribute('data-state', 'rendering');
|
|
348
|
-
flamegraph?.current?.render();
|
|
387
|
+
flamegraph?.current?.render({ renderText: false });
|
|
349
388
|
canvasRef?.current?.setAttribute('data-state', 'rendered');
|
|
350
389
|
};
|
|
351
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
|
+
|
|
352
397
|
const dataUnavailable =
|
|
353
398
|
!flamebearer || (flamebearer && flamebearer.names.length <= 1);
|
|
354
399
|
|
|
@@ -400,6 +445,7 @@ export default function FlameGraphComponent(props: FlamegraphProps) {
|
|
|
400
445
|
<div
|
|
401
446
|
data-testid={dataTestId}
|
|
402
447
|
style={{
|
|
448
|
+
position: 'relative',
|
|
403
449
|
opacity: dataUnavailable && !showSingleLevel ? 0 : 1,
|
|
404
450
|
}}
|
|
405
451
|
>
|
|
@@ -411,22 +457,28 @@ export default function FlameGraphComponent(props: FlamegraphProps) {
|
|
|
411
457
|
ref={canvasRef}
|
|
412
458
|
onClick={!disableClick ? onClick : undefined}
|
|
413
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
|
+
)}
|
|
414
480
|
</div>
|
|
415
481
|
{showCredit ? <LogoLink /> : ''}
|
|
416
|
-
{flamegraph && canvasRef && (
|
|
417
|
-
<Highlight
|
|
418
|
-
barHeight={PX_PER_LEVEL}
|
|
419
|
-
canvasRef={canvasRef}
|
|
420
|
-
zoom={zoom}
|
|
421
|
-
xyToHighlightData={xyToHighlightData}
|
|
422
|
-
/>
|
|
423
|
-
)}
|
|
424
|
-
{flamegraph && (
|
|
425
|
-
<ContextMenuHighlight
|
|
426
|
-
barHeight={PX_PER_LEVEL}
|
|
427
|
-
node={rightClickedNode}
|
|
428
|
-
/>
|
|
429
|
-
)}
|
|
430
482
|
{flamegraph && (
|
|
431
483
|
<FlamegraphTooltip
|
|
432
484
|
format={flamebearer.format}
|
|
@@ -147,24 +147,28 @@ class FlameGraphRenderer extends Component<
|
|
|
147
147
|
prevProps: FlamegraphRendererProps,
|
|
148
148
|
prevState: FlamegraphRendererState
|
|
149
149
|
) {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
+
}
|
|
168
172
|
}
|
|
169
173
|
|
|
170
174
|
// flamegraph configs changed
|
|
@@ -614,6 +618,9 @@ class FlameGraphRenderer extends Component<
|
|
|
614
618
|
);
|
|
615
619
|
|
|
616
620
|
const sandwichPane = (() => {
|
|
621
|
+
if (this.state.view !== 'sandwich') {
|
|
622
|
+
return <div className={styles.sandwichPane} key="sandwich-pane" />;
|
|
623
|
+
}
|
|
617
624
|
if (this.state.selectedItem.isNothing) {
|
|
618
625
|
return (
|
|
619
626
|
<div className={styles.sandwichPane} key="sandwich-pane">
|