@parca/profile 0.19.122 → 0.19.124

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 (37) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/ProfileFlameChart/SamplesStrips/SamplesGraph/index.js +1 -1
  3. package/dist/ProfileFlameChart/index.d.ts.map +1 -1
  4. package/dist/ProfileFlameChart/index.js +11 -1
  5. package/dist/ProfileFlameGraph/FlameGraphArrow/MiniMap.d.ts +20 -0
  6. package/dist/ProfileFlameGraph/FlameGraphArrow/MiniMap.d.ts.map +1 -0
  7. package/dist/ProfileFlameGraph/FlameGraphArrow/MiniMap.js +173 -0
  8. package/dist/ProfileFlameGraph/FlameGraphArrow/ZoomControls.d.ts +11 -0
  9. package/dist/ProfileFlameGraph/FlameGraphArrow/ZoomControls.d.ts.map +1 -0
  10. package/dist/ProfileFlameGraph/FlameGraphArrow/ZoomControls.js +10 -0
  11. package/dist/ProfileFlameGraph/FlameGraphArrow/index.d.ts +1 -0
  12. package/dist/ProfileFlameGraph/FlameGraphArrow/index.d.ts.map +1 -1
  13. package/dist/ProfileFlameGraph/FlameGraphArrow/index.js +19 -8
  14. package/dist/ProfileFlameGraph/FlameGraphArrow/useZoom.d.ts +9 -0
  15. package/dist/ProfileFlameGraph/FlameGraphArrow/useZoom.d.ts.map +1 -0
  16. package/dist/ProfileFlameGraph/FlameGraphArrow/useZoom.js +88 -0
  17. package/dist/ProfileFlameGraph/index.d.ts +2 -1
  18. package/dist/ProfileFlameGraph/index.d.ts.map +1 -1
  19. package/dist/ProfileFlameGraph/index.js +4 -6
  20. package/dist/ProfileSelector/index.d.ts.map +1 -1
  21. package/dist/ProfileSelector/index.js +1 -0
  22. package/dist/ProfileSelector/useAutoQuerySelector.d.ts +2 -1
  23. package/dist/ProfileSelector/useAutoQuerySelector.d.ts.map +1 -1
  24. package/dist/ProfileSelector/useAutoQuerySelector.js +6 -1
  25. package/dist/TimelineGuide/index.js +1 -1
  26. package/dist/styles.css +1 -1
  27. package/package.json +3 -3
  28. package/src/ProfileFlameChart/SamplesStrips/SamplesGraph/index.tsx +1 -1
  29. package/src/ProfileFlameChart/index.tsx +23 -0
  30. package/src/ProfileFlameGraph/FlameGraphArrow/MiniMap.tsx +270 -0
  31. package/src/ProfileFlameGraph/FlameGraphArrow/ZoomControls.tsx +67 -0
  32. package/src/ProfileFlameGraph/FlameGraphArrow/index.tsx +97 -38
  33. package/src/ProfileFlameGraph/FlameGraphArrow/useZoom.ts +116 -0
  34. package/src/ProfileFlameGraph/index.tsx +6 -14
  35. package/src/ProfileSelector/index.tsx +1 -0
  36. package/src/ProfileSelector/useAutoQuerySelector.ts +9 -0
  37. package/src/TimelineGuide/index.tsx +2 -2
@@ -28,22 +28,27 @@ import {FlamegraphArrow} from '@parca/client';
28
28
  import {FlameGraphSkeleton, SandwichFlameGraphSkeleton, useParcaContext} from '@parca/components';
29
29
  import {USER_PREFERENCES, useCurrentColorProfile, useUserPreference} from '@parca/hooks';
30
30
  import {ProfileType} from '@parca/parser';
31
- import {getColorForFeature, selectDarkMode, useAppSelector} from '@parca/store';
31
+ import {getColorForFeature} from '@parca/store';
32
32
  import {type ColorConfig} from '@parca/utilities';
33
33
 
34
34
  import {ProfileSource} from '../../ProfileSource';
35
35
  import {useProfileFilters} from '../../ProfileView/components/ProfileFilters/useProfileFilters';
36
36
  import {useProfileViewContext} from '../../ProfileView/context/ProfileViewContext';
