@kylincloud/flamegraph 0.35.27 → 0.35.29

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 (65) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/dist/FlameGraph/FlameGraphComponent/DiffLegend.d.ts.map +1 -1
  3. package/dist/FlameGraph/FlameGraphComponent/DiffLegendPaletteDropdown.d.ts.map +1 -1
  4. package/dist/FlameGraph/FlameGraphComponent/Flamegraph.d.ts +16 -2
  5. package/dist/FlameGraph/FlameGraphComponent/Flamegraph.d.ts.map +1 -1
  6. package/dist/FlameGraph/FlameGraphComponent/Flamegraph_render.d.ts +15 -2
  7. package/dist/FlameGraph/FlameGraphComponent/Flamegraph_render.d.ts.map +1 -1
  8. package/dist/FlameGraph/FlameGraphComponent/Highlight.d.ts.map +1 -1
  9. package/dist/FlameGraph/FlameGraphComponent/index.d.ts.map +1 -1
  10. package/dist/FlameGraph/normalize.d.ts.map +1 -1
  11. package/dist/FlameGraph/uniqueness.d.ts.map +1 -1
  12. package/dist/Icons.d.ts.map +1 -1
  13. package/dist/ProfilerTable.d.ts.map +1 -1
  14. package/dist/SharedQueryInput.d.ts.map +1 -1
  15. package/dist/Toolbar.d.ts.map +1 -1
  16. package/dist/Tooltip/Tooltip.d.ts.map +1 -1
  17. package/dist/flamegraphRenderWorker.js +2 -0
  18. package/dist/flamegraphRenderWorker.js.map +1 -0
  19. package/dist/index.cjs.js +4 -4
  20. package/dist/index.cjs.js.map +1 -1
  21. package/dist/index.esm.js +4 -4
  22. package/dist/index.esm.js.map +1 -1
  23. package/dist/index.node.cjs.js +4 -4
  24. package/dist/index.node.cjs.js.map +1 -1
  25. package/dist/index.node.esm.js +4 -4
  26. package/dist/index.node.esm.js.map +1 -1
  27. package/dist/shims/Table.d.ts +15 -1
  28. package/dist/shims/Table.d.ts.map +1 -1
  29. package/dist/shims/Tooltip.d.ts.map +1 -1
  30. package/dist/workers/createFlamegraphRenderWorker.d.ts +2 -0
  31. package/dist/workers/createFlamegraphRenderWorker.d.ts.map +1 -0
  32. package/dist/workers/flamegraphRenderWorker.d.ts +2 -0
  33. package/dist/workers/flamegraphRenderWorker.d.ts.map +1 -0
  34. package/dist/workers/profilerTableWorker.d.ts +73 -0
  35. package/dist/workers/profilerTableWorker.d.ts.map +1 -0
  36. package/package.json +1 -1
  37. package/src/FlameGraph/FlameGraphComponent/DiffLegend.module.css +8 -2
  38. package/src/FlameGraph/FlameGraphComponent/DiffLegend.tsx +12 -1
  39. package/src/FlameGraph/FlameGraphComponent/DiffLegendPaletteDropdown.module.css +93 -10
  40. package/src/FlameGraph/FlameGraphComponent/DiffLegendPaletteDropdown.tsx +9 -4
  41. package/src/FlameGraph/FlameGraphComponent/Flamegraph.ts +33 -8
  42. package/src/FlameGraph/FlameGraphComponent/Flamegraph_render.ts +289 -85
  43. package/src/FlameGraph/FlameGraphComponent/Highlight.tsx +43 -17
  44. package/src/FlameGraph/FlameGraphComponent/index.tsx +208 -57
  45. package/src/FlameGraph/FlameGraphComponent/styles.module.scss +8 -0
  46. package/src/FlameGraph/normalize.ts +9 -7
  47. package/src/FlameGraph/uniqueness.ts +69 -59
  48. package/src/Icons.tsx +18 -9
  49. package/src/ProfilerTable.tsx +463 -33
  50. package/src/SharedQueryInput.module.scss +50 -0
  51. package/src/SharedQueryInput.tsx +18 -3
  52. package/src/Toolbar.module.scss +90 -0
  53. package/src/Toolbar.tsx +30 -16
  54. package/src/Tooltip/Tooltip.tsx +49 -16
  55. package/src/i18n.tsx +1 -1
  56. package/src/sass/_common.scss +22 -3
  57. package/src/sass/_css-variables.scss +5 -1
  58. package/src/sass/flamegraph.scss +26 -23
  59. package/src/shims/Table.module.scss +91 -13
  60. package/src/shims/Table.tsx +202 -7
  61. package/src/shims/Tooltip.module.scss +40 -0
  62. package/src/shims/Tooltip.tsx +31 -3
  63. package/src/workers/createFlamegraphRenderWorker.ts +7 -0
  64. package/src/workers/flamegraphRenderWorker.ts +198 -0
  65. package/src/workers/profilerTableWorker.ts +368 -0
