@kylincloud/flamegraph 0.35.28 → 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 (43) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/FlameGraph/FlameGraphComponent/Flamegraph.d.ts +16 -2
  3. package/dist/FlameGraph/FlameGraphComponent/Flamegraph.d.ts.map +1 -1
  4. package/dist/FlameGraph/FlameGraphComponent/Flamegraph_render.d.ts +15 -2
  5. package/dist/FlameGraph/FlameGraphComponent/Flamegraph_render.d.ts.map +1 -1
  6. package/dist/FlameGraph/FlameGraphComponent/Highlight.d.ts.map +1 -1
  7. package/dist/FlameGraph/FlameGraphComponent/index.d.ts.map +1 -1
  8. package/dist/FlameGraph/normalize.d.ts.map +1 -1
  9. package/dist/FlameGraph/uniqueness.d.ts.map +1 -1
  10. package/dist/ProfilerTable.d.ts.map +1 -1
  11. package/dist/Tooltip/Tooltip.d.ts.map +1 -1
  12. package/dist/flamegraphRenderWorker.js +2 -0
  13. package/dist/flamegraphRenderWorker.js.map +1 -0
  14. package/dist/index.cjs.js +4 -4
  15. package/dist/index.cjs.js.map +1 -1
  16. package/dist/index.esm.js +4 -4
  17. package/dist/index.esm.js.map +1 -1
  18. package/dist/index.node.cjs.js +4 -4
  19. package/dist/index.node.cjs.js.map +1 -1
  20. package/dist/index.node.esm.js +4 -4
  21. package/dist/index.node.esm.js.map +1 -1
  22. package/dist/shims/Table.d.ts +15 -1
  23. package/dist/shims/Table.d.ts.map +1 -1
  24. package/dist/workers/createFlamegraphRenderWorker.d.ts +2 -0
  25. package/dist/workers/createFlamegraphRenderWorker.d.ts.map +1 -0
  26. package/dist/workers/flamegraphRenderWorker.d.ts +2 -0
  27. package/dist/workers/flamegraphRenderWorker.d.ts.map +1 -0
  28. package/dist/workers/profilerTableWorker.d.ts +73 -0
  29. package/dist/workers/profilerTableWorker.d.ts.map +1 -0
  30. package/package.json +1 -1
  31. package/src/FlameGraph/FlameGraphComponent/Flamegraph.ts +33 -8
  32. package/src/FlameGraph/FlameGraphComponent/Flamegraph_render.ts +289 -85
  33. package/src/FlameGraph/FlameGraphComponent/Highlight.tsx +43 -17
  34. package/src/FlameGraph/FlameGraphComponent/index.tsx +150 -1
  35. package/src/FlameGraph/normalize.ts +9 -7
  36. package/src/FlameGraph/uniqueness.ts +69 -59
  37. package/src/ProfilerTable.tsx +463 -33
  38. package/src/Tooltip/Tooltip.tsx +49 -16
  39. package/src/shims/Table.module.scss +5 -0
  40. package/src/shims/Table.tsx +195 -5
  41. package/src/workers/createFlamegraphRenderWorker.ts +7 -0
  42. package/src/workers/flamegraphRenderWorker.ts +198 -0
  43. package/src/workers/profilerTableWorker.ts +368 -0
@@ -73,6 +73,11 @@
73
73
  background: var(--ps-ui-element-bg-highlight);
74
74
  }
75
75
  }
76
+
77
+ &.virtualSpacer {
78
+ pointer-events: none;
79
+ background: transparent !important;
80
+ }
76
81
  }
77
82
 
78
83
  td,
@@ -1,6 +1,14 @@
1
1
  // src/shims/Table.tsx
2
2
  /* eslint-disable react/jsx-props-no-spreading */
3
- import React, { useState, useRef, ReactNode, CSSProperties, RefObject } from 'react';
3
+ import React, {
4
+ useState,
5
+ useRef,
6
+ ReactNode,
7
+ CSSProperties,
8
+ RefObject,
9
+ useEffect,
10
+ useLayoutEffect,
11
+ } from 'react';
4
12
  import { faChevronLeft } from '@fortawesome/free-solid-svg-icons/faChevronLeft';