37
+ import {TimelineGuide} from '../../TimelineGuide';
37
38
  import {alignedUint8Array} from '../../utils';
38
39
  import ContextMenuWrapper, {ContextMenuWrapperRef} from './ContextMenuWrapper';
39
40
  import {FlameNode, RowHeight, colorByColors} from './FlameGraphNodes';
40
41
  import {MemoizedTooltip} from './MemoizedTooltip';
42
+ import {MiniMap} from './MiniMap';
41
43
  import {TooltipProvider} from './TooltipContext';
44
+ import {ZoomControls} from './ZoomControls';
42
45
  import {useBatchedRendering} from './useBatchedRendering';
43
46
  import {useScrollViewport} from './useScrollViewport';
44
47
  import {useVisibleNodes} from './useVisibleNodes';
48
+ import {useZoom} from './useZoom';
45
49
  import {
46
50
  CurrentPathFrame,
51
+ boundsFromProfileSource,
47
52
  extractFeature,
48
53
  extractFilenameFeature,
49
54
  getCurrentPathFrameData,
@@ -93,6 +98,7 @@ interface FlameGraphArrowProps {
93
98
  tooltipId?: string;
94
99
  maxFrameCount?: number;
95
100
  isExpanded?: boolean;
101
+ zoomControlsRef?: React.RefObject<HTMLDivElement | null>;
96
102
  }
97
103
 
98
104
  export const getMappingColors = (
@@ -145,14 +151,14 @@ export const FlameGraphArrow = memo(function FlameGraphArrow({
145
151
  mappingsListFromMetadata,
146
152
  filenamesListFromMetadata,
147
153
  colorBy,
154
+ zoomControlsRef,
148
155
  }: FlameGraphArrowProps): React.JSX.Element {
149
156
  const [highlightSimilarStacksPreference] = useUserPreference<boolean>(
150
157
  USER_PREFERENCES.HIGHLIGHT_SIMILAR_STACKS.key
151
158
  );
152
159
  const [hoveringRow, setHoveringRow] = useState<number | undefined>(undefined);
153
160
  const [dockedMetainfo] = useUserPreference<boolean>(USER_PREFERENCES.GRAPH_METAINFO_DOCKED.key);
154
- const isDarkMode = useAppSelector(selectDarkMode);
155
- const {perf} = useParcaContext();
161
+ const {perf, isDarkMode} = useParcaContext();
156
162
 
157
163
  const table: Table = useMemo(() => {
158
164
  const result = tableFromIPC(alignedUint8Array(arrow.record), {useBigInt: true});
@@ -261,6 +267,18 @@ export const FlameGraphArrow = memo(function FlameGraphArrow({
261
267
  // Get the viewport of the container, this is used to determine which rows are visible.
262
268
  const viewport = useScrollViewport(containerRef);
263
269
 
270
+ const isZoomEnabled = isFlameChart;
271
+
272
+ const {zoomLevel, zoomIn, zoomOut, resetZoom} = useZoom(
273
+ isZoomEnabled ? containerRef : {current: null}
274
+ );
275
+ const zoomedWidth = isZoomEnabled ? Math.round((width ?? 1) * zoomLevel) : width ?? 0;
276
+
277
+ // Reset zoom when the data changes (e.g. new query, different time range)
278
+ useEffect(() => {
279
+ resetZoom();
280
+ }, [table, resetZoom]);
281
+
264
282
  // To find the selected row, we must walk the current path and look at which
265
283
  // children of the current frame matches the path element exactly. Until the
266
284
  // end, the row we find at the end is our selected row.
@@ -290,7 +308,7 @@ export const FlameGraphArrow = memo(function FlameGraphArrow({
290
308
  table,
291
309
  viewport,
292
310
  total,
293
- width: width ?? 1,
311
+ width: zoomedWidth,
294
312
  selectedRow,
295
313
  effectiveDepth: deferredEffectiveDepth,
296
314
  });
@@ -328,6 +346,15 @@ export const FlameGraphArrow = memo(function FlameGraphArrow({
328
346
  tooltipId={tooltipId}
329
347
  >
330
348
  <div className="relative">
349
+ {isZoomEnabled && (
350
+ <ZoomControls
351
+ zoomLevel={zoomLevel}
352
+ zoomIn={zoomIn}
353
+ zoomOut={zoomOut}
354
+ resetZoom={resetZoom}
355
+ portalRef={zoomControlsRef}
356
+ />
357
+ )}
331
358
  <ContextMenuWrapper
332
359
  ref={contextMenuRef}
333
360
  menuId={MENU_ID}
@@ -352,49 +379,81 @@ export const FlameGraphArrow = memo(function FlameGraphArrow({
352
379
  )}
353
380
  </div>
354
381
  )}
382
+ {isZoomEnabled && (
383
+ <MiniMap
384
+ containerRef={containerRef}
385
+ table={table}
386
+ width={width ?? 0}
387
+ zoomedWidth={zoomedWidth}
388
+ totalHeight={totalHeight}
389
+ maxDepth={deferredEffectiveDepth}
390
+ colorByColors={colorByColors}
391
+ colorBy={colorByValue}
392
+ profileSource={profileSource}
393
+ isDarkMode={isDarkMode}
394
+ scrollLeft={viewport.scrollLeft}
395
+ />
396
+ )}
355
397
  <div
356
398
  ref={containerRef}
357
- className="overflow-auto scrollbar-thin scrollbar-thumb-gray-400 scrollbar-track-gray-100 dark:scrollbar-thumb-gray-600 dark:scrollbar-track-gray-800 will-change-transform scroll-smooth webkit-overflow-scrolling-touch contain"
399
+ className={`${
400
+ isZoomEnabled ? '[scrollbar-width:none] [&::-webkit-scrollbar]:hidden' : ''
401
+ } will-change-transform webkit-overflow-scrolling-touch contain ${
402
+ !isZoomEnabled ? 'overflow-auto' : ''
403
+ }`}
358
404
  style={{
359
405
  width: width ?? '100%',
406
+ ...(isZoomEnabled ? {overflowX: 'scroll' as const, overflowY: 'auto' as const} : {}),
360
407
  contain: 'layout style paint',
361
408
  visibility: !showSkeleton ? 'visible' : 'hidden',
362
409
  }}
363
410
  >
364
- <svg
365
- className="font-robotoMono"
366
- width={width ?? 0}
367
- height={totalHeight}
368
- preserveAspectRatio="xMinYMid"
369
- ref={svg}
370
- >
371
- {batchedNodes.map(row => (
372
- <FlameNode
373
- key={row}
374
- table={table}
375
- row={row}
376
- colors={colorByColors}
377
- colorBy={colorByValue}
378
- totalWidth={width ?? 1}
379
- height={RowHeight}
380
- darkMode={isDarkMode}
381
- compareMode={compareMode}
382
- colorForSimilarNodes={colorForSimilarNodes}
383
- selectedRow={selectedRow}
384
- onClick={() => handleRowClick(row)}
385
- onContextMenu={displayMenu}
386
- hoveringRow={highlightSimilarStacksPreference ? hoveringRow : undefined}
387
- setHoveringRow={highlightSimilarStacksPreference ? setHoveringRow : noop}
388
- isFlameChart={isFlameChart}
389
- profileSource={profileSource}
390
- isRenderedAsFlamegraph={isRenderedAsFlamegraph}
391
- isInSandwichView={isInSandwichView}
392
- maxDepth={maxDepth}
393
- effectiveDepth={deferredEffectiveDepth}
394
- tooltipId={tooltipId}
411
+ <div>
412
+ {isFlameChart && (
413
+ <TimelineGuide
414
+ bounds={boundsFromProfileSource(profileSource)}
415
+ width={zoomedWidth}
416
+ height={totalHeight}
417
+ margin={0}
418
+ ticks={12}
419
+ timeUnit="nanoseconds"
395
420
  />
396
- ))}
397
- </svg>
421
+ )}
422
+ <svg
423
+ className="relative font-robotoMono"
424
+ width={zoomedWidth}
425
+ height={totalHeight}
426
+ preserveAspectRatio="xMinYMid"
427
+ ref={svg}
428
+ >
429
+ {batchedNodes.map(row => (
430
+ <FlameNode
431
+ key={row}
432
+ table={table}
433
+ row={row}
434
+ colors={colorByColors}
435
+ colorBy={colorByValue}
436
+ totalWidth={zoomedWidth}
437
+ height={RowHeight}
438
+ darkMode={isDarkMode}
439
+ compareMode={compareMode}
440
+ colorForSimilarNodes={colorForSimilarNodes}
441
+ selectedRow={selectedRow}
442
+ onClick={() => handleRowClick(row)}
443
+ onContextMenu={displayMenu}
444
+ hoveringRow={highlightSimilarStacksPreference ? hoveringRow : undefined}
445
+ setHoveringRow={highlightSimilarStacksPreference ? setHoveringRow : noop}
446
+ isFlameChart={isFlameChart}
447
+ profileSource={profileSource}
448
+ isRenderedAsFlamegraph={isRenderedAsFlamegraph}
449
+ isInSandwichView={isInSandwichView}
450
+ maxDepth={maxDepth}
451
+ effectiveDepth={deferredEffectiveDepth}
452
+ tooltipId={tooltipId}
453
+ />
454
+ ))}
455
+ </svg>
456
+ </div>
398
457
  </div>
399
458
  </div>
400
459
  </TooltipProvider>
@@ -0,0 +1,116 @@
1
+ // Copyright 2022 The Parca Authors
2
+ // Licensed under the Apache License, Version 2.0 (the "License");
3
+ // you may not use this file except in compliance with the License.
4
+ // You may obtain a copy of the License at
5
+ //
6
+ // http://www.apache.org/licenses/LICENSE-2.0
7
+ //
8
+ // Unless required by applicable law or agreed to in writing, software
9
+ // distributed under the License is distributed on an "AS IS" BASIS,
10
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11
+ // See the License for the specific language governing permissions and
12
+ // limitations under the License.
13
+
14
+ import {useCallback, useEffect, useRef, useState} from 'react';
15
+
16
+ import {flushSync} from 'react-dom';
17
+
18
+ const MIN_ZOOM = 1.0;
19
+ const MAX_ZOOM = 20.0;
20
+ const BUTTON_ZOOM_STEP = 1.5;
21
+ // Sensitivity for trackpad/wheel zoom - smaller = smoother
22
+ const WHEEL_ZOOM_SENSITIVITY = 0.01;
23
+
24
+ interface UseZoomResult {
25
+ zoomLevel: number;
26
+ zoomIn: () => void;
27
+ zoomOut: () => void;
28
+ resetZoom: () => void;
29
+ }
30
+
31
+ const clampZoom = (zoom: number): number => {
32
+ return Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, zoom));
33
+ };
34
+
35
+ export const useZoom = (containerRef: React.RefObject<HTMLDivElement | null>): UseZoomResult => {
36
+ const [zoomLevel, setZoomLevel] = useState(MIN_ZOOM);
37
+ const zoomLevelRef = useRef(MIN_ZOOM);
38
+
39
+ // Adjust scrollLeft so the content under focalX stays fixed after zoom change.
40
+ const adjustScroll = useCallback(
41
+ (oldZoom: number, newZoom: number, focalX: number) => {
42
+ const container = containerRef.current;
43
+ if (container === null) return;
44
+
45
+ const contentX = container.scrollLeft + focalX;
46
+ const ratio = contentX / oldZoom;
47
+ container.scrollLeft = ratio * newZoom - focalX;
48
+ },
49
+ [containerRef]
50
+ );
51
+
52
+ // Apply a new zoom level around a focal point
53
+ const applyZoom = useCallback(
54
+ (newZoom: number, focalX: number) => {
55
+ const oldZoom = zoomLevelRef.current;
56
+ if (newZoom === oldZoom) return;
57
+ zoomLevelRef.current = newZoom;
58
+
59
+ // flushSync ensures the DOM updates with the new content width before adjustScroll reads it
60
+ flushSync(() => setZoomLevel(newZoom));
61
+ adjustScroll(oldZoom, newZoom, focalX);
62
+ },
63
+ [adjustScroll]
64
+ );
65
+
66
+ const zoomIn = useCallback(() => {
67
+ const newZoom = clampZoom(zoomLevelRef.current * BUTTON_ZOOM_STEP);
68
+ const container = containerRef.current;
69
+ applyZoom(newZoom, container !== null ? container.clientWidth / 2 : 0);
70
+ }, [containerRef, applyZoom]);
71
+
72
+ const zoomOut = useCallback(() => {
73
+ const newZoom = clampZoom(zoomLevelRef.current / BUTTON_ZOOM_STEP);
74
+ const container = containerRef.current;
75
+ applyZoom(newZoom, container !== null ? container.clientWidth / 2 : 0);
76
+ }, [containerRef, applyZoom]);
77
+
78
+ const resetZoom = useCallback(() => {
79
+ zoomLevelRef.current = MIN_ZOOM;
80
+ setZoomLevel(MIN_ZOOM);
81
+ const container = containerRef.current;
82
+ if (container !== null) {
83
+ container.scrollLeft = 0;
84
+ }
85
+ }, [containerRef]);
86
+
87
+ useEffect(() => {
88
+ const container = containerRef.current;
89
+ if (container === null) return;
90
+
91
+ const handleWheel = (e: WheelEvent): void => {
92
+ if (!e.ctrlKey && !e.metaKey) return;
93
+ e.preventDefault();
94
+
95
+ let delta = e.deltaY;
96
+ if (e.deltaMode === 1) {
97
+ delta *= 20;
98
+ }
99
+
100
+ // Limiting the max zoom step per event to 15%, so to fix the huge jumps in Linux OS.
101
+ const MAX_FACTOR = 0.15;
102
+ const rawFactor = -delta * WHEEL_ZOOM_SENSITIVITY;
103
+ const zoomFactor = 1 + Math.max(-MAX_FACTOR, Math.min(MAX_FACTOR, rawFactor));
104
+
105
+ const newZoom = clampZoom(zoomLevelRef.current * zoomFactor);
106
+ applyZoom(newZoom, e.clientX - container.getBoundingClientRect().left);
107
+ };
108
+
109
+ container.addEventListener('wheel', handleWheel, {passive: false});
110
+ return () => {
111
+ container.removeEventListener('wheel', handleWheel);
112
+ };
113
+ }, [containerRef, applyZoom]);
114
+
115
+ return {zoomLevel, zoomIn, zoomOut, resetZoom};
116
+ };
@@ -33,9 +33,8 @@ import DiffLegend from '../ProfileView/components/DiffLegend';
33
33
  import {useProfileViewContext} from '../ProfileView/context/ProfileViewContext';
34
34
  import {useProfileMetadata} from '../ProfileView/hooks/useProfileMetadata';
35
35
  import {useVisualizationState} from '../ProfileView/hooks/useVisualizationState';
36
- import {TimelineGuide} from '../TimelineGuide';
37
36
  import {FlameGraphArrow} from './FlameGraphArrow';
38
- import {CurrentPathFrame, boundsFromProfileSource} from './FlameGraphArrow/utils';
37
+ import {CurrentPathFrame} from './FlameGraphArrow/utils';
39
38
 
40
39
  const numberFormatter = new Intl.NumberFormat('en-US');
41
40
 
@@ -62,6 +61,7 @@ interface ProfileFlameGraphProps {
62
61
  tooltipId?: string;
63
62
  maxFrameCount?: number;
64
63
  isExpanded?: boolean;
64
+ zoomControlsRef?: React.RefObject<HTMLDivElement | null>;
65
65
  }
66
66
 
67
67
  const ErrorContent = ({errorMessage}: {errorMessage: string | ReactNode}): JSX.Element => {
@@ -101,11 +101,12 @@ const ProfileFlameGraph = function ProfileFlameGraphNonMemo({
101
101
  maxFrameCount,
102
102
  isExpanded = false,
103
103
  metadataLoading = false,
104
+ zoomControlsRef,
104
105
  }: ProfileFlameGraphProps): JSX.Element {
105
106
  const {onError, authenticationErrorMessage, isDarkMode, flamechartHelpText} = useParcaContext();
106
107
  const {compareMode} = useProfileViewContext();
107
108
  const [isLoading, setIsLoading] = useState<boolean>(true);
108
- const [flameChartRef, {height: flameChartHeight}] = useMeasure();
109
+ const [flameChartRef] = useMeasure();
109
110
  const {colorBy, setColorBy} = useVisualizationState();
110
111
 
111
112
  // Create local state for paths when in sandwich view to avoid URL updates
@@ -263,16 +264,6 @@ const ProfileFlameGraph = function ProfileFlameGraphNonMemo({
263
264
  if (arrow !== undefined) {
264
265
  return (
265
266
  <div className="relative">
266
- {isFlameChart ? (
267
- <TimelineGuide
268
- bounds={boundsFromProfileSource(profileSource)}
269
- width={width}
270
- height={flameChartHeight ?? 420}
271
- margin={0}
272
- ticks={12}
273
- timeUnit="nanoseconds"
274
- />
275
- ) : null}
276
267
  <div ref={flameChartRef as LegacyRef<HTMLDivElement>}>
277
268
  <FlameGraphArrow
278
269
  width={width}
@@ -294,6 +285,7 @@ const ProfileFlameGraph = function ProfileFlameGraphNonMemo({
294
285
  maxFrameCount={maxFrameCount}
295
286
  isExpanded={isExpanded}
296
287
  colorBy={colorBy}
288
+ zoomControlsRef={zoomControlsRef}
297
289
  />
298
290
  </div>
299
291
  </div>
@@ -312,7 +304,6 @@ const ProfileFlameGraph = function ProfileFlameGraphNonMemo({
312
304
  isCompareAbsolute,
313
305
  isFlameChart,
314
306
  profileSource,
315
- flameChartHeight,
316
307
  flameChartRef,
317
308
  flamechartHelpText,
318
309
  isRenderedAsFlamegraph,
@@ -325,6 +316,7 @@ const ProfileFlameGraph = function ProfileFlameGraphNonMemo({
325
316
  mappingsList,
326
317
  filenamesList,
327
318
  colorBy,
319
+ zoomControlsRef,
328
320
  ]);
329
321
 
330
322
  useEffect(() => {
@@ -276,6 +276,7 @@ const ProfileSelector = ({
276
276
  querySelection,
277
277
  navigateTo,
278
278
  loading: sumByLoading,
279
+ defaultProfileType: viewComponent?.defaultProfileType,
279
280
  });
280
281
 
281
282
  const searchDisabled =
@@ -29,6 +29,7 @@ interface Props {
29
29
  querySelection: QuerySelection;
30
30
  navigateTo: NavigateFunction;
31
31
  loading: boolean;
32
+ defaultProfileType?: string;
32
33
  }
33
34
 
34
35
  export const useAutoQuerySelector = ({
@@ -39,6 +40,7 @@ export const useAutoQuerySelector = ({
39
40
  querySelection,
40
41
  navigateTo,
41
42
  loading,
43
+ defaultProfileType,
42
44
  }: Props): void => {
43
45
  const autoQuery = useAppSelector(selectAutoQuery);
44
46
  const dispatch = useAppDispatch();
@@ -136,6 +138,12 @@ export const useAutoQuerySelector = ({
136
138
  return;
137
139
  }
138
140
  dispatch(setAutoQuery('true'));
141
+
142
+ if (defaultProfileType != null && defaultProfileType.length > 0) {
143
+ setProfileName(defaultProfileType);
144
+ return;
145
+ }
146
+
139
147
  let profileType = profileTypesData.types.find(
140
148
  type => type.name === 'parca_agent' && type.sampleType === 'samples' && type.delta
141
149
  );
@@ -166,6 +174,7 @@ export const useAutoQuerySelector = ({
166
174
  dispatch,
167
175
  setQueryExpression,
168
176
  setProfileName,
177
+ defaultProfileType,
169
178
  ]);
170
179
 
171
180
  useEffect(() => {
@@ -49,8 +49,8 @@ export const TimelineGuide = ({
49
49
 
50
50
  return (
51
51
  <div className="relative h-5">
52
- <div className="absolute" style={{width, height}}>
53
- <svg style={{width: '100%', height: '100%'}} className="z-[5]">
52
+ <div className="pointer-events-none absolute" style={{width, height}}>
53
+ <svg style={{width: '100%', height: '100%'}}>
54
54
  <g
55
55
  className="x axis"
56
56
  fill="none"