@@ -57,6 +57,116 @@ export interface CanvasI18nMessages {
57
57
  isZh?: boolean;
58
58
  }
59
59
 
60
+ const formattedValueCacheByCanvas = new WeakMap<
61
+ HTMLCanvasElement | OffscreenCanvas,
62
+ Map<number, string>
63
+ >();
64
+ const highlightCacheByNames = new WeakMap<
65
+ string[],
66
+ Map<string, Uint8Array>
67
+ >();
68
+ const colorCacheByNames = new WeakMap<
69
+ string[],
70
+ Map<string, ReturnType<typeof colorBasedOnPackageName>[]>
71
+ >();
72
+ const barIndexByLevels = new WeakMap<
73
+ Flamebearer['levels'],
74
+ Map<number, Array<{ i: number; j: number }>>
75
+ >();
76
+
77
+ function getFormattedValueCache(canvas: HTMLCanvasElement | OffscreenCanvas) {
78
+ const cached = formattedValueCacheByCanvas.get(canvas);
79
+ if (cached) {
80
+ return cached;
81
+ }
82
+ const next = new Map<number, string>();
83
+ formattedValueCacheByCanvas.set(canvas, next);
84
+ return next;
85
+ }
86
+
87
+ function getHighlightByNameIndex(names: string[], query: string) {
88
+ const cachedByQuery = highlightCacheByNames.get(names);
89
+ if (cachedByQuery) {
90
+ const cached = cachedByQuery.get(query);
91
+ if (cached) {
92
+ return cached;
93
+ }
94
+ }
95
+ const queryLower = query.toLowerCase();
96
+ const matches = new Uint8Array(names.length);
97
+ for (let i = 1; i < names.length; i += 1) {
98
+ const name = names[i];
99
+ if (!name) {
100
+ continue;
101
+ }
102
+ if (name.toLowerCase().includes(queryLower)) {
103
+ matches[i] = 1;
104
+ }
105
+ }
106
+ const nextMap = cachedByQuery ?? new Map<string, Uint8Array>();
107
+ nextMap.set(query, matches);
108
+ highlightCacheByNames.set(names, nextMap);
109
+ return matches;
110
+ }
111
+
112
+ function getColorByNameIndexCached(
113
+ names: string[],
114
+ spyName: SpyName,
115
+ palette: FlamegraphPalette
116
+ ) {
117
+ const key = `${spyName}|${palette.name}`;
118
+ const cachedByPalette = colorCacheByNames.get(names);
119
+ if (cachedByPalette) {
120
+ const cached = cachedByPalette.get(key);
121
+ if (cached) {
122
+ return cached;
123
+ }
124
+ }
125
+ const colors = new Array<ReturnType<typeof colorBasedOnPackageName>>(
126
+ names.length
127
+ );
128
+ for (let i = 0; i < names.length; i += 1) {
129
+ const name = names[i] || '';
130
+ const packageName = getPackageNameFromStackTrace(spyName, name) || '';
131
+ colors[i] = colorBasedOnPackageName(palette, packageName);
132
+ }
133
+ const nextMap =
134
+ cachedByPalette ??
135
+ new Map<string, ReturnType<typeof colorBasedOnPackageName>[]>();
136
+ nextMap.set(key, colors);
137
+ colorCacheByNames.set(names, nextMap);
138
+ return colors;
139
+ }
140
+
141
+ function getBarIndex(
142
+ levels: Flamebearer['levels'],
143
+ format: Flamebearer['format']
144
+ ) {
145
+ const cached = barIndexByLevels.get(levels);
146
+ if (cached) {
147
+ return cached;
148
+ }
149
+ const ff = createFF(format);
150
+ const index = new Map<number, Array<{ i: number; j: number }>>();
151
+ for (let i = 0; i < levels.length; i += 1) {
152
+ const level = levels[i];
153
+ for (let j = 0; j < level.length; j += ff.jStep) {
154
+ const nameIndex = ff.getBarName(level, j);
155
+ if (nameIndex === undefined || nameIndex < 0) {
156
+ continue;
157
+ }
158
+ const list = index.get(nameIndex);
159
+ if (list) {
160
+ list.push({ i, j });
161
+ } else {
162
+ index.set(nameIndex, [{ i, j }]);
163
+ }
164
+ }
165
+ }
166
+ barIndexByLevels.set(levels, index);
167
+ return index;
168
+ }
169
+
60
170
  /** Default English messages (fallback) */
