@parca/profile 0.19.121 → 0.19.123

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 (45) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/GraphTooltipArrow/Content.js +1 -1
  3. package/dist/MetricsGraph/useMetricsGraphDimensions.d.ts.map +1 -1
  4. package/dist/MetricsGraph/useMetricsGraphDimensions.js +5 -3
  5. package/dist/ProfileFlameChart/SamplesStrips/SamplesGraph/index.js +1 -1
  6. package/dist/ProfileFlameChart/index.d.ts.map +1 -1
  7. package/dist/ProfileFlameChart/index.js +11 -1
  8. package/dist/ProfileFlameGraph/FlameGraphArrow/MiniMap.d.ts +20 -0
  9. package/dist/ProfileFlameGraph/FlameGraphArrow/MiniMap.d.ts.map +1 -0
  10. package/dist/ProfileFlameGraph/FlameGraphArrow/MiniMap.js +173 -0
  11. package/dist/ProfileFlameGraph/FlameGraphArrow/ZoomControls.d.ts +11 -0
  12. package/dist/ProfileFlameGraph/FlameGraphArrow/ZoomControls.d.ts.map +1 -0
  13. package/dist/ProfileFlameGraph/FlameGraphArrow/ZoomControls.js +10 -0
  14. package/dist/ProfileFlameGraph/FlameGraphArrow/index.d.ts +1 -0
  15. package/dist/ProfileFlameGraph/FlameGraphArrow/index.d.ts.map +1 -1
  16. package/dist/ProfileFlameGraph/FlameGraphArrow/index.js +19 -8
  17. package/dist/ProfileFlameGraph/FlameGraphArrow/useZoom.d.ts +9 -0
  18. package/dist/ProfileFlameGraph/FlameGraphArrow/useZoom.d.ts.map +1 -0
  19. package/dist/ProfileFlameGraph/FlameGraphArrow/useZoom.js +88 -0
  20. package/dist/ProfileFlameGraph/index.d.ts +2 -1
  21. package/dist/ProfileFlameGraph/index.d.ts.map +1 -1
  22. package/dist/ProfileFlameGraph/index.js +4 -6
  23. package/dist/ProfileMetricsGraph/index.d.ts.map +1 -1
  24. package/dist/ProfileMetricsGraph/index.js +2 -1
  25. package/dist/ProfileSelector/MetricsGraphSection.d.ts +1 -2
  26. package/dist/ProfileSelector/MetricsGraphSection.d.ts.map +1 -1
  27. package/dist/ProfileSelector/MetricsGraphSection.js +4 -1
  28. package/dist/ProfileSelector/index.d.ts.map +1 -1
  29. package/dist/ProfileSelector/index.js +8 -3
  30. package/dist/TimelineGuide/index.js +1 -1
  31. package/dist/styles.css +1 -1
  32. package/package.json +3 -3
  33. package/src/GraphTooltipArrow/Content.tsx +1 -1
  34. package/src/MetricsGraph/useMetricsGraphDimensions.ts +7 -5
  35. package/src/ProfileFlameChart/SamplesStrips/SamplesGraph/index.tsx +1 -1
  36. package/src/ProfileFlameChart/index.tsx +23 -0
  37. package/src/ProfileFlameGraph/FlameGraphArrow/MiniMap.tsx +270 -0
  38. package/src/ProfileFlameGraph/FlameGraphArrow/ZoomControls.tsx +67 -0
  39. package/src/ProfileFlameGraph/FlameGraphArrow/index.tsx +97 -38
  40. package/src/ProfileFlameGraph/FlameGraphArrow/useZoom.ts +116 -0
  41. package/src/ProfileFlameGraph/index.tsx +6 -14
  42. package/src/ProfileMetricsGraph/index.tsx +5 -1
  43. package/src/ProfileSelector/MetricsGraphSection.tsx +3 -2
  44. package/src/ProfileSelector/index.tsx +7 -3
  45. package/src/TimelineGuide/index.tsx +2 -2
package/CHANGELOG.md CHANGED
@@ -3,6 +3,14 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ ## [0.19.123](https://github.com/parca-dev/parca/compare/@parca/profile@0.19.122...@parca/profile@0.19.123) (2026-02-16)
7
+
8
+ **Note:** Version bump only for package @parca/profile
9
+
10
+ ## [0.19.122](https://github.com/parca-dev/parca/compare/@parca/profile@0.19.121...@parca/profile@0.19.122) (2026-02-13)
11
+
12
+ **Note:** Version bump only for package @parca/profile
13
+
6
14
  ## [0.19.121](https://github.com/parca-dev/parca/compare/@parca/profile@0.19.120...@parca/profile@0.19.121) (2026-02-12)
7
15
 
8
16
  **Note:** Version bump only for package @parca/profile
@@ -35,6 +35,6 @@ const TooltipMetaInfo = ({ table, row }) => {
35
35
  const labels = labelPairs.map((l) => (_jsx("span", { className: "mr-3 inline-block rounded-lg bg-gray-200 px-2 py-1 text-xs font-bold text-gray-700 dark:bg-gray-700 dark:text-gray-400", children: `${l[0]}="${l[1]}"` }, l[0])));
36
36
  const isMappingBuildIDAvailable = mappingBuildID !== null && mappingBuildID !== '';
37
37
  const inlinedText = inlined === null ? 'merged' : inlined ? 'yes' : 'no';
38
- return (_jsxs(_Fragment, { children: [timestamp != null && timestamp !== 0n && (_jsxs("tr", { children: [_jsx("td", { className: "w-1/4 pt-2", children: "Timestamp" }), _jsx("td", { className: "w-3/4 pt-2 break-all", children: formatDateTimeDownToMS(new Date(Number(timestamp / 1000000n)), timezone) })] })), _jsxs("tr", { children: [_jsx("td", { className: "w-1/4", children: "File" }), _jsx("td", { className: "w-3/4 break-all", children: functionFilename === '' ? (_jsx(NoData, {})) : (_jsx("div", { className: "flex gap-4", children: _jsx("div", { className: "whitespace-nowrap text-left", children: _jsx(ExpandOnHover, { value: file, displayValue: truncateStringReverse(file, 30) }) }) })) })] }), _jsxs("tr", { children: [_jsx("td", { className: "w-1/4", children: "Address" }), _jsx("td", { className: "w-3/4 break-all", children: locationAddress === 0n ? _jsx(NoData, {}) : _jsx("div", { children: hexifyAddress(locationAddress) }) })] }), _jsxs("tr", { children: [_jsx("td", { className: "w-1/4", children: "Inlined" }), _jsx("td", { className: "w-3/4 break-all", children: inlinedText })] }), _jsxs("tr", { children: [_jsx("td", { className: "w-1/4", children: "Binary" }), _jsx("td", { className: "w-3/4 break-all", children: (mappingFile != null ? getLastItem(mappingFile) : null) ?? _jsx(NoData, {}) })] }), _jsxs("tr", { children: [_jsx("td", { className: "w-1/4", children: "Build Id" }), _jsx("td", { className: "w-3/4 break-all", children: isMappingBuildIDAvailable ? _jsx("div", { children: truncateString(mappingBuildID, 28) }) : _jsx(NoData, {}) })] }), labelPairs.length > 0 && (_jsxs("tr", { children: [_jsx("td", { className: "w-1/4", children: "Labels" }), _jsx("td", { className: "w-3/4 break-all", children: labels })] }))] }));
38
+ return (_jsxs(_Fragment, { children: [timestamp != null && timestamp !== 0n && (_jsxs("tr", { children: [_jsx("td", { className: "w-1/4 pt-2", children: "Timestamp" }), _jsx("td", { className: "w-3/4 pt-2 break-all", children: formatDateTimeDownToMS(new Date(Number(timestamp / 1000000n)), timezone) })] })), _jsxs("tr", { children: [_jsx("td", { className: "w-1/4", children: "File" }), _jsx("td", { className: "w-3/4 break-all", children: functionFilename === '' ? (_jsx(NoData, {})) : (_jsx("div", { className: "flex gap-4", children: _jsx("div", { className: "whitespace-nowrap text-left", children: _jsx(ExpandOnHover, { value: file, displayValue: truncateStringReverse(file, 50) }) }) })) })] }), _jsxs("tr", { children: [_jsx("td", { className: "w-1/4", children: "Address" }), _jsx("td", { className: "w-3/4 break-all", children: locationAddress === 0n ? _jsx(NoData, {}) : _jsx("div", { children: hexifyAddress(locationAddress) }) })] }), _jsxs("tr", { children: [_jsx("td", { className: "w-1/4", children: "Inlined" }), _jsx("td", { className: "w-3/4 break-all", children: inlinedText })] }), _jsxs("tr", { children: [_jsx("td", { className: "w-1/4", children: "Binary" }), _jsx("td", { className: "w-3/4 break-all", children: (mappingFile != null ? getLastItem(mappingFile) : null) ?? _jsx(NoData, {}) })] }), _jsxs("tr", { children: [_jsx("td", { className: "w-1/4", children: "Build Id" }), _jsx("td", { className: "w-3/4 break-all", children: isMappingBuildIDAvailable ? _jsx("div", { children: truncateString(mappingBuildID, 28) }) : _jsx(NoData, {}) })] }), labelPairs.length > 0 && (_jsxs("tr", { children: [_jsx("td", { className: "w-1/4", children: "Labels" }), _jsx("td", { className: "w-3/4 break-all", children: labels })] }))] }));
39
39
  };