5
13
  import { faChevronRight } from '@fortawesome/free-solid-svg-icons/faChevronRight';
6
14
  import clsx from 'clsx';
@@ -46,6 +54,13 @@ export type TableBodyType =
46
54
  | {
47
55
  type: 'filled';
48
56
  bodyRows: BodyRow[];
57
+ }
58
+ | {
59
+ type: 'virtual';
60
+ rowCount: number;
61
+ rows: BodyRow[];
62
+ range: { start: number; end: number };
63
+ onRangeChange?: (start: number, end: number) => void;
49
64
  };
50
65
 
51
66
  type Table = TableBodyType & {
@@ -92,6 +107,13 @@ interface TableProps {
92
107
  isLoading?: boolean;
93
108
  /* enables pagination */
94
109
  itemsPerPage?: number;
110
+ /* enables virtualization */
111
+ virtualize?: boolean;
112
+ virtualizeRowHeight?: number;
113
+ virtualizeOverscan?: number;
114
+ scrollRef?: RefObject<HTMLElement>;
115
+ /* debug: disable sort interactions */
116
+ disableSort?: boolean;
95
117
  }
96
118
 
97
119
  /**
@@ -143,9 +165,145 @@ function Table({
143
165
  className,
144
166
  isLoading,
145
167
  itemsPerPage,
168
+ virtualize,
169
+ virtualizeRowHeight,
170
+ virtualizeOverscan = 6,
171
+ scrollRef,
172
+ disableSort,
146
173
  }: TableProps) {
147
- const hasSort = sortByDirection && sortBy && updateSortParams;
174
+ const hasSort = !disableSort && sortByDirection && sortBy && updateSortParams;
148
175
  const [currPage, setCurrPage] = useState(0);
176
+ const [scrollTop, setScrollTop] = useState(0);
177
+ const [measuredRowHeight, setMeasuredRowHeight] = useState<number | null>(null);
178
+ const [scrollEl, setScrollEl] = useState<HTMLElement | null>(null);
179
+ const rafRef = useRef<number | null>(null);
180
+
181
+ useLayoutEffect(() => {
182
+ if (scrollRef?.current && scrollRef.current !== scrollEl) {
183
+ setScrollEl(scrollRef.current);
184
+ }
185
+ }, [scrollRef?.current, scrollEl]);
186
+
187
+ useEffect(() => {
188
+ if (!scrollEl || !virtualize) {
189
+ return () => {};
190
+ }
191
+ const onScroll = () => {
192
+ if (rafRef.current) {
193
+ cancelAnimationFrame(rafRef.current);
194
+ }
195
+ rafRef.current = requestAnimationFrame(() => {
196
+ setScrollTop(scrollEl.scrollTop || 0);
197
+ });
198
+ };
199
+ scrollEl.addEventListener('scroll', onScroll, { passive: true });
200
+ return () => {
201
+ scrollEl.removeEventListener('scroll', onScroll as EventListener);
202
+ if (rafRef.current) {
203
+ cancelAnimationFrame(rafRef.current);
204
+ }
205
+ };
206
+ }, [scrollEl, virtualize]);
207
+
208
+ useEffect(() => {
209
+ if (!virtualize || measuredRowHeight || !tableBodyRef?.current) {
210
+ return;
211
+ }
212
+ const rows = tableBodyRef.current.querySelectorAll('tr[data-row]');
213
+ if (!rows.length) {
214
+ return;
215
+ }
216
+ const rect = rows[0].getBoundingClientRect();
217
+ if (rect.height) {
218
+ setMeasuredRowHeight(rect.height);
219
+ }
220
+ }, [virtualize, measuredRowHeight, tableBodyRef?.current, table]);
221
+
222
+ const bodyRows =
223
+ table.type === 'filled'
224
+ ? paginate(table.bodyRows, currPage, itemsPerPage)
225
+ : null;
226
+
227
+ const totalRows =
228
+ table.type === 'virtual'
229
+ ? table.rowCount
230
+ : bodyRows
231
+ ? bodyRows.length
232
+ : 0;
233
+
234
+ const rowHeight = virtualizeRowHeight || measuredRowHeight || 0;
235
+ const canVirtualize =
236
+ !!virtualize &&
237
+ !!scrollEl &&
238
+ (table.type === 'filled' || table.type === 'virtual') &&
239
+ rowHeight > 0;
240
+
241
+ const virtualState = canVirtualize
242
+ ? (() => {
243
+ const viewportHeight = scrollEl!.clientHeight || 0;
244
+ const startIndex = Math.max(
245
+ 0,
246
+ Math.floor(scrollTop / rowHeight) - virtualizeOverscan
247
+ );
248
+ const endIndex = Math.min(
249
+ totalRows,
250
+ Math.ceil((scrollTop + viewportHeight) / rowHeight) + virtualizeOverscan
251
+ );
252
+ const topSpacer = startIndex * rowHeight;
253
+ const bottomSpacer = Math.max(0, (totalRows - endIndex) * rowHeight);
254
+ const visibleRows =
255
+ table.type === 'filled' && bodyRows
256
+ ? bodyRows.slice(startIndex, endIndex)
257
+ : [];
258
+
259
+ return {
260
+ viewportHeight,
261
+ totalRows,
262
+ startIndex,
263
+ endIndex,
264
+ topSpacer,
265
+ bottomSpacer,
266
+ visibleRows,
267
+ };
268
+ })()
269
+ : null;
270
+ const shouldLimitRows = !!virtualize && !canVirtualize && table.type === 'filled';
271
+ const limitedRows = shouldLimitRows
272
+ ? (bodyRows || []).slice(0, Math.max(24, virtualizeOverscan * 4))
273
+ : null;
274
+
275
+ useEffect(() => {
276
+ if (table.type !== 'virtual' || !virtualState) {
277
+ return;
278
+ }
279
+ table.onRangeChange?.(virtualState.startIndex, virtualState.endIndex);
280
+ }, [
281
+ table,
282
+ virtualState?.startIndex,
283
+ virtualState?.endIndex,
284
+ ]);
285
+
286
+ const placeholderRowCount =
287
+ table.type === 'virtual' && virtualState
288
+ ? Math.max(0, virtualState.endIndex - virtualState.startIndex)
289
+ : 0;
290
+ const placeholderRows: BodyRow[] =
291
+ table.type === 'virtual' && placeholderRowCount > 0
292
+ ? Array.from({ length: placeholderRowCount }, () => ({
293
+ cells: Array.from({ length: table.headRow.length }, () => ({
294
+ value: '\u00A0',
295
+ })),
296
+ }))
297
+ : [];
298
+
299
+ const renderVirtualRows =
300
+ table.type === 'virtual' &&
301
+ virtualState &&
302
+ table.range &&
303
+ table.range.start === virtualState.startIndex &&
304
+ table.range.end === virtualState.endIndex
305
+ ? table.rows
306
+ : placeholderRows;
149
307
 
150
308
  return isLoading ? (
151
309
  <div className={styles.loadingSpinner}>
@@ -197,11 +355,43 @@ function Table({
197
355
  <tr className={table?.bodyClassName}>
198
356
  <td colSpan={table.headRow.length}>{table.value}</td>
199
357
  </tr>
358
+ ) : virtualState ? (
359
+ <>
360
+ {virtualState.topSpacer > 0 && (
361
+ <tr aria-hidden="true" className={styles.virtualSpacer}>
362
+ <td
363
+ colSpan={table.headRow.length}
364
+ style={{ height: virtualState.topSpacer, padding: 0, border: 0 }}
365
+ />
366
+ </tr>
367
+ )}
368
+ {(table.type === 'virtual'
369
+ ? renderVirtualRows
370
+ : virtualState.visibleRows
371
+ ).map((row, idx) => (
372
+ <TableRow
373
+ key={row['data-row'] ?? `row-${virtualState.startIndex + idx}`}
374
+ row={row}
375
+ />
376
+ ))}
377
+ {virtualState.bottomSpacer > 0 && (
378
+ <tr aria-hidden="true" className={styles.virtualSpacer}>
379
+ <td
380
+ colSpan={table.headRow.length}
381
+ style={{ height: virtualState.bottomSpacer, padding: 0, border: 0 }}
382
+ />
383
+ </tr>
384
+ )}
385
+ </>
200
386
  ) : (
201
- paginate(table.bodyRows, currPage, itemsPerPage).map(
202
- (row, idx) => (
387
+ shouldLimitRows ? (
388
+ (limitedRows || []).map((row, idx) => (
389
+ <TableRow key={row['data-row'] ?? `row-${idx}`} row={row} />
390
+ ))
391
+ ) : (
392
+ (bodyRows || []).map((row, idx) => (
203
393
  <TableRow key={row['data-row'] ?? `row-${idx}`} row={row} />
204
- )
394
+ ))
205
395
  )
206
396
  )}
207
397
  </tbody>
@@ -0,0 +1,7 @@
1
+ export function createFlamegraphRenderWorker(): Worker {
2
+ const fromSrc = import.meta.url.includes('/src/');
3
+ const workerUrl = fromSrc
4
+ ? new URL('./flamegraphRenderWorker.ts', import.meta.url)
5
+ : new URL('./flamegraphRenderWorker.js', import.meta.url);
6
+ return new Worker(workerUrl, { type: 'module' });
7
+ }
@@ -0,0 +1,198 @@
1
+ import Color from 'color';
2
+ import { Maybe } from 'true-myth';
3
+ import type { Flamebearer } from '../models';
4
+ import Flamegraph from '../FlameGraph/FlameGraphComponent/Flamegraph';
5
+ import type { CanvasI18nMessages } from '../FlameGraph/FlameGraphComponent/Flamegraph_render';
6
+ import type { FlamegraphPalette } from '../FlameGraph/FlameGraphComponent/colorPalette';
7
+
8
+ type SerializablePalette = {
9
+ name: string;
10
+ goodColor: [number, number, number];
11
+ neutralColor: [number, number, number];
12
+ badColor: [number, number, number];
13
+ colors: [number, number, number][];
14
+ };
15
+
16
+ type FocusNode = { i: number; j: number } | null;
17
+
18
+ type FlamegraphRenderInit = {
19
+ type: 'init';
20
+ payload: {
21
+ kind: 'rect' | 'text';
22
+ canvas: OffscreenCanvas;
23
+ };
24
+ };
25
+
26
+ type FlamegraphRenderRequest = {
27
+ type: 'render';
28
+ payload: {
29
+ kind: 'rect' | 'text';
30
+ flamebearer: Flamebearer;
31
+ focusedNode: FocusNode;
32
+ fitMode: 'HEAD' | 'TAIL';
33
+ highlightQuery: string;
34
+ zoom: FocusNode;
35
+ palette: SerializablePalette;
36
+ messages?: CanvasI18nMessages;
37
+ renderRects?: boolean;
38
+ renderText?: boolean;
39
+ width: number;
40
+ devicePixelRatio: number;
41
+ };
42
+ };
43
+
44
+ type FlamegraphWorkerMessage = FlamegraphRenderInit | FlamegraphRenderRequest;
45
+
46
+ const state: {
47
+ rectCanvas: OffscreenCanvas | null;
48
+ textCanvas: OffscreenCanvas | null;
49
+ renderState: Record<
50
+ 'rect' | 'text',
51
+ {
52
+ token: number;
53
+ running: boolean;
54
+ nextI: number;
55
+ nextJ: number;
56
+ firstChunk: boolean;
57
+ payload: FlamegraphRenderRequest['payload'] | null;
58
+ flamegraph: Flamegraph | null;
59
+ }
60
+ >;
61
+ } = {
62
+ rectCanvas: null,
63
+ textCanvas: null,
64
+ renderState: {
65
+ rect: {
66
+ token: 0,
67
+ running: false,
68
+ nextI: 0,
69
+ nextJ: 0,
70
+ firstChunk: true,
71
+ payload: null,
72
+ flamegraph: null,
73
+ },
74
+ text: {
75
+ token: 0,
76
+ running: false,
77
+ nextI: 0,
78
+ nextJ: 0,
79
+ firstChunk: true,
80
+ payload: null,
81
+ flamegraph: null,
82
+ },
83
+ },
84
+ };
85
+
86
+ const buildPalette = (payload: SerializablePalette): FlamegraphPalette => ({
87
+ name: payload.name,
88
+ goodColor: Color.rgb(...payload.goodColor),
89
+ neutralColor: Color.rgb(...payload.neutralColor),
90
+ badColor: Color.rgb(...payload.badColor),
91
+ colors: payload.colors.map((c) => Color.rgb(...c)),
92
+ });
93
+
94
+ const toMaybe = (node: FocusNode) =>
95
+ node
96
+ ? Maybe.just(node)
97
+ : Maybe.nothing<{ i: number; j: number }>();
98
+
99
+ const RECT_BUDGET_MS = 12;
100
+ const TEXT_BUDGET_MS = 8;
101
+
102
+ const runChunk = (kind: 'rect' | 'text', token: number) => {
103
+ const renderState = state.renderState[kind];
104
+ const payload = renderState.payload;
105
+ if (!payload || renderState.token !== token) {
106
+ return;
107
+ }
108
+ const canvas = kind === 'rect' ? state.rectCanvas : state.textCanvas;
109
+ if (!canvas) {
110
+ // eslint-disable-next-line no-console
111
+ console.debug('[flamegraph-worker] missing canvas for', kind);
112
+ renderState.running = false;
113
+ return;
114
+ }
115
+ if (!renderState.flamegraph) {
116
+ renderState.flamegraph = new Flamegraph(
117
+ payload.flamebearer,
118
+ canvas,
119
+ toMaybe(payload.focusedNode),
120
+ payload.fitMode,
121
+ payload.highlightQuery,
122
+ toMaybe(payload.zoom),
123
+ buildPalette(payload.palette),
124
+ payload.messages
125
+ );
126
+ }
127
+
128
+ const result = renderState.flamegraph.render({
129
+ renderRects: payload.renderRects,
130
+ renderText: payload.renderText,
131
+ timeBudgetMs: kind === 'rect' ? RECT_BUDGET_MS : TEXT_BUDGET_MS,
132
+ startI: renderState.nextI,
133
+ startJ: renderState.nextJ,
134
+ skipCanvasResize: !renderState.firstChunk,
135
+ skipDprScale: !renderState.firstChunk,
136
+ devicePixelRatio: payload.devicePixelRatio,
137
+ });
138
+
139
+ renderState.firstChunk = false;
140
+ if (!result.done) {
141
+ renderState.nextI = result.nextI;
142
+ renderState.nextJ = result.nextJ;
143
+ setTimeout(() => runChunk(kind, token), 0);
144
+ return;
145
+ }
146
+
147
+ renderState.running = false;
148
+ renderState.nextI = 0;
149
+ renderState.nextJ = 0;
150
+ renderState.firstChunk = true;
151
+ renderState.flamegraph = null;
152
+ };
153
+
154
+ const renderFlamegraph = (payload: FlamegraphRenderRequest['payload']) => {
155
+ const canvas =
156
+ payload.kind === 'rect' ? state.rectCanvas : state.textCanvas;
157
+ if (!canvas) {
158
+ // eslint-disable-next-line no-console
159
+ console.debug('[flamegraph-worker] missing canvas for', payload.kind);
160
+ return;
161
+ }
162
+
163
+ if (payload.width > 0) {
164
+ canvas.width = payload.width;
165
+ }
166
+
167
+ const renderState = state.renderState[payload.kind];
168
+ renderState.token += 1;
169
+ renderState.running = true;
170
+ renderState.nextI = 0;
171
+ renderState.nextJ = 0;
172
+ renderState.firstChunk = true;
173
+ renderState.payload = payload;
174
+ renderState.flamegraph = null;
175
+
176
+ const token = renderState.token;
177
+ setTimeout(() => runChunk(payload.kind, token), 0);
178
+ };
179
+
180
+ self.onmessage = (event: MessageEvent<FlamegraphWorkerMessage>) => {
181
+ const msg = event.data;
182
+ if (!msg || !msg.type) {
183
+ return;
184
+ }
185
+ if (msg.type === 'init') {
186
+ if (msg.payload.kind === 'rect') {
187
+ state.rectCanvas = msg.payload.canvas;
188
+ } else {
189
+ state.textCanvas = msg.payload.canvas;
190
+ }
191
+ // eslint-disable-next-line no-console
192
+ console.debug('[flamegraph-worker] init', msg.payload.kind);
193
+ return;
194
+ }
195
+ if (msg.type === 'render') {
196
+ renderFlamegraph(msg.payload);
197
+ }
198
+ };