61
171
  const defaultCanvasMessages: CanvasI18nMessages = {
62
172
  collapsedLevelsSingular: 'total (1 level collapsed)',
@@ -65,13 +175,22 @@ const defaultCanvasMessages: CanvasI18nMessages = {
65
175
  };
66
176
 
67
177
  type CanvasRendererConfig = Flamebearer & {
68
- canvas: HTMLCanvasElement;
178
+ canvas: HTMLCanvasElement | OffscreenCanvas;
69
179
  focusedNode: ConstructorParameters<typeof Flamegraph>[2];
70
180
  fitMode: ConstructorParameters<typeof Flamegraph>[3];
71
181
  highlightQuery: ConstructorParameters<typeof Flamegraph>[4];
72
182
  zoom: ConstructorParameters<typeof Flamegraph>[5];
73
183
  renderRects?: boolean;
74
184
  renderText?: boolean;
185
+ renderMode?: 'normal' | 'highlightOnly' | 'forceGrey';
186
+ levelStart?: number;
187
+ levelEnd?: number;
188
+ startI?: number;
189
+ startJ?: number;
190
+ timeBudgetMs?: number;
191
+ skipCanvasResize?: boolean;
192
+ skipDprScale?: boolean;
193
+ devicePixelRatio?: number;
75
194
 
76
195
  /**
77
196
  * Used when zooming, values between 0 and 1.
@@ -108,18 +227,25 @@ function getCollapsedText(
108
227
  return messages.collapsedLevelsPlural.replace('{n}', String(levelCount));
109
228
  }
110
229
 
111
- export default function RenderCanvas(props: CanvasRendererConfig) {
230
+ export default function RenderCanvas(
231
+ props: CanvasRendererConfig
232
+ ): { done: boolean; nextI: number; nextJ: number } {
233
+ const renderStart = performance.now();
112
234
  const renderRects = props.renderRects !== false;
113
235
  const renderText = props.renderText !== false;
236
+ const renderMode = props.renderMode ?? 'normal';
237
+ const timeBudgetMs = props.timeBudgetMs;
114
238
  const { canvas, fitMode, units, tickToX, levels, palette } = props;
115
239
  const { numTicks, sampleRate, pxPerTick } = props;
116
240
  const { rangeMin, rangeMax } = props;
117
241
  const { focusedNode, zoom } = props;
118
242
  const messages = props.messages || defaultCanvasMessages;
119
243
 
120
- const graphWidth = getCanvasWidth(canvas);
244
+ const graphWidth = props.skipCanvasResize ? canvas.width : getCanvasWidth(canvas);
121
245
  // TODO: why is this needed? otherwise height is all messed up
122
- canvas.width = graphWidth;
246
+ if (!props.skipCanvasResize) {
247
+ canvas.width = graphWidth;
248
+ }
123
249
 
124
250
  if (rangeMin >= rangeMax) {
125
251
  throw new Error(`'rangeMin' should be strictly smaller than 'rangeMax'`);
@@ -151,13 +277,21 @@ export default function RenderCanvas(props: CanvasRendererConfig) {
151
277
  const canvasHeight =
152
278
  PX_PER_LEVEL * (levels.length - topLevel) + (isFocused ? BAR_HEIGHT : 0);
153
279
  // const canvasHeight = PX_PER_LEVEL * (levels.length - topLevel);
154
- canvas.height = canvasHeight;
280
+ if (!props.skipCanvasResize) {
281
+ canvas.height = canvasHeight;
282
+ }
155
283
 
156
284
  // increase pixel ratio, otherwise it looks bad in high resolution devices
157
- if (devicePixelRatio > 1) {
158
- canvas.width *= 2;
159
- canvas.height *= 2;
160
- ctx.scale(2, 2);
285
+ const dpr =
286
+ typeof props.devicePixelRatio === 'number'
287
+ ? props.devicePixelRatio
288
+ : typeof devicePixelRatio !== 'undefined'
289
+ ? devicePixelRatio
290
+ : 1;
291
+ if (!props.skipDprScale && dpr > 1) {
292
+ canvas.width *= dpr;
293
+ canvas.height *= dpr;
294
+ ctx.scale(dpr, dpr);
161
295
  }
162
296
 
163
297
  ctx.textBaseline = 'middle';
@@ -167,13 +301,13 @@ export default function RenderCanvas(props: CanvasRendererConfig) {
167
301
  const characterSize = ctx.measureText('a').width;
168
302
 
169
303
  const { names } = props;
170
- const formattedValueCache = new Map<number, string>();
304
+ const formattedValueCache = getFormattedValueCache(canvas);
171
305
  const highlightByNameIndex = highlightModeOn
172
- ? buildHighlightByNameIndex(names, props.highlightQuery)
306
+ ? getHighlightByNameIndex(names, props.highlightQuery)
173
307
  : null;
174
308
  const colorByNameIndex =
175
309
  props.format === 'single'
176
- ? buildColorByNameIndex(names, props.spyName as SpyName, palette)
310
+ ? getColorByNameIndexCached(names, props.spyName as SpyName, palette)
177
311
  : null;
178
312
  const diffColorFn =
179
313
  props.format === 'double'
@@ -194,7 +328,15 @@ export default function RenderCanvas(props: CanvasRendererConfig) {
194
328
  // are we focused?
195
329
  // if so, add an initial bar telling it's a collapsed one
196
330
  // TODO clean this up
197
- if (isFocused) {
331
+ const initialStartI = props.startI ?? 0;
332
+ const initialStartJ = props.startJ ?? 0;
333
+ if (
334
+ isFocused &&
335
+ renderMode !== 'highlightOnly' &&
336
+ (props.levelStart ?? 0) === 0 &&
337
+ initialStartI === 0 &&
338
+ initialStartJ === 0
339
+ ) {
198
340
  const width = numTicks * pxPerTick;
199
341
  if (renderRects) {
200
342
  ctx.beginPath();
@@ -239,13 +381,120 @@ export default function RenderCanvas(props: CanvasRendererConfig) {
239
381
  }
240
382
  }
241
383
 
242
- for (let i = 0; i < levels.length - topLevel; i += 1) {
384
+ const levelStart = Math.max(0, props.levelStart ?? 0);
385
+ const maxLevels = levels.length - topLevel;
386
+ const levelEnd = Math.min(maxLevels, props.levelEnd ?? maxLevels);
387
+ const renderStartI = Math.max(levelStart, props.startI ?? levelStart);
388
+ const renderStartJ = Math.max(0, props.startJ ?? 0);
389
+
390
+ const { spyName } = props;
391
+ const getColorForBar = (
392
+ level: number[],
393
+ j: number,
394
+ i: number,
395
+ collapsed: boolean,
396
+ isHighlighted: boolean
397
+ ) => {
398
+ const common = {
399
+ level,
400
+ j,
401
+ // discount for the levels we skipped
402
+ // otherwise it will dim out all nodes
403
+ i:
404
+ i +
405
+ focusedNode.mapOrElse(
406
+ () => 0,
407
+ (f) => f.i
408
+ ),
409
+ names,
410
+ collapsed,
411
+ selectedLevel,
412
+ highlightModeOn,
413
+ isHighlighted,
414
+ // keep type narrow https://stackoverflow.com/q/54333982
415
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
416
+ spyName: spyName as SpyName,
417
+ palette,
418
+ colorByNameIndex,
419
+ diffColorFn,
420
+ };
421
+
422
+ switch (format) {
423
+ case 'single': {
424
+ return getColorSingle({ ...common });
425
+ }
426
+ case 'double': {
427
+ return getColorDouble({
428
+ ...common,
429
+ leftTicks: props.leftTicks,
430
+ rightTicks: props.rightTicks,
431
+ });
432
+ }
433
+ default: {
434
+ throw new Error(`Unsupported format: ${format}`);
435
+ }
436
+ }
437
+ };
438
+
439
+ if (renderMode === 'highlightOnly' && highlightModeOn && renderRects) {
440
+ const barIndex = getBarIndex(levels, format);
441
+ const highlightNameIndices: number[] = [];
442
+ for (let i = 1; i < names.length; i += 1) {
443
+ if (highlightByNameIndex && highlightByNameIndex[i] === 1) {
444
+ highlightNameIndices.push(i);
445
+ }
446
+ }
447
+ for (let i = 0; i < highlightNameIndices.length; i += 1) {
448
+ const nameIndex = highlightNameIndices[i];
449
+ const entries = barIndex.get(nameIndex);
450
+ if (!entries) {
451
+ continue;
452
+ }
453
+ for (let k = 0; k < entries.length; k += 1) {
454
+ const entry = entries[k];
455
+ const relativeLevel = entry.i - topLevel;
456
+ if (
457
+ entry.i < topLevel ||
458
+ relativeLevel < levelStart ||
459
+ relativeLevel >= levelEnd
460
+ ) {
461
+ continue;
462
+ }
463
+ const level = levels[entry.i];
464
+ const barIndexValue = ff.getBarOffset(level, entry.j);
465
+ const x = tickToX(barIndexValue);
466
+ const y = relativeLevel * PX_PER_LEVEL + focusOffset;
467
+ const sh = BAR_HEIGHT;
468
+ const numBarTicks = ff.getBarTotal(level, entry.j);
469
+ const collapsed = numBarTicks * pxPerTick <= COLLAPSE_THRESHOLD;
470
+ const sw = numBarTicks * pxPerTick - (collapsed ? 0 : GAP);
471
+ if (sw <= 0) {
472
+ continue;
473
+ }
474
+ ctx.beginPath();
475
+ ctx.rect(x, y, sw, sh);
476
+ const color = getColorForBar(
477
+ level,
478
+ entry.j,
479
+ relativeLevel,
480
+ collapsed,
481
+ true
482
+ );
483
+ ctx.fillStyle = color.string();
484
+ ctx.fill();
485
+ }
486
+ }
487
+ return { done: true, nextI: levelEnd, nextJ: 0 };
488
+ }
489
+
490
+ for (let i = renderStartI; i < levelEnd; i += 1) {
243
491
  const level = levels[topLevel + i];
244
492
  if (!level) {
245
493
  throw new Error(`Could not find level: ${topLevel + i}`);
246
494
  }
247
495
 
248
- for (let j = 0; j < level.length; j += ff.jStep) {
496
+ let j = i === renderStartI ? renderStartJ : 0;
497
+ for (; j < level.length; j += ff.jStep) {
249
498
  const barIndex = ff.getBarOffset(level, j);
250
499
  const x = tickToX(barIndex);
251
500
  const y = i * PX_PER_LEVEL + focusOffset;
@@ -277,55 +526,15 @@ export default function RenderCanvas(props: CanvasRendererConfig) {
277
526
  /*******************************/
278
527
  /* D r a w R e c t */
279
528
  /*******************************/
280
- const { spyName } = props;
281
-
282
- const getColor = () => {
283
- const common = {
284
- level,
285
- j,
286
- // discount for the levels we skipped
287
- // otherwise it will dim out all nodes
288
- i:
289
- i +
290
- focusedNode.mapOrElse(
291
- () => 0,
292
- (f) => f.i
293
- ),
294
- names,
295
- collapsed,
296
- selectedLevel,
297
- highlightModeOn,
298
- isHighlighted,
299
- // keep type narrow https://stackoverflow.com/q/54333982
300
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
301
- spyName: spyName as SpyName,
302
- palette,
303
- colorByNameIndex,
304
- diffColorFn,
305
- };
306
-
307
- switch (format) {
308
- case 'single': {
309
- return getColorSingle({ ...common });
310
- }
311
- case 'double': {
312
- return getColorDouble({
313
- ...common,
314
- leftTicks: props.leftTicks,
315
- rightTicks: props.rightTicks,
316
- });
317
- }
318
- default: {
319
- throw new Error(`Unsupported format: ${format}`);
320
- }
321
- }
322
- };
323
-
324
529
  ctx.beginPath();
325
530
  ctx.rect(x, y, sw, sh);
326
531
  if (renderRects) {
327
- const color = getColor();
328
- ctx.fillStyle = color.string();
532
+ if (renderMode === 'forceGrey') {
533
+ ctx.fillStyle = colorGreyscale(200, 0.66).rgb().string();
534
+ } else {
535
+ const color = getColorForBar(level, j, i, collapsed, isHighlighted);
536
+ ctx.fillStyle = color.string();
537
+ }
329
538
  ctx.fill();
330
539
  }
331
540
 
@@ -333,7 +542,10 @@ export default function RenderCanvas(props: CanvasRendererConfig) {
333
542
  /* D r a w T e x t */
334
543
  /*******************************/
335
544
  // don't write text if there's not enough space for a single letter
336
- if (!renderText || collapsed) {
545
+ if (!renderText || collapsed || renderMode === 'forceGrey') {
546
+ if (timeBudgetMs && performance.now() - renderStart > timeBudgetMs) {
547
+ return { done: false, nextI: i, nextJ: j + ff.jStep };
548
+ }
337
549
  continue;
338
550
  }
339
551
 
@@ -365,8 +577,16 @@ export default function RenderCanvas(props: CanvasRendererConfig) {
365
577
  const namePosX = Math.round(Math.max(x, 0));
366
578
  ctx.fillText(fitCalc.text, namePosX + fitCalc.marginLeft, y + sh / 2 + 1);
367
579
  ctx.restore();
580
+
581
+ if (timeBudgetMs && performance.now() - renderStart > timeBudgetMs) {
582
+ return { done: false, nextI: i, nextJ: j + ff.jStep };
583
+ }
584
+ }
585
+ if (timeBudgetMs && performance.now() - renderStart > timeBudgetMs) {
586
+ return { done: false, nextI: i + 1, nextJ: 0 };
368
587
  }
369
588
  }
589
+ return { done: true, nextI: levelEnd, nextJ: 0 };
370
590
  }
371
591
 
372
592
  function getFunctionName(
@@ -517,26 +737,18 @@ function getColorDouble(
517
737
  return colorBasedOnDiffPercent(cfg.palette, leftPercent, rightPercent).alpha(a);
518
738
  }
519
739
 
520
- function getCanvasWidth(canvas: HTMLCanvasElement) {
740
+ function getCanvasWidth(canvas: HTMLCanvasElement | OffscreenCanvas) {
521
741
  // clientWidth includes padding
522
742
  // however it's not present in node-canvas (used for testing)
523
743
  // so we also fallback to canvas.width
524
- return canvas.clientWidth || canvas.width;
744
+ if ('clientWidth' in canvas && typeof canvas.clientWidth === 'number') {
745
+ return canvas.clientWidth;
746
+ }
747
+ return canvas.width;
525
748
  }
526
749
 
527
750
  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;
751
+ return getHighlightByNameIndex(names, query);
540
752
  }
541
753
 
542
754
  function buildColorByNameIndex(
@@ -544,13 +756,5 @@ function buildColorByNameIndex(
544
756
  spyName: SpyName,
545
757
  palette: FlamegraphPalette
546
758
  ) {
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;
759
+ return getColorByNameIndexCached(names, spyName, palette);
556
760
  }
@@ -24,6 +24,9 @@ export default function Highlight(props: HighlightProps) {
24
24
  height: '0px',
25
25
  visibility: 'hidden',
26
26
  });
27
+ const highlightRef = React.useRef<HTMLDivElement>(null);
28
+ const rafRef = React.useRef<number | null>(null);
29
+ const lastStyleRef = React.useRef<React.CSSProperties | null>(null);
27
30
 
28
31
  React.useEffect(() => {
29
32
  // stops highlighting every time a node is zoomed or unzoomed
@@ -35,27 +38,46 @@ export default function Highlight(props: HighlightProps) {
35
38
  });
36
39
  }, [zoom]);
37
40
 
38
- const onMouseMove = (e: MouseEvent) => {
39
- const opt = xyToHighlightData(e.offsetX, e.offsetY);
40
-
41
- if (opt.isJust) {
42
- const data = opt.value;
43
-
44
- setStyle({
45
- visibility: 'visible',
46
- height: `${barHeight}px`,
47
- ...data,
48
- });
49
- } else {
50
- // it doesn't map to a valid xy
51
- // so it means we are hovering out
52
- onMouseOut();
41
+ const applyStyle = (next: React.CSSProperties) => {
42
+ const node = highlightRef.current;
43
+ if (!node) {
44
+ setStyle(next);
45
+ return;
53
46
  }
47
+ const prev = lastStyleRef.current;
48
+ if (prev && prev.left === next.left && prev.top === next.top && prev.width === next.width && prev.visibility === next.visibility) {
49
+ return;
50
+ }
51
+ lastStyleRef.current = next;
52
+ node.style.visibility = String(next.visibility || '');
53
+ if (next.left !== undefined) node.style.left = String(next.left);
54
+ if (next.top !== undefined) node.style.top = String(next.top);
55
+ if (next.width !== undefined) node.style.width = String(next.width);
56
+ if (next.height !== undefined) node.style.height = String(next.height);
57
+ };
58
+
59
+ const onMouseMove = (e: MouseEvent) => {
60
+ if (rafRef.current) cancelAnimationFrame(rafRef.current);
61
+ const x = e.offsetX;
62
+ const y = e.offsetY;
63
+ rafRef.current = requestAnimationFrame(() => {
64
+ const opt = xyToHighlightData(x, y);
65
+ if (opt.isJust) {
66
+ const data = opt.value;
67
+ applyStyle({
68
+ visibility: 'visible',
69
+ height: `${barHeight}px`,
70
+ ...data,
71
+ });
72
+ } else {
73
+ onMouseOut();
74
+ }
75
+ });
54
76
  };
55
77
 
56
78
  const onMouseOut = () => {
57
- setStyle({
58
- ...style,
79
+ applyStyle({
80
+ ...(lastStyleRef.current || {}),
59
81
  visibility: 'hidden',
60
82
  });
61
83
  };
@@ -77,6 +99,9 @@ export default function Highlight(props: HighlightProps) {
77
99
  return () => {
78
100
  canvasEl.removeEventListener('mousemove', onMouseMove);
79
101
  canvasEl.removeEventListener('mouseout', onMouseOut);
102
+ if (rafRef.current) {
103
+ cancelAnimationFrame(rafRef.current);
104
+ }
80
105
  };
81
106
  },
82
107
 
@@ -86,6 +111,7 @@ export default function Highlight(props: HighlightProps) {
86
111
 
87
112
  return (
88
113
  <div
114
+ ref={highlightRef}
89
115
  className={styles.highlight}
90
116
  style={style}
91
117
  data-testid="flamegraph-highlight"