@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kylincloud/flamegraph",
3
- "version": "0.35.24",
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
 
@@ -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
- ctx.beginPath();
162
- ctx.rect(0, 0, numTicks * pxPerTick, BAR_HEIGHT);
163
- // TODO find a neutral color
164
- // TODO use getColor ?
165
- ctx.fillStyle = colorGreyscale(200, 1).rgb().string();
166
- 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
+ }
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
- ctx.save();
195
- ctx.clip();
196
- ctx.fillStyle = 'black';
197
- const namePosX = Math.round(Math.max(x, 0));
198
- ctx.fillText(fitCalc.text, namePosX + fitCalc.marginLeft, y + sh / 2 + 1);
199
- 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
+ }
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 + (isFocused ? BAR_HEIGHT : 0);
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
- ((props.highlightQuery &&
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
- ctx.fillStyle = color.string();
302
- ctx.fill();
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
- const formatted = formatter.format(numBarTicks, sampleRate);
389
- const localized = localizeDurationString(formatted, !!messages.isZh);
390
- const longName = `${shortName} (${localized || formatted})`;
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
- return colorBasedOnDiffPercent(cfg.palette, leftPercent, rightPercent).alpha(
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
- const l2 = names[l];
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
+ }
@@ -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
+ }
@@ -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
- renderCanvas();
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
- renderCanvas();
363
+ renderRectCanvas();
364
+ renderTextCanvas();
332
365
  }, [palette, canvasMessages]);
333
366
 
334
367
  React.useEffect(() => {
335
368
  constructCanvas();
336
- renderCanvas();
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
- const renderCanvas = () => {
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
- // TODO: this is a slow operation
151
- const prevFlame = normalize(prevProps);
152
- const currFlame = normalize(this.props);
153
-
154
- if (!this.isSameFlamebearer(prevFlame, currFlame)) {
155
- const newConfigs = this.calcNewConfigs(prevFlame, currFlame);
156
-
157
- // Batch these updates to not do unnecessary work
158
- // eslint-disable-next-line react/no-did-update-set-state
159
- this.setState({
160
- flamebearer: currFlame,
161
- flamegraphConfigs: {
162
- ...this.state.flamegraphConfigs,
163
- ...newConfigs,
164
- },
165
- selectedItem: Maybe.nothing(),
166
- });
167
- return;
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">