40
40
  export default GraphTooltipArrowContent;
@@ -1 +1 @@
1
- {"version":3,"file":"useMetricsGraphDimensions.d.ts","sourceRoot":"","sources":["../../src/MetricsGraph/useMetricsGraphDimensions.ts"],"names":[],"mappings":"AAiBA,UAAU,sBAAsB;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;CAChB;AAMD,eAAO,MAAM,yBAAyB,GACpC,WAAW,OAAO,EAClB,gBAAc,KACb,sBA4BF,CAAC"}
1
+ {"version":3,"file":"useMetricsGraphDimensions.d.ts","sourceRoot":"","sources":["../../src/MetricsGraph/useMetricsGraphDimensions.ts"],"names":[],"mappings":"AAiBA,UAAU,sBAAsB;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;CAChB;AAMD,eAAO,MAAM,yBAAyB,GACpC,WAAW,OAAO,EAClB,gBAAc,KACb,sBA8BF,CAAC"}
@@ -32,9 +32,11 @@ export const useMetricsGraphDimensions = (comparing, isMini = false) => {
32
32
  width = width / 2 - 32;
33
33
  }
34
34
  const height = isMini ? MINI_VARIANT_HEIGHT : Math.min(width / 2.5, maxHeight);
35
- const heightStyle = `min(${maxHeight + margin}px, ${comparing
36
- ? profileExplorer.metricsGraph.maxHeightStyle.compareMode
37
- : profileExplorer.metricsGraph.maxHeightStyle.default})`;
35
+ const heightStyle = isMini
36
+ ? `${MINI_VARIANT_HEIGHT + margin}px`
37
+ : `min(${maxHeight + margin}px, ${comparing
38
+ ? profileExplorer.metricsGraph.maxHeightStyle.compareMode
39
+ : profileExplorer.metricsGraph.maxHeightStyle.default})`;
38
40
  return {
39
41
  width,
40
42
  height,
@@ -81,7 +81,7 @@ const ZoomWindow = ({ zoomWindow, onZoomWindowChange, setIsHoveringDragHandle, }
81
81
  height: '100%',
82
82
  width: zoomWindowState[1] - zoomWindowState[0],
83
83
  left: zoomWindowState[0],
84
- }, className: cx('bg-gray-500/50 dark:bg-gray-100/90 absolute top-0 border-x-2 border-gray-900 dark:border-gray-100 z-20'), children: [_jsx("div", { className: "w-3 h-4 absolute top-0 left-[-7px] rounded-b bg-gray-200 dark:bg-gray-600 cursor-ew-resize flex justify-center z-30", onMouseDown: e => {
84
+ }, className: cx('bg-gray-500/50 dark:bg-gray-400/90 absolute top-0 border-x-2 border-gray-900 dark:border-gray-100 z-20'), children: [_jsx("div", { className: "w-3 h-4 absolute top-0 left-[-7px] rounded-b bg-gray-200 dark:bg-gray-600 cursor-ew-resize flex justify-center z-30", onMouseDown: e => {
85
85
  setDraggingStart(true);
86
86
  e.stopPropagation();
87
87
  e.preventDefault();
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/ProfileFlameChart/index.tsx"],"names":[],"mappings":"AAeA,OAAO,EAAoC,kBAAkB,EAAC,MAAM,eAAe,CAAC;AAQpF,OAAO,EAAwB,WAAW,EAAQ,MAAM,eAAe,CAAC;AAIxE,OAAO,EAAsB,aAAa,EAAC,MAAM,kBAAkB,CAAC;AACpE,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,oCAAoC,CAAC;AA4CpE,UAAU,sBAAsB;IAC9B,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,WAAW,EAAE,kBAAkB,CAAC;IAChC,aAAa,EAAE,aAAa,CAAC;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,YAAY,EAAE,OAAO,CAAC;IACtB,oBAAoB,CAAC,EAAE,MAAM,EAAE,CAAC;IAChC,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,mBAAmB,CAAC,EAAE,MAAM,IAAI,CAAC;CAClC;AA+BD,eAAO,MAAM,iBAAiB,GAAI,6JAY/B,sBAAsB,KAAG,GAAG,CAAC,OA+K/B,CAAC;AAEF,eAAe,iBAAiB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/ProfileFlameChart/index.tsx"],"names":[],"mappings":"AAeA,OAAO,EAAoC,kBAAkB,EAAC,MAAM,eAAe,CAAC;AAQpF,OAAO,EAAwB,WAAW,EAAQ,MAAM,eAAe,CAAC;AAKxE,OAAO,EAAsB,aAAa,EAAC,MAAM,kBAAkB,CAAC;AACpE,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,oCAAoC,CAAC;AA4CpE,UAAU,sBAAsB;IAC9B,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,WAAW,EAAE,kBAAkB,CAAC;IAChC,aAAa,EAAE,aAAa,CAAC;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,YAAY,EAAE,OAAO,CAAC;IACtB,oBAAoB,CAAC,EAAE,MAAM,EAAE,CAAC;IAChC,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,mBAAmB,CAAC,EAAE,MAAM,IAAI,CAAC;CAClC;AA+BD,eAAO,MAAM,iBAAiB,GAAI,6JAY/B,sBAAsB,KAAG,GAAG,CAAC,OAqM/B,CAAC;AAEF,eAAe,iBAAiB,CAAC"}
@@ -15,6 +15,7 @@ import { useEffect, useMemo, useRef } from 'react';
15
15
  import { QueryRequest_ReportType } from '@parca/client';
16
16
  import { Button, useParcaContext, useURLState, useURLStateCustom, } from '@parca/components';
17
17
  import { Matcher, MatcherTypes, Query } from '@parca/parser';
18
+ import { TimeUnits, formatDateTimeDownToMS, formatDuration } from '@parca/utilities';
18
19
  import ProfileFlameGraph, { validateFlameChartQuery } from '../ProfileFlameGraph';
19
20
  import { boundsFromProfileSource } from '../ProfileFlameGraph/FlameGraphArrow/utils';
20
21
  import { MergedProfileSource } from '../ProfileSource';
@@ -71,6 +72,7 @@ const createFilteredProfileSource = (profileSource, selectedTimeframe) => {
71
72
  };
72
73
  export const ProfileFlameChart = ({ samplesData, queryClient, profileSource, width, total, filtered, profileType, isHalfScreen, metadataMappingFiles, metadataLoading, onSwitchToOneMinute, }) => {
73
74
  const { loader } = useParcaContext();
75
+ const zoomControlsRef = useRef(null);
74
76
  const [selectedTimeframe, setSelectedTimeframe] = useURLStateCustom('flamechart_timeframe', TimeframeStateSerializer);
75
77
  // Read flamechart dimension from URL state to detect changes
76
78
  const [flamechartDimension] = useURLState('flamechart_dimension', {
@@ -150,6 +152,14 @@ export const ProfileFlameChart = ({ samplesData, queryClient, profileSource, wid
150
152
  if (samplesData?.loading === true) {
151
153
  return _jsx(_Fragment, { children: loader });
152
154
  }
153
- return (_jsxs("div", { children: [stripsData.cpus.length > 0 && stripsData.data.length > 0 && (_jsx("div", { className: "mb-2", children: _jsx(SamplesStrip, { cpus: stripsData.cpus, data: stripsData.data, selectedTimeframe: selectedTimeframe, onSelectedTimeframe: handleSelectedTimeframe, width: width, bounds: [Number(timeBounds[0] / 1000000n), Number(timeBounds[1] / 1000000n)], stepMs: stripsData.stepMs }) })), selectedTimeframe != null && filteredProfileSource != null ? (_jsx(ProfileFlameGraph, { arrow: flamechartArrow, loading: flamechartLoading, error: flamechartError, profileSource: filteredProfileSource, width: width, total: flamechartTotal, filtered: flamechartFiltered, profileType: profileType, isHalfScreen: isHalfScreen, metadataMappingFiles: metadataMappingFiles, metadataLoading: metadataLoading, isFlameChart: true, curPathArrow: [], setNewCurPathArrow: () => { } })) : (_jsx("div", { className: "flex justify-center items-center py-10 text-gray-500 dark:text-gray-400 text-sm", children: "Select a time range in the samples strips above to view the flamechart." }))] }));
155
+ return (_jsxs("div", { children: [stripsData.cpus.length > 0 && stripsData.data.length > 0 && (_jsx("div", { className: "mb-2", children: _jsx(SamplesStrip, { cpus: stripsData.cpus, data: stripsData.data, selectedTimeframe: selectedTimeframe, onSelectedTimeframe: handleSelectedTimeframe, width: width, bounds: [Number(timeBounds[0] / 1000000n), Number(timeBounds[1] / 1000000n)], stepMs: stripsData.stepMs }) })), selectedTimeframe != null &&
156
+ (() => {
157
+ const labels = selectedTimeframe.labels.labels
158
+ .map(l => `${l.name} = ${l.value}`)
159
+ .join(', ');
160
+ const durationMs = selectedTimeframe.bounds[1] - selectedTimeframe.bounds[0];
161
+ const duration = formatDuration({ [TimeUnits.Milliseconds]: durationMs });
162
+ return (_jsxs("div", { className: "flex items-center justify-between px-2 py-1", children: [_jsxs("div", { className: "text-xs font-medium text-gray-500 dark:text-gray-400", children: ["Samples matching ", labels, " over ", duration, " from", ' ', formatDateTimeDownToMS(selectedTimeframe.bounds[0]), " to", ' ', formatDateTimeDownToMS(selectedTimeframe.bounds[1])] }), _jsx("div", { ref: zoomControlsRef })] }));
163
+ })(), selectedTimeframe != null && filteredProfileSource != null ? (_jsx(ProfileFlameGraph, { arrow: flamechartArrow, loading: flamechartLoading, error: flamechartError, profileSource: filteredProfileSource, width: width, total: flamechartTotal, filtered: flamechartFiltered, profileType: profileType, isHalfScreen: isHalfScreen, metadataMappingFiles: metadataMappingFiles, metadataLoading: metadataLoading, isFlameChart: true, curPathArrow: [], setNewCurPathArrow: () => { }, zoomControlsRef: zoomControlsRef })) : (_jsx("div", { className: "flex justify-center items-center py-10 text-gray-500 dark:text-gray-400 text-sm", children: "Select a time range in the samples strips above to view the flamechart." }))] }));
154
164
  };
155
165
  export default ProfileFlameChart;
@@ -0,0 +1,20 @@
1
+ import React from 'react';
2
+ import { Table } from '@uwdata/flechette';
3
+ import { ProfileSource } from '../../ProfileSource';
4
+ import { type colorByColors } from './FlameGraphNodes';
5
+ interface MiniMapProps {
6
+ containerRef: React.RefObject<HTMLDivElement | null>;
7
+ table: Table;
8
+ width: number;
9
+ zoomedWidth: number;
10
+ totalHeight: number;
11
+ maxDepth: number;
12
+ colorByColors: colorByColors;
13
+ colorBy: string;
14
+ profileSource: ProfileSource;
15
+ isDarkMode: boolean;
16
+ scrollLeft: number;
17
+ }
18
+ export declare const MiniMap: React.NamedExoticComponent<MiniMapProps>;
19
+ export {};
20
+ //# sourceMappingURL=MiniMap.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MiniMap.d.ts","sourceRoot":"","sources":["../../../src/ProfileFlameGraph/FlameGraphArrow/MiniMap.tsx"],"names":[],"mappings":"AAaA,OAAO,KAAuC,MAAM,OAAO,CAAC;AAE5D,OAAO,EAAC,KAAK,EAAC,MAAM,mBAAmB,CAAC;AAKxC,OAAO,EAAC,aAAa,EAAC,MAAM,qBAAqB,CAAC;AAClD,OAAO,EAAY,KAAK,aAAa,EAAC,MAAM,mBAAmB,CAAC;AAYhE,UAAU,YAAY;IACpB,YAAY,EAAE,KAAK,CAAC,SAAS,CAAC,cAAc,GAAG,IAAI,CAAC,CAAC;IACrD,KAAK,EAAE,KAAK,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,aAAa,CAAC;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,aAAa,CAAC;IAC7B,UAAU,EAAE,OAAO,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,eAAO,MAAM,OAAO,0CA8NlB,CAAC"}
@@ -0,0 +1,173 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // Copyright 2022 The Parca Authors
3
+ // Licensed under the Apache License, Version 2.0 (the "License");
4
+ // you may not use this file except in compliance with the License.
5
+ // You may obtain a copy of the License at
6
+ //
7
+ // http://www.apache.org/licenses/LICENSE-2.0
8
+ //
9
+ // Unless required by applicable law or agreed to in writing, software
10
+ // distributed under the License is distributed on an "AS IS" BASIS,
11
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ // See the License for the specific language governing permissions and
13
+ // limitations under the License.
14
+ import React, { useCallback, useEffect, useRef } from 'react';
15
+ import { EVERYTHING_ELSE } from '@parca/store';
16
+ import { getLastItem } from '@parca/utilities';
17
+ import { RowHeight } from './FlameGraphNodes';
18
+ import { FIELD_CUMULATIVE, FIELD_DEPTH, FIELD_FUNCTION_FILE_NAME, FIELD_MAPPING_FILE, FIELD_TIMESTAMP, } from './index';
19
+ import { arrowToString, boundsFromProfileSource } from './utils';
20
+ const MINIMAP_HEIGHT = 20;
21
+ export const MiniMap = React.memo(function MiniMap({ containerRef, table, width, zoomedWidth, totalHeight, maxDepth, colorByColors: colors, colorBy, profileSource, isDarkMode, scrollLeft, }) {
22
+ const canvasRef = useRef(null);
23
+ const containerElRef = useRef(null);
24
+ const isDragging = useRef(false);
25
+ const dragStartX = useRef(0);
26
+ const dragStartScrollLeft = useRef(0);
27
+ // Render minimap canvas
28
+ useEffect(() => {
29
+ const canvas = canvasRef.current;
30
+ if (canvas == null || width <= 0 || zoomedWidth <= 0)
31
+ return;
32
+ const dpr = window.devicePixelRatio !== 0 ? window.devicePixelRatio : 1;
33
+ canvas.width = width * dpr;
34
+ canvas.height = MINIMAP_HEIGHT * dpr;
35
+ const ctx = canvas.getContext('2d');
36
+ if (ctx == null)
37
+ return;
38
+ ctx.scale(dpr, dpr);
39
+ ctx.clearRect(0, 0, width, MINIMAP_HEIGHT);
40
+ // Background
41
+ ctx.fillStyle = isDarkMode ? '#374151' : '#f3f4f6';
42
+ ctx.fillRect(0, 0, width, MINIMAP_HEIGHT);
43
+ const xScale = width / zoomedWidth;
44
+ const yScale = MINIMAP_HEIGHT / totalHeight;
45
+ const tsBounds = boundsFromProfileSource(profileSource);
46
+ const tsRange = Number(tsBounds[1]) - Number(tsBounds[0]);
47
+ if (tsRange <= 0)
48
+ return;
49
+ const depthCol = table.getChild(FIELD_DEPTH);
50
+ const cumulativeCol = table.getChild(FIELD_CUMULATIVE);
51
+ const tsCol = table.getChild(FIELD_TIMESTAMP);
52
+ const mappingCol = table.getChild(FIELD_MAPPING_FILE);
53
+ const filenameCol = table.getChild(FIELD_FUNCTION_FILE_NAME);
54
+ if (depthCol == null || cumulativeCol == null)
55
+ return;
56
+ const numRows = table.numRows;
57
+ for (let row = 0; row < numRows; row++) {
58
+ const depth = depthCol.get(row) ?? 0;
59
+ if (depth === 0)
60
+ continue; // skip root
61
+ if (depth > maxDepth)
62
+ continue;
63
+ const cumulative = Number(cumulativeCol.get(row) ?? 0n);
64
+ if (cumulative <= 0)
65
+ continue;
66
+ const nodeWidth = (cumulative / tsRange) * zoomedWidth * xScale;
67
+ if (nodeWidth < 0.5)
68
+ continue;
69
+ const ts = tsCol != null ? Number(tsCol.get(row)) : 0;
70
+ const x = ((ts - Number(tsBounds[0])) / tsRange) * zoomedWidth * xScale;
71
+ const y = (depth - 1) * RowHeight * yScale;
72
+ const h = Math.max(1, RowHeight * yScale);
73
+ // Get color using same logic as useNodeColor
74
+ const colorAttribute = colorBy === 'filename'
75
+ ? arrowToString(filenameCol?.get(row))
76
+ : colorBy === 'binary'
77
+ ? arrowToString(mappingCol?.get(row))
78
+ : null;
79
+ const color = colors[getLastItem(colorAttribute ?? '') ?? EVERYTHING_ELSE];
80
+ ctx.fillStyle = color ?? (isDarkMode ? '#6b7280' : '#9ca3af');
81
+ ctx.fillRect(x, y, Math.max(0.5, nodeWidth), h);
82
+ }
83
+ }, [
84
+ table,
85
+ width,
86
+ zoomedWidth,
87
+ totalHeight,
88
+ maxDepth,
89
+ colorBy,
90
+ colors,
91
+ isDarkMode,
92
+ profileSource,
93
+ ]);
94
+ const isZoomed = zoomedWidth > width;
95
+ const sliderWidth = Math.max(20, (width / zoomedWidth) * width);
96
+ const sliderLeft = Math.min((scrollLeft / zoomedWidth) * width, width - sliderWidth);
97
+ const handleMouseDown = useCallback((e) => {
98
+ e.preventDefault();
99
+ const rect = containerElRef.current?.getBoundingClientRect();
100
+ if (rect == null)
101
+ return;
102
+ const clickX = e.clientX - rect.left;
103
+ // Check if clicking inside the slider
104
+ if (clickX >= sliderLeft && clickX <= sliderLeft + sliderWidth) {
105
+ // Start dragging
106
+ isDragging.current = true;
107
+ dragStartX.current = e.clientX;
108
+ dragStartScrollLeft.current = scrollLeft;
109
+ }
110
+ else {
111
+ // Click-to-jump: center viewport at click position
112
+ const targetCenter = (clickX / width) * zoomedWidth;
113
+ const containerWidth = containerRef.current?.clientWidth ?? width;
114
+ const newScrollLeft = targetCenter - containerWidth / 2;
115
+ if (containerRef.current != null) {
116
+ containerRef.current.scrollLeft = Math.max(0, Math.min(newScrollLeft, zoomedWidth - containerWidth));
117
+ }
118
+ // Also start dragging from new position
119
+ isDragging.current = true;
120
+ dragStartX.current = e.clientX;
121
+ dragStartScrollLeft.current = containerRef.current?.scrollLeft ?? 0;
122
+ }
123
+ const handleMouseMove = (moveEvent) => {
124
+ if (!isDragging.current)
125
+ return;
126
+ const delta = moveEvent.clientX - dragStartX.current;
127
+ const scrollDelta = delta * (zoomedWidth / width);
128
+ const containerWidth = containerRef.current?.clientWidth ?? width;
129
+ if (containerRef.current != null) {
130
+ containerRef.current.scrollLeft = Math.max(0, Math.min(dragStartScrollLeft.current + scrollDelta, zoomedWidth - containerWidth));
131
+ }
132
+ };
133
+ const handleMouseUp = () => {
134
+ isDragging.current = false;
135
+ document.removeEventListener('mousemove', handleMouseMove);
136
+ document.removeEventListener('mouseup', handleMouseUp);
137
+ };
138
+ document.addEventListener('mousemove', handleMouseMove);
139
+ document.addEventListener('mouseup', handleMouseUp);
140
+ }, [sliderLeft, sliderWidth, scrollLeft, width, zoomedWidth, containerRef]);
141
+ // Forward wheel events to the container so zoom (Ctrl+scroll) works on the minimap
142
+ useEffect(() => {
143
+ const el = containerElRef.current;
144
+ if (el == null)
145
+ return;
146
+ const handleWheel = (e) => {
147
+ if (!e.ctrlKey && !e.metaKey)
148
+ return;
149
+ e.preventDefault();
150
+ containerRef.current?.dispatchEvent(new WheelEvent('wheel', {
151
+ deltaY: e.deltaY,
152
+ deltaX: e.deltaX,
153
+ ctrlKey: e.ctrlKey,
154
+ metaKey: e.metaKey,
155
+ clientX: e.clientX,
156
+ clientY: e.clientY,
157
+ bubbles: true,
158
+ }));
159
+ };
160
+ el.addEventListener('wheel', handleWheel, { passive: false });
161
+ return () => {
162
+ el.removeEventListener('wheel', handleWheel);
163
+ };
164
+ }, [containerRef]);
165
+ if (width <= 0)
166
+ return null;
167
+ return (_jsxs("div", { ref: containerElRef, className: "relative select-none", style: { width, height: MINIMAP_HEIGHT, cursor: isZoomed ? 'pointer' : 'default' }, onMouseDown: isZoomed ? handleMouseDown : undefined, children: [_jsx("canvas", { ref: canvasRef, style: {
168
+ width,
169
+ height: MINIMAP_HEIGHT,
170
+ display: 'block',
171
+ visibility: isZoomed ? 'visible' : 'hidden',
172
+ } }), isZoomed && (_jsxs(_Fragment, { children: [_jsx("div", { className: "absolute top-0 bottom-0 bg-black/30 dark:bg-black/50", style: { left: 0, width: Math.max(0, sliderLeft) } }), _jsx("div", { className: "absolute top-0 bottom-0 border-x-2 border-gray-500", style: { left: sliderLeft, width: sliderWidth } }), _jsx("div", { className: "absolute top-0 bottom-0 bg-black/30 dark:bg-black/50", style: { left: sliderLeft + sliderWidth, right: 0 } })] }))] }));
173
+ });
@@ -0,0 +1,11 @@
1
+ import React from 'react';
2
+ interface ZoomControlsProps {
3
+ zoomLevel: number;
4
+ zoomIn: () => void;
5
+ zoomOut: () => void;
6
+ resetZoom: () => void;
7
+ portalRef?: React.RefObject<HTMLDivElement | null>;
8
+ }
9
+ export declare const ZoomControls: ({ zoomLevel, zoomIn, zoomOut, resetZoom, portalRef, }: ZoomControlsProps) => React.JSX.Element;
10
+ export {};
11
+ //# sourceMappingURL=ZoomControls.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ZoomControls.d.ts","sourceRoot":"","sources":["../../../src/ProfileFlameGraph/FlameGraphArrow/ZoomControls.tsx"],"names":[],"mappings":"AAaA,OAAO,KAAK,MAAM,OAAO,CAAC;AAK1B,UAAU,iBAAiB;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,IAAI,CAAC;IACnB,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,SAAS,EAAE,MAAM,IAAI,CAAC;IACtB,SAAS,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC,cAAc,GAAG,IAAI,CAAC,CAAC;CACpD;AAED,eAAO,MAAM,YAAY,GAAI,uDAM1B,iBAAiB,KAAG,KAAK,CAAC,GAAG,CAAC,OAkChC,CAAC"}
@@ -0,0 +1,10 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Icon } from '@iconify/react';
3
+ import { createPortal } from 'react-dom';
4
+ export const ZoomControls = ({ zoomLevel, zoomIn, zoomOut, resetZoom, portalRef, }) => {
5
+ const controls = (_jsxs("div", { className: "flex items-center gap-1 rounded-md border border-gray-200 bg-white/90 px-1 py-0.5 shadow-sm backdrop-blur-sm dark:border-gray-600 dark:bg-gray-800/90", children: [_jsx("button", { onClick: zoomOut, disabled: zoomLevel <= 1, className: "rounded p-1 text-gray-600 hover:bg-gray-100 disabled:opacity-30 dark:text-gray-300 dark:hover:bg-gray-700", title: "Zoom out", children: _jsx(Icon, { icon: "mdi:minus", width: 16, height: 16 }) }), _jsxs("button", { onClick: resetZoom, className: "min-w-[3rem] px-1 text-center text-xs text-gray-600 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700 rounded", title: "Reset zoom", children: [Math.round(zoomLevel * 100), "%"] }), _jsx("button", { onClick: zoomIn, disabled: zoomLevel >= 20, className: "rounded p-1 text-gray-600 hover:bg-gray-100 disabled:opacity-30 dark:text-gray-300 dark:hover:bg-gray-700", title: "Zoom in", children: _jsx(Icon, { icon: "mdi:plus", width: 16, height: 16 }) })] }));
6
+ if (portalRef?.current != null) {
7
+ return createPortal(controls, portalRef.current);
8
+ }
9
+ return controls;
10
+ };
@@ -46,6 +46,7 @@ interface FlameGraphArrowProps {
46
46
  tooltipId?: string;
47
47
  maxFrameCount?: number;
48
48
  isExpanded?: boolean;
49
+ zoomControlsRef?: React.RefObject<HTMLDivElement | null>;
49
50
  }
50
51
  export declare const getMappingColors: (mappingsList: string[], isDarkMode: boolean, currentColorProfile: ColorConfig) => colorByColors;
51
52
  export declare const getFilenameColors: (filenamesList: string[], isDarkMode: boolean, currentColorProfile: ColorConfig) => colorByColors;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/ProfileFlameGraph/FlameGraphArrow/index.tsx"],"names":[],"mappings":"AAaA,OAAO,KAQN,MAAM,OAAO,CAAC;AAKf,OAAO,EAAC,eAAe,EAAC,MAAM,eAAe,CAAC;AAG9C,OAAO,EAAC,WAAW,EAAC,MAAM,eAAe,CAAC;AAE1C,OAAO,EAAC,KAAK,WAAW,EAAC,MAAM,kBAAkB,CAAC;AAElD,OAAO,EAAC,aAAa,EAAC,MAAM,qBAAqB,CAAC;AAKlD,OAAO,EAAuB,aAAa,EAAC,MAAM,mBAAmB,CAAC;AAMtE,OAAO,EACL,gBAAgB,EAMjB,MAAM,SAAS,CAAC;AAEjB,eAAO,MAAM,iBAAiB,gBAAgB,CAAC;AAC/C,eAAO,MAAM,kBAAkB,iBAAiB,CAAC;AACjD,eAAO,MAAM,sBAAsB,qBAAqB,CAAC;AACzD,eAAO,MAAM,sBAAsB,qBAAqB,CAAC;AACzD,eAAO,MAAM,mBAAmB,kBAAkB,CAAC;AACnD,eAAO,MAAM,aAAa,YAAY,CAAC;AACvC,eAAO,MAAM,eAAe,cAAc,CAAC;AAC3C,eAAO,MAAM,cAAc,aAAa,CAAC;AACzC,eAAO,MAAM,sBAAsB,qBAAqB,CAAC;AACzD,eAAO,MAAM,mBAAmB,kBAAkB,CAAC;AACnD,eAAO,MAAM,0BAA0B,yBAAyB,CAAC;AACjE,eAAO,MAAM,wBAAwB,uBAAuB,CAAC;AAC7D,eAAO,MAAM,yBAAyB,uBAAuB,CAAC;AAC9D,eAAO,MAAM,cAAc,aAAa,CAAC;AACzC,eAAO,MAAM,YAAY,WAAW,CAAC;AACrC,eAAO,MAAM,gBAAgB,eAAe,CAAC;AAC7C,eAAO,MAAM,UAAU,SAAS,CAAC;AACjC,eAAO,MAAM,UAAU,SAAS,CAAC;AACjC,eAAO,MAAM,YAAY,WAAW,CAAC;AACrC,eAAO,MAAM,WAAW,UAAU,CAAC;AACnC,eAAO,MAAM,kBAAkB,iBAAiB,CAAC;AAEjD,UAAU,oBAAoB;IAC5B,KAAK,EAAE,eAAe,CAAC;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,aAAa,EAAE,aAAa,CAAC;IAC7B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,gBAAgB,EAAE,CAAC;IAC5B,UAAU,EAAE,CAAC,IAAI,EAAE,gBAAgB,EAAE,KAAK,IAAI,CAAC;IAC/C,YAAY,EAAE,OAAO,CAAC;IACtB,wBAAwB,EAAE,MAAM,EAAE,CAAC;IACnC,yBAAyB,EAAE,MAAM,EAAE,CAAC;IACpC,OAAO,EAAE,MAAM,CAAC;IAChB,eAAe,EAAE,OAAO,CAAC;IACzB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,sBAAsB,CAAC,EAAE,OAAO,CAAC;IACjC,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED,eAAO,MAAM,gBAAgB,GAC3B,cAAc,MAAM,EAAE,EACtB,YAAY,OAAO,EACnB,qBAAqB,WAAW,KAC/B,aAQF,CAAC;AAEF,eAAO,MAAM,iBAAiB,GAC5B,eAAe,MAAM,EAAE,EACvB,YAAY,OAAO,EACnB,qBAAqB,WAAW,KAC/B,aAQF,CAAC;AAIF,eAAO,MAAM,eAAe,kDAkR1B,CAAC;AAEH,eAAe,eAAe,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/ProfileFlameGraph/FlameGraphArrow/index.tsx"],"names":[],"mappings":"AAaA,OAAO,KAQN,MAAM,OAAO,CAAC;AAKf,OAAO,EAAC,eAAe,EAAC,MAAM,eAAe,CAAC;AAG9C,OAAO,EAAC,WAAW,EAAC,MAAM,eAAe,CAAC;AAE1C,OAAO,EAAC,KAAK,WAAW,EAAC,MAAM,kBAAkB,CAAC;AAElD,OAAO,EAAC,aAAa,EAAC,MAAM,qBAAqB,CAAC;AAMlD,OAAO,EAAuB,aAAa,EAAC,MAAM,mBAAmB,CAAC;AAStE,OAAO,EACL,gBAAgB,EAOjB,MAAM,SAAS,CAAC;AAEjB,eAAO,MAAM,iBAAiB,gBAAgB,CAAC;AAC/C,eAAO,MAAM,kBAAkB,iBAAiB,CAAC;AACjD,eAAO,MAAM,sBAAsB,qBAAqB,CAAC;AACzD,eAAO,MAAM,sBAAsB,qBAAqB,CAAC;AACzD,eAAO,MAAM,mBAAmB,kBAAkB,CAAC;AACnD,eAAO,MAAM,aAAa,YAAY,CAAC;AACvC,eAAO,MAAM,eAAe,cAAc,CAAC;AAC3C,eAAO,MAAM,cAAc,aAAa,CAAC;AACzC,eAAO,MAAM,sBAAsB,qBAAqB,CAAC;AACzD,eAAO,MAAM,mBAAmB,kBAAkB,CAAC;AACnD,eAAO,MAAM,0BAA0B,yBAAyB,CAAC;AACjE,eAAO,MAAM,wBAAwB,uBAAuB,CAAC;AAC7D,eAAO,MAAM,yBAAyB,uBAAuB,CAAC;AAC9D,eAAO,MAAM,cAAc,aAAa,CAAC;AACzC,eAAO,MAAM,YAAY,WAAW,CAAC;AACrC,eAAO,MAAM,gBAAgB,eAAe,CAAC;AAC7C,eAAO,MAAM,UAAU,SAAS,CAAC;AACjC,eAAO,MAAM,UAAU,SAAS,CAAC;AACjC,eAAO,MAAM,YAAY,WAAW,CAAC;AACrC,eAAO,MAAM,WAAW,UAAU,CAAC;AACnC,eAAO,MAAM,kBAAkB,iBAAiB,CAAC;AAEjD,UAAU,oBAAoB;IAC5B,KAAK,EAAE,eAAe,CAAC;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,aAAa,EAAE,aAAa,CAAC;IAC7B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,gBAAgB,EAAE,CAAC;IAC5B,UAAU,EAAE,CAAC,IAAI,EAAE,gBAAgB,EAAE,KAAK,IAAI,CAAC;IAC/C,YAAY,EAAE,OAAO,CAAC;IACtB,wBAAwB,EAAE,MAAM,EAAE,CAAC;IACnC,yBAAyB,EAAE,MAAM,EAAE,CAAC;IACpC,OAAO,EAAE,MAAM,CAAC;IAChB,eAAe,EAAE,OAAO,CAAC;IACzB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,sBAAsB,CAAC,EAAE,OAAO,CAAC;IACjC,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,eAAe,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC,cAAc,GAAG,IAAI,CAAC,CAAC;CAC1D;AAED,eAAO,MAAM,gBAAgB,GAC3B,cAAc,MAAM,EAAE,EACtB,YAAY,OAAO,EACnB,qBAAqB,WAAW,KAC/B,aAQF,CAAC;AAEF,eAAO,MAAM,iBAAiB,GAC5B,eAAe,MAAM,EAAE,EACvB,YAAY,OAAO,EACnB,qBAAqB,WAAW,KAC/B,aAQF,CAAC;AAIF,eAAO,MAAM,eAAe,kDAuU1B,CAAC;AAEH,eAAe,eAAe,CAAC"}
@@ -16,18 +16,22 @@ import { tableFromIPC } from '@uwdata/flechette';
16
16
  import { useContextMenu } from 'react-contexify';
17
17
  import { FlameGraphSkeleton, SandwichFlameGraphSkeleton, useParcaContext } from '@parca/components';
18
18
  import { USER_PREFERENCES, useCurrentColorProfile, useUserPreference } from '@parca/hooks';
19
- import { getColorForFeature, selectDarkMode, useAppSelector } from '@parca/store';
19
+ import { getColorForFeature } from '@parca/store';
20
20
  import { useProfileFilters } from '../../ProfileView/components/ProfileFilters/useProfileFilters';
21
21
  import { useProfileViewContext } from '../../ProfileView/context/ProfileViewContext';
22
+ import { TimelineGuide } from '../../TimelineGuide';
22
23
  import { alignedUint8Array } from '../../utils';
23
24
  import ContextMenuWrapper from './ContextMenuWrapper';
24
25
  import { FlameNode, RowHeight } from './FlameGraphNodes';
25
26
  import { MemoizedTooltip } from './MemoizedTooltip';
27
+ import { MiniMap } from './MiniMap';
26
28
  import { TooltipProvider } from './TooltipContext';
29
+ import { ZoomControls } from './ZoomControls';
27
30
  import { useBatchedRendering } from './useBatchedRendering';
28
31
  import { useScrollViewport } from './useScrollViewport';
29
32
  import { useVisibleNodes } from './useVisibleNodes';
30
- import { extractFeature, extractFilenameFeature, getCurrentPathFrameData, getMaxDepth, isCurrentPathFrameMatch, } from './utils';
33
+ import { useZoom } from './useZoom';
34
+ import { boundsFromProfileSource, extractFeature, extractFilenameFeature, getCurrentPathFrameData, getMaxDepth, isCurrentPathFrameMatch, } from './utils';
31
35
  export const FIELD_LABELS_ONLY = 'labels_only';
32
36
  export const FIELD_MAPPING_FILE = 'mapping_file';
33
37
  export const FIELD_MAPPING_BUILD_ID = 'mapping_build_id';
@@ -66,12 +70,11 @@ export const getFilenameColors = (filenamesList, isDarkMode, currentColorProfile
66
70
  return colors;
67
71
  };
68
72
  const noop = () => { };
69
- export const FlameGraphArrow = memo(function FlameGraphArrow({ arrow, total, filtered, width, setCurPath, curPath, profileType, profileSource, compareAbsolute, isFlameChart = false, isRenderedAsFlamegraph = false, isInSandwichView = false, isHalfScreen, tooltipId = 'default', maxFrameCount, isExpanded = false, mappingsListFromMetadata, filenamesListFromMetadata, colorBy, }) {
73
+ export const FlameGraphArrow = memo(function FlameGraphArrow({ arrow, total, filtered, width, setCurPath, curPath, profileType, profileSource, compareAbsolute, isFlameChart = false, isRenderedAsFlamegraph = false, isInSandwichView = false, isHalfScreen, tooltipId = 'default', maxFrameCount, isExpanded = false, mappingsListFromMetadata, filenamesListFromMetadata, colorBy, zoomControlsRef, }) {
70
74
  const [highlightSimilarStacksPreference] = useUserPreference(USER_PREFERENCES.HIGHLIGHT_SIMILAR_STACKS.key);
71
75
  const [hoveringRow, setHoveringRow] = useState(undefined);
72
76
  const [dockedMetainfo] = useUserPreference(USER_PREFERENCES.GRAPH_METAINFO_DOCKED.key);
73
- const isDarkMode = useAppSelector(selectDarkMode);
74
- const { perf } = useParcaContext();
77
+ const { perf, isDarkMode } = useParcaContext();
75
78
  const table = useMemo(() => {
76
79
  const result = tableFromIPC(alignedUint8Array(arrow.record), { useBigInt: true });
77
80
  if (perf?.setMeasurement != null) {
@@ -150,6 +153,13 @@ export const FlameGraphArrow = memo(function FlameGraphArrow({ arrow, total, fil
150
153
  : (deferredEffectiveDepth + 1) * RowHeight;
151
154
  // Get the viewport of the container, this is used to determine which rows are visible.
152
155
  const viewport = useScrollViewport(containerRef);
156
+ const isZoomEnabled = isFlameChart;
157
+ const { zoomLevel, zoomIn, zoomOut, resetZoom } = useZoom(isZoomEnabled ? containerRef : { current: null });
158
+ const zoomedWidth = isZoomEnabled ? Math.round((width ?? 1) * zoomLevel) : width ?? 0;
159
+ // Reset zoom when the data changes (e.g. new query, different time range)
160
+ useEffect(() => {
161
+ resetZoom();
162
+ }, [table, resetZoom]);
153
163
  // To find the selected row, we must walk the current path and look at which
154
164
  // children of the current frame matches the path element exactly. Until the
155
165
  // end, the row we find at the end is our selected row.
@@ -178,7 +188,7 @@ export const FlameGraphArrow = memo(function FlameGraphArrow({ arrow, total, fil
178
188
  table,
179
189
  viewport,
180
190
  total,
181
- width: width ?? 1,
191
+ width: zoomedWidth,
182
192
  selectedRow,
183
193
  effectiveDepth: deferredEffectiveDepth,
184
194
  });
@@ -199,10 +209,11 @@ export const FlameGraphArrow = memo(function FlameGraphArrow({ arrow, total, fil
199
209
  useEffect(() => {
200
210
  setSvgElement(svg.current);
201
211
  }, [tooltipId]);
202
- return (_jsx(TooltipProvider, { table: table, total: total, totalUnfiltered: total + filtered, profileType: profileType, unit: arrow.unit, compareAbsolute: compareAbsolute, tooltipId: tooltipId, children: _jsxs("div", { className: "relative", children: [_jsx(ContextMenuWrapper, { ref: contextMenuRef, menuId: MENU_ID, table: table, total: total, totalUnfiltered: total + filtered, compareAbsolute: compareAbsolute, resetPath: () => setCurPath([]), hideMenu: hideAll, hideBinary: hideBinary, unit: arrow.unit, profileType: profileType, isInSandwichView: isInSandwichView }), _jsx(MemoizedTooltip, { contextElement: svgElement, dockedMetainfo: dockedMetainfo }), showSkeleton && (_jsx("div", { className: "absolute inset-0 z-10", children: isRenderedAsFlamegraph ? (_jsx(SandwichFlameGraphSkeleton, { isHalfScreen: isHalfScreen, isDarkMode: isDarkMode })) : (_jsx(FlameGraphSkeleton, { isHalfScreen: isHalfScreen, isDarkMode: isDarkMode })) })), _jsx("div", { ref: containerRef, 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", style: {
212
+ return (_jsx(TooltipProvider, { table: table, total: total, totalUnfiltered: total + filtered, profileType: profileType, unit: arrow.unit, compareAbsolute: compareAbsolute, tooltipId: tooltipId, children: _jsxs("div", { className: "relative", children: [isZoomEnabled && (_jsx(ZoomControls, { zoomLevel: zoomLevel, zoomIn: zoomIn, zoomOut: zoomOut, resetZoom: resetZoom, portalRef: zoomControlsRef })), _jsx(ContextMenuWrapper, { ref: contextMenuRef, menuId: MENU_ID, table: table, total: total, totalUnfiltered: total + filtered, compareAbsolute: compareAbsolute, resetPath: () => setCurPath([]), hideMenu: hideAll, hideBinary: hideBinary, unit: arrow.unit, profileType: profileType, isInSandwichView: isInSandwichView }), _jsx(MemoizedTooltip, { contextElement: svgElement, dockedMetainfo: dockedMetainfo }), showSkeleton && (_jsx("div", { className: "absolute inset-0 z-10", children: isRenderedAsFlamegraph ? (_jsx(SandwichFlameGraphSkeleton, { isHalfScreen: isHalfScreen, isDarkMode: isDarkMode })) : (_jsx(FlameGraphSkeleton, { isHalfScreen: isHalfScreen, isDarkMode: isDarkMode })) })), isZoomEnabled && (_jsx(MiniMap, { containerRef: containerRef, table: table, width: width ?? 0, zoomedWidth: zoomedWidth, totalHeight: totalHeight, maxDepth: deferredEffectiveDepth, colorByColors: colorByColors, colorBy: colorByValue, profileSource: profileSource, isDarkMode: isDarkMode, scrollLeft: viewport.scrollLeft })), _jsx("div", { ref: containerRef, className: `${isZoomEnabled ? '[scrollbar-width:none] [&::-webkit-scrollbar]:hidden' : ''} will-change-transform webkit-overflow-scrolling-touch contain ${!isZoomEnabled ? 'overflow-auto' : ''}`, style: {
203
213
  width: width ?? '100%',
214
+ ...(isZoomEnabled ? { overflowX: 'scroll', overflowY: 'auto' } : {}),
204
215
  contain: 'layout style paint',
205
216
  visibility: !showSkeleton ? 'visible' : 'hidden',
206
- }, children: _jsx("svg", { className: "font-robotoMono", width: width ?? 0, height: totalHeight, preserveAspectRatio: "xMinYMid", ref: svg, children: batchedNodes.map(row => (_jsx(FlameNode, { table: table, row: row, colors: colorByColors, colorBy: colorByValue, totalWidth: width ?? 1, height: RowHeight, darkMode: isDarkMode, compareMode: compareMode, colorForSimilarNodes: colorForSimilarNodes, selectedRow: selectedRow, onClick: () => handleRowClick(row), onContextMenu: displayMenu, hoveringRow: highlightSimilarStacksPreference ? hoveringRow : undefined, setHoveringRow: highlightSimilarStacksPreference ? setHoveringRow : noop, isFlameChart: isFlameChart, profileSource: profileSource, isRenderedAsFlamegraph: isRenderedAsFlamegraph, isInSandwichView: isInSandwichView, maxDepth: maxDepth, effectiveDepth: deferredEffectiveDepth, tooltipId: tooltipId }, row))) }) })] }) }));
217
+ }, children: _jsxs("div", { children: [isFlameChart && (_jsx(TimelineGuide, { bounds: boundsFromProfileSource(profileSource), width: zoomedWidth, height: totalHeight, margin: 0, ticks: 12, timeUnit: "nanoseconds" })), _jsx("svg", { className: "relative font-robotoMono", width: zoomedWidth, height: totalHeight, preserveAspectRatio: "xMinYMid", ref: svg, children: batchedNodes.map(row => (_jsx(FlameNode, { table: table, row: row, colors: colorByColors, colorBy: colorByValue, totalWidth: zoomedWidth, height: RowHeight, darkMode: isDarkMode, compareMode: compareMode, colorForSimilarNodes: colorForSimilarNodes, selectedRow: selectedRow, onClick: () => handleRowClick(row), onContextMenu: displayMenu, hoveringRow: highlightSimilarStacksPreference ? hoveringRow : undefined, setHoveringRow: highlightSimilarStacksPreference ? setHoveringRow : noop, isFlameChart: isFlameChart, profileSource: profileSource, isRenderedAsFlamegraph: isRenderedAsFlamegraph, isInSandwichView: isInSandwichView, maxDepth: maxDepth, effectiveDepth: deferredEffectiveDepth, tooltipId: tooltipId }, row))) })] }) })] }) }));
207
218
  });
208
219
  export default FlameGraphArrow;
@@ -0,0 +1,9 @@
1
+ interface UseZoomResult {
2
+ zoomLevel: number;
3
+ zoomIn: () => void;
4
+ zoomOut: () => void;
5
+ resetZoom: () => void;
6
+ }
7
+ export declare const useZoom: (containerRef: React.RefObject<HTMLDivElement | null>) => UseZoomResult;
8
+ export {};
9
+ //# sourceMappingURL=useZoom.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useZoom.d.ts","sourceRoot":"","sources":["../../../src/ProfileFlameGraph/FlameGraphArrow/useZoom.ts"],"names":[],"mappings":"AAuBA,UAAU,aAAa;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,IAAI,CAAC;IACnB,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,SAAS,EAAE,MAAM,IAAI,CAAC;CACvB;AAMD,eAAO,MAAM,OAAO,GAAI,cAAc,KAAK,CAAC,SAAS,CAAC,cAAc,GAAG,IAAI,CAAC,KAAG,aAiF9E,CAAC"}
@@ -0,0 +1,88 @@
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
+ import { useCallback, useEffect, useRef, useState } from 'react';
14
+ import { flushSync } from 'react-dom';
15
+ const MIN_ZOOM = 1.0;
16
+ const MAX_ZOOM = 20.0;
17
+ const BUTTON_ZOOM_STEP = 1.5;
18
+ // Sensitivity for trackpad/wheel zoom - smaller = smoother
19
+ const WHEEL_ZOOM_SENSITIVITY = 0.01;
20
+ const clampZoom = (zoom) => {
21
+ return Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, zoom));
22
+ };
23
+ export const useZoom = (containerRef) => {
24
+ const [zoomLevel, setZoomLevel] = useState(MIN_ZOOM);
25
+ const zoomLevelRef = useRef(MIN_ZOOM);
26
+ // Adjust scrollLeft so the content under focalX stays fixed after zoom change.
27
+ const adjustScroll = useCallback((oldZoom, newZoom, focalX) => {
28
+ const container = containerRef.current;
29
+ if (container === null)
30
+ return;
31
+ const contentX = container.scrollLeft + focalX;
32
+ const ratio = contentX / oldZoom;
33
+ container.scrollLeft = ratio * newZoom - focalX;
34
+ }, [containerRef]);
35
+ // Apply a new zoom level around a focal point
36
+ const applyZoom = useCallback((newZoom, focalX) => {
37
+ const oldZoom = zoomLevelRef.current;
38
+ if (newZoom === oldZoom)
39
+ return;
40
+ zoomLevelRef.current = newZoom;
41
+ // flushSync ensures the DOM updates with the new content width before adjustScroll reads it
42
+ flushSync(() => setZoomLevel(newZoom));
43
+ adjustScroll(oldZoom, newZoom, focalX);
44
+ }, [adjustScroll]);
45
+ const zoomIn = useCallback(() => {
46
+ const newZoom = clampZoom(zoomLevelRef.current * BUTTON_ZOOM_STEP);
47
+ const container = containerRef.current;
48
+ applyZoom(newZoom, container !== null ? container.clientWidth / 2 : 0);
49
+ }, [containerRef, applyZoom]);
50
+ const zoomOut = useCallback(() => {
51
+ const newZoom = clampZoom(zoomLevelRef.current / BUTTON_ZOOM_STEP);
52
+ const container = containerRef.current;
53
+ applyZoom(newZoom, container !== null ? container.clientWidth / 2 : 0);
54
+ }, [containerRef, applyZoom]);
55
+ const resetZoom = useCallback(() => {
56
+ zoomLevelRef.current = MIN_ZOOM;
57
+ setZoomLevel(MIN_ZOOM);
58
+ const container = containerRef.current;
59
+ if (container !== null) {
60
+ container.scrollLeft = 0;
61
+ }
62
+ }, [containerRef]);
63
+ useEffect(() => {
64
+ const container = containerRef.current;
65
+ if (container === null)
66
+ return;
67
+ const handleWheel = (e) => {
68
+ if (!e.ctrlKey && !e.metaKey)
69
+ return;
70
+ e.preventDefault();
71
+ let delta = e.deltaY;
72
+ if (e.deltaMode === 1) {
73
+ delta *= 20;
74
+ }
75
+ // Limiting the max zoom step per event to 15%, so to fix the huge jumps in Linux OS.
76
+ const MAX_FACTOR = 0.15;
77
+ const rawFactor = -delta * WHEEL_ZOOM_SENSITIVITY;
78
+ const zoomFactor = 1 + Math.max(-MAX_FACTOR, Math.min(MAX_FACTOR, rawFactor));
79
+ const newZoom = clampZoom(zoomLevelRef.current * zoomFactor);
80
+ applyZoom(newZoom, e.clientX - container.getBoundingClientRect().left);
81
+ };
82
+ container.addEventListener('wheel', handleWheel, { passive: false });
83
+ return () => {
84
+ container.removeEventListener('wheel', handleWheel);
85
+ };
86
+ }, [containerRef, applyZoom]);
87
+ return { zoomLevel, zoomIn, zoomOut, resetZoom };
88
+ };
@@ -25,12 +25,13 @@ interface ProfileFlameGraphProps {
25
25
  tooltipId?: string;
26
26
  maxFrameCount?: number;
27
27
  isExpanded?: boolean;
28
+ zoomControlsRef?: React.RefObject<HTMLDivElement | null>;
28
29
  }
29
30
  export declare const validateFlameChartQuery: (profileSource: MergedProfileSource) => {
30
31
  isValid: boolean;
31
32
  isNonDelta: boolean;
32
33
  isDurationTooLong: boolean;
33
34
  };
34
- declare const ProfileFlameGraph: ({ arrow, total, filtered, curPathArrow, setNewCurPathArrow, profileType, loading, error, width, isHalfScreen, metadataMappingFiles, isFlameChart, profileSource, isInSandwichView, isRenderedAsFlamegraph, tooltipId, maxFrameCount, isExpanded, metadataLoading, }: ProfileFlameGraphProps) => JSX.Element;
35
+ declare const ProfileFlameGraph: ({ arrow, total, filtered, curPathArrow, setNewCurPathArrow, profileType, loading, error, width, isHalfScreen, metadataMappingFiles, isFlameChart, profileSource, isInSandwichView, isRenderedAsFlamegraph, tooltipId, maxFrameCount, isExpanded, metadataLoading, zoomControlsRef, }: ProfileFlameGraphProps) => JSX.Element;
35
36
  export default ProfileFlameGraph;
36
37
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/ProfileFlameGraph/index.tsx"],"names":[],"mappings":"AAaA,OAAO,KAAwE,MAAM,OAAO,CAAC;AAM7F,OAAO,EAAC,eAAe,EAAC,MAAM,eAAe,CAAC;AAO9C,OAAO,EAAC,WAAW,EAAC,MAAM,eAAe,CAAC;AAI1C,OAAO,EAAC,mBAAmB,EAAE,aAAa,EAAC,MAAM,kBAAkB,CAAC;AAOpE,OAAO,EAAC,gBAAgB,EAA0B,MAAM,yBAAyB,CAAC;AAIlF,MAAM,MAAM,aAAa,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;AAEpE,UAAU,sBAAsB;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,eAAe,CAAC;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,aAAa,EAAE,aAAa,CAAC;IAC7B,YAAY,EAAE,gBAAgB,EAAE,GAAG,EAAE,CAAC;IACtC,kBAAkB,EAAE,CAAC,IAAI,EAAE,gBAAgB,EAAE,KAAK,IAAI,CAAC;IACvD,OAAO,EAAE,OAAO,CAAC;IACjB,gBAAgB,CAAC,EAAE,CAAC,OAAO,EAAE,KAAK,CAAC,GAAG,CAAC,OAAO,KAAK,IAAI,CAAC;IACxD,KAAK,CAAC,EAAE,GAAG,CAAC;IACZ,YAAY,EAAE,OAAO,CAAC;IACtB,oBAAoB,CAAC,EAAE,MAAM,EAAE,CAAC;IAChC,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,sBAAsB,CAAC,EAAE,OAAO,CAAC;IACjC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAUD,eAAO,MAAM,uBAAuB,GAClC,eAAe,mBAAmB,KACjC;IAAC,OAAO,EAAE,OAAO,CAAC;IAAC,UAAU,EAAE,OAAO,CAAC;IAAC,iBAAiB,EAAE,OAAO,CAAA;CAKpE,CAAC;AAEF,QAAA,MAAM,iBAAiB,GAAqC,qQAoBzD,sBAAsB,KAAG,GAAG,CAAC,OA0T/B,CAAC;AAEF,eAAe,iBAAiB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/ProfileFlameGraph/index.tsx"],"names":[],"mappings":"AAaA,OAAO,KAAwE,MAAM,OAAO,CAAC;AAM7F,OAAO,EAAC,eAAe,EAAC,MAAM,eAAe,CAAC;AAO9C,OAAO,EAAC,WAAW,EAAC,MAAM,eAAe,CAAC;AAI1C,OAAO,EAAC,mBAAmB,EAAE,aAAa,EAAC,MAAM,kBAAkB,CAAC;AAMpE,OAAO,EAAC,gBAAgB,EAAC,MAAM,yBAAyB,CAAC;AAIzD,MAAM,MAAM,aAAa,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;AAEpE,UAAU,sBAAsB;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,eAAe,CAAC;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,aAAa,EAAE,aAAa,CAAC;IAC7B,YAAY,EAAE,gBAAgB,EAAE,GAAG,EAAE,CAAC;IACtC,kBAAkB,EAAE,CAAC,IAAI,EAAE,gBAAgB,EAAE,KAAK,IAAI,CAAC;IACvD,OAAO,EAAE,OAAO,CAAC;IACjB,gBAAgB,CAAC,EAAE,CAAC,OAAO,EAAE,KAAK,CAAC,GAAG,CAAC,OAAO,KAAK,IAAI,CAAC;IACxD,KAAK,CAAC,EAAE,GAAG,CAAC;IACZ,YAAY,EAAE,OAAO,CAAC;IACtB,oBAAoB,CAAC,EAAE,MAAM,EAAE,CAAC;IAChC,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,sBAAsB,CAAC,EAAE,OAAO,CAAC;IACjC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,eAAe,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC,cAAc,GAAG,IAAI,CAAC,CAAC;CAC1D;AAUD,eAAO,MAAM,uBAAuB,GAClC,eAAe,mBAAmB,KACjC;IAAC,OAAO,EAAE,OAAO,CAAC;IAAC,UAAU,EAAE,OAAO,CAAC;IAAC,iBAAiB,EAAE,OAAO,CAAA;CAKpE,CAAC;AAEF,QAAA,MAAM,iBAAiB,GAAqC,sRAqBzD,sBAAsB,KAAG,GAAG,CAAC,OAiT/B,CAAC;AAEF,eAAe,iBAAiB,CAAC"}