@parca/profile 0.19.113 → 0.19.114
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4 -0
- package/dist/ProfileExplorer/ProfileExplorerSingle.d.ts.map +1 -1
- package/dist/ProfileExplorer/ProfileExplorerSingle.js +9 -3
- package/dist/ProfileFlameChart/SamplesStrips/SamplesGraph/index.d.ts +31 -0
- package/dist/ProfileFlameChart/SamplesStrips/SamplesGraph/index.d.ts.map +1 -0
- package/dist/{MetricsGraphStrips/AreaGraph → ProfileFlameChart/SamplesStrips/SamplesGraph}/index.js +32 -60
- package/dist/{MetricsGraphStrips/MetricsGraphStrips.stories.d.ts → ProfileFlameChart/SamplesStrips/SamplesStrips.stories.d.ts} +4 -3
- package/dist/ProfileFlameChart/SamplesStrips/SamplesStrips.stories.d.ts.map +1 -0
- package/dist/{MetricsGraphStrips/MetricsGraphStrips.stories.js → ProfileFlameChart/SamplesStrips/SamplesStrips.stories.js} +5 -4
- package/dist/{MetricsGraphStrips → ProfileFlameChart/SamplesStrips}/index.d.ts +5 -4
- package/dist/ProfileFlameChart/SamplesStrips/index.d.ts.map +1 -0
- package/dist/ProfileFlameChart/SamplesStrips/index.js +141 -0
- package/dist/ProfileFlameChart/index.d.ts +20 -0
- package/dist/ProfileFlameChart/index.d.ts.map +1 -0
- package/dist/ProfileFlameChart/index.js +155 -0
- package/dist/ProfileFlameGraph/index.d.ts.map +1 -1
- package/dist/ProfileFlameGraph/index.js +0 -1
- package/dist/ProfileMetricsGraph/hooks/useQueryRange.d.ts +2 -1
- package/dist/ProfileMetricsGraph/hooks/useQueryRange.d.ts.map +1 -1
- package/dist/ProfileMetricsGraph/hooks/useQueryRange.js +11 -21
- package/dist/ProfileMetricsGraph/index.d.ts.map +1 -1
- package/dist/ProfileMetricsGraph/index.js +13 -3
- package/dist/ProfileSelector/index.d.ts.map +1 -1
- package/dist/ProfileSelector/index.js +4 -0
- package/dist/ProfileView/components/ActionButtons/GroupByDropdown.d.ts +1 -0
- package/dist/ProfileView/components/ActionButtons/GroupByDropdown.d.ts.map +1 -1
- package/dist/ProfileView/components/ActionButtons/GroupByDropdown.js +2 -2
- package/dist/ProfileView/components/DashboardItems/index.d.ts +5 -4
- package/dist/ProfileView/components/DashboardItems/index.d.ts.map +1 -1
- package/dist/ProfileView/components/DashboardItems/index.js +4 -3
- package/dist/ProfileView/components/GroupByLabelsDropdown/index.d.ts +2 -1
- package/dist/ProfileView/components/GroupByLabelsDropdown/index.d.ts.map +1 -1
- package/dist/ProfileView/components/GroupByLabelsDropdown/index.js +2 -2
- package/dist/ProfileView/components/Toolbars/MultiLevelDropdown.js +1 -1
- package/dist/ProfileView/components/Toolbars/index.d.ts +2 -0
- package/dist/ProfileView/components/Toolbars/index.d.ts.map +1 -1
- package/dist/ProfileView/components/Toolbars/index.js +4 -2
- package/dist/ProfileView/hooks/useAutoSelectDimension.d.ts +16 -0
- package/dist/ProfileView/hooks/useAutoSelectDimension.d.ts.map +1 -0
- package/dist/ProfileView/hooks/useAutoSelectDimension.js +75 -0
- package/dist/ProfileView/hooks/useVisualizationState.d.ts +2 -0
- package/dist/ProfileView/hooks/useVisualizationState.d.ts.map +1 -1
- package/dist/ProfileView/hooks/useVisualizationState.js +8 -0
- package/dist/ProfileView/index.d.ts +1 -1
- package/dist/ProfileView/index.d.ts.map +1 -1
- package/dist/ProfileView/index.js +7 -4
- package/dist/ProfileView/types/visualization.d.ts +15 -3
- package/dist/ProfileView/types/visualization.d.ts.map +1 -1
- package/dist/ProfileViewWithData.d.ts +2 -1
- package/dist/ProfileViewWithData.d.ts.map +1 -1
- package/dist/ProfileViewWithData.js +41 -29
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/styles.css +1 -1
- package/package.json +8 -7
- package/src/ProfileExplorer/ProfileExplorerSingle.tsx +14 -3
- package/src/{MetricsGraphStrips/AreaGraph → ProfileFlameChart/SamplesStrips/SamplesGraph}/index.tsx +77 -81
- package/src/{MetricsGraphStrips/MetricsGraphStrips.stories.tsx → ProfileFlameChart/SamplesStrips/SamplesStrips.stories.tsx} +7 -6
- package/src/ProfileFlameChart/SamplesStrips/index.tsx +301 -0
- package/src/ProfileFlameChart/index.tsx +305 -0
- package/src/ProfileFlameGraph/index.tsx +0 -1
- package/src/ProfileMetricsGraph/hooks/useQueryRange.ts +18 -26
- package/src/ProfileMetricsGraph/index.tsx +24 -2
- package/src/ProfileSelector/index.tsx +11 -0
- package/src/ProfileView/components/ActionButtons/GroupByDropdown.tsx +3 -0
- package/src/ProfileView/components/DashboardItems/index.tsx +19 -17
- package/src/ProfileView/components/GroupByLabelsDropdown/index.tsx +4 -2
- package/src/ProfileView/components/Toolbars/MultiLevelDropdown.tsx +1 -1
- package/src/ProfileView/components/Toolbars/index.tsx +18 -1
- package/src/ProfileView/hooks/useAutoSelectDimension.ts +90 -0
- package/src/ProfileView/hooks/useVisualizationState.ts +17 -0
- package/src/ProfileView/index.tsx +16 -2
- package/src/ProfileView/types/visualization.ts +17 -3
- package/src/ProfileViewWithData.tsx +80 -37
- package/src/index.tsx +4 -0
- package/dist/MetricsGraphStrips/AreaGraph/Tooltip.d.ts +0 -10
- package/dist/MetricsGraphStrips/AreaGraph/Tooltip.d.ts.map +0 -1
- package/dist/MetricsGraphStrips/AreaGraph/Tooltip.js +0 -44
- package/dist/MetricsGraphStrips/AreaGraph/index.d.ts +0 -21
- package/dist/MetricsGraphStrips/AreaGraph/index.d.ts.map +0 -1
- package/dist/MetricsGraphStrips/MetricsGraphStrips.stories.d.ts.map +0 -1
- package/dist/MetricsGraphStrips/index.d.ts.map +0 -1
- package/dist/MetricsGraphStrips/index.js +0 -70
- package/src/MetricsGraphStrips/AreaGraph/Tooltip.tsx +0 -83
- package/src/MetricsGraphStrips/index.tsx +0 -142
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,10 @@
|
|
|
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.114](https://github.com/parca-dev/parca/compare/@parca/profile@0.19.113...@parca/profile@0.19.114) (2026-02-06)
|
|
7
|
+
|
|
8
|
+
**Note:** Version bump only for package @parca/profile
|
|
9
|
+
|
|
6
10
|
## [0.19.113](https://github.com/parca-dev/parca/compare/@parca/profile@0.19.112...@parca/profile@0.19.113) (2026-01-29)
|
|
7
11
|
|
|
8
12
|
**Note:** Version bump only for package @parca/profile
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ProfileExplorerSingle.d.ts","sourceRoot":"","sources":["../../src/ProfileExplorer/ProfileExplorerSingle.tsx"],"names":[],"mappings":"AAeA,OAAO,EAAC,kBAAkB,EAAC,MAAM,eAAe,CAAC;AACjD,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,kBAAkB,CAAC;AAMvD,UAAU,0BAA0B;IAClC,WAAW,EAAE,kBAAkB,CAAC;IAChC,UAAU,EAAE,gBAAgB,CAAC;CAC9B;AAED,QAAA,MAAM,qBAAqB,GAAI,8BAG5B,0BAA0B,KAAG,GAAG,CAAC,
|
|
1
|
+
{"version":3,"file":"ProfileExplorerSingle.d.ts","sourceRoot":"","sources":["../../src/ProfileExplorer/ProfileExplorerSingle.tsx"],"names":[],"mappings":"AAeA,OAAO,EAAC,kBAAkB,EAAC,MAAM,eAAe,CAAC;AACjD,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,kBAAkB,CAAC;AAMvD,UAAU,0BAA0B;IAClC,WAAW,EAAE,kBAAkB,CAAC;IAChC,UAAU,EAAE,gBAAgB,CAAC;CAC9B;AAED,QAAA,MAAM,qBAAqB,GAAI,8BAG5B,0BAA0B,KAAG,GAAG,CAAC,OAmCnC,CAAC;AAEF,eAAe,qBAAqB,CAAC"}
|
|
@@ -11,13 +11,19 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
|
|
|
11
11
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
12
|
// See the License for the specific language governing permissions and
|
|
13
13
|
// limitations under the License.
|
|
14
|
-
import { useState } from 'react';
|
|
14
|
+
import { useCallback, useState } from 'react';
|
|
15
15
|
import { ProfileViewWithData } from '..';
|
|
16
16
|
import ProfileSelector from '../ProfileSelector';
|
|
17
17
|
import { useQueryState } from '../hooks/useQueryState';
|
|
18
18
|
const ProfileExplorerSingle = ({ queryClient, navigateTo, }) => {
|
|
19
19
|
const [showMetricsGraph, setShowMetricsGraph] = useState(true);
|
|
20
|
-
const { profileSource } = useQueryState({ suffix: '_a' });
|
|
21
|
-
|
|
20
|
+
const { profileSource, setDraftTimeRange, commitDraft } = useQueryState({ suffix: '_a' });
|
|
21
|
+
const handleSwitchToOneMinute = useCallback(() => {
|
|
22
|
+
const now = Date.now();
|
|
23
|
+
const from = now - 60000; // 1 minute ago
|
|
24
|
+
setDraftTimeRange(from, now, 'relative:minute|1');
|
|
25
|
+
commitDraft({ from, to: now, timeSelection: 'relative:minute|1' });
|
|
26
|
+
}, [setDraftTimeRange, commitDraft]);
|
|
27
|
+
return (_jsxs(_Fragment, { children: [_jsx("div", { className: "relative", children: _jsx(ProfileSelector, { queryClient: queryClient, closeProfile: () => { }, comparing: false, enforcedProfileName: '', navigateTo: navigateTo, suffix: "_a", showMetricsGraph: showMetricsGraph, setDisplayHideMetricsGraphButton: setShowMetricsGraph }) }), profileSource != null && (_jsx(ProfileViewWithData, { queryClient: queryClient, profileSource: profileSource, onSwitchToOneMinute: handleSwitchToOneMinute }))] }));
|
|
22
28
|
};
|
|
23
29
|
export default ProfileExplorerSingle;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { NumberDuo } from '../../../utils';
|
|
2
|
+
export interface DataPoint {
|
|
3
|
+
timestamp: number;
|
|
4
|
+
value: number;
|
|
5
|
+
sampleCount?: number;
|
|
6
|
+
}
|
|
7
|
+
interface DragState {
|
|
8
|
+
stripIndex: number;
|
|
9
|
+
startX: number;
|
|
10
|
+
currentX: number;
|
|
11
|
+
}
|
|
12
|
+
interface Props {
|
|
13
|
+
width: number;
|
|
14
|
+
height: number;
|
|
15
|
+
marginLeft?: number;
|
|
16
|
+
marginRight?: number;
|
|
17
|
+
marginTop?: number;
|
|
18
|
+
marginBottom?: number;
|
|
19
|
+
fill?: string;
|
|
20
|
+
data: DataPoint[];
|
|
21
|
+
selectionBounds?: NumberDuo | undefined;
|
|
22
|
+
setSelectionBounds: (newBounds: NumberDuo | undefined) => void;
|
|
23
|
+
stepMs: number;
|
|
24
|
+
onDragStart?: (startX: number) => void;
|
|
25
|
+
dragState?: DragState;
|
|
26
|
+
isAnyDragActive?: boolean;
|
|
27
|
+
timeBounds?: NumberDuo;
|
|
28
|
+
}
|
|
29
|
+
export declare const SamplesGraph: ({ data, height, width, marginLeft, marginRight, marginBottom, marginTop, fill, selectionBounds, setSelectionBounds, stepMs, onDragStart, dragState, isAnyDragActive, timeBounds, }: Props) => JSX.Element;
|
|
30
|
+
export {};
|
|
31
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/ProfileFlameChart/SamplesStrips/SamplesGraph/index.tsx"],"names":[],"mappings":"AAmBA,OAAO,EAAC,SAAS,EAAC,MAAM,gBAAgB,CAAC;AAEzC,MAAM,WAAW,SAAS;IACxB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,UAAU,SAAS;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,UAAU,KAAK;IACb,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,SAAS,EAAE,CAAC;IAClB,eAAe,CAAC,EAAE,SAAS,GAAG,SAAS,CAAC;IACxC,kBAAkB,EAAE,CAAC,SAAS,EAAE,SAAS,GAAG,SAAS,KAAK,IAAI,CAAC;IAC/D,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,UAAU,CAAC,EAAE,SAAS,CAAC;CACxB;AAwJD,eAAO,MAAM,YAAY,GAAI,oLAgB1B,KAAK,KAAG,GAAG,CAAC,OAkId,CAAC"}
|
package/dist/{MetricsGraphStrips/AreaGraph → ProfileFlameChart/SamplesStrips/SamplesGraph}/index.js
RENAMED
|
@@ -15,7 +15,6 @@ import { useEffect, useMemo, useRef, useState } from 'react';
|
|
|
15
15
|
import { Icon } from '@iconify/react';
|
|
16
16
|
import cx from 'classnames';
|
|
17
17
|
import * as d3 from 'd3';
|
|
18
|
-
import { Tooltip } from './Tooltip';
|
|
19
18
|
const DraggingWindow = ({ dragStart, currentX, }) => {
|
|
20
19
|
const start = useMemo(() => Math.min(dragStart ?? 0, currentX ?? 0), [dragStart, currentX]);
|
|
21
20
|
const width = useMemo(() => Math.abs((dragStart ?? 0) - (currentX ?? 0)), [dragStart, currentX]);
|
|
@@ -100,26 +99,22 @@ const ZoomWindow = ({ zoomWindow, onZoomWindowChange, setIsHoveringDragHandle, }
|
|
|
100
99
|
setIsHoveringDragHandle(false);
|
|
101
100
|
}, children: _jsx(Icon, { icon: "si:drag-handle-line", className: "rotate-90", fontSize: 16 }) })] }) }));
|
|
102
101
|
};
|
|
103
|
-
export const
|
|
102
|
+
export const SamplesGraph = ({ data, height, width, marginLeft = 0, marginRight = 0, marginBottom = 0, marginTop = 0, fill = 'gray', selectionBounds, setSelectionBounds, stepMs, onDragStart, dragState, isAnyDragActive = false, timeBounds, }) => {
|
|
104
103
|
const [mousePosition, setMousePosition] = useState(undefined);
|
|
105
|
-
const [dragStart, setDragStart] = useState(undefined);
|
|
106
104
|
const [isHoveringDragHandle, setIsHoveringDragHandle] = useState(false);
|
|
107
|
-
|
|
108
|
-
const
|
|
109
|
-
const
|
|
110
|
-
//
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
.
|
|
119
|
-
.
|
|
120
|
-
.x(d => x(d.timestamp))
|
|
121
|
-
.y0(y(0))
|
|
122
|
-
.y1(d => y(d.value));
|
|
105
|
+
// use the bounds from props if provided, else compute from data
|
|
106
|
+
const xDomain = timeBounds ?? d3.extent(data, d => d.timestamp);
|
|
107
|
+
const x = d3.scaleUtc(xDomain, [marginLeft, width - marginRight]);
|
|
108
|
+
// Calculate sample count range for opacity scaling
|
|
109
|
+
const sampleCounts = data.map(d => Number(d.sampleCount ?? 1));
|
|
110
|
+
const maxSampleCount = Math.max(...sampleCounts);
|
|
111
|
+
const minSampleCount = Math.min(...sampleCounts);
|
|
112
|
+
// Create opacity scale: more samples = higher opacity
|
|
113
|
+
const opacityScale = d3
|
|
114
|
+
.scaleLinear()
|
|
115
|
+
.domain([minSampleCount, maxSampleCount])
|
|
116
|
+
.range([0.5, 1.0])
|
|
117
|
+
.clamp(true);
|
|
123
118
|
const zoomWindow = useMemo(() => {
|
|
124
119
|
if (selectionBounds === undefined) {
|
|
125
120
|
return undefined;
|
|
@@ -130,67 +125,44 @@ export const AreaGraph = ({ data, height, width, marginLeft = 0, marginRight = 0
|
|
|
130
125
|
const setSelectionBoundsWithScaling = ([startPx, endPx]) => {
|
|
131
126
|
setSelectionBounds([x.invert(startPx).getTime(), x.invert(endPx).getTime()]);
|
|
132
127
|
};
|
|
133
|
-
return (_jsxs("div", { style: { height, width }, onMouseMove: e => {
|
|
128
|
+
return (_jsxs("div", { style: { height, width }, className: "relative", onMouseMove: e => {
|
|
129
|
+
// Only track hover position when no drag is active anywhere
|
|
130
|
+
if (isAnyDragActive)
|
|
131
|
+
return;
|
|
134
132
|
const [xPos, yPos] = d3.pointer(e);
|
|
135
133
|
if (xPos >= marginLeft &&
|
|
136
134
|
xPos <= width - marginRight &&
|
|
137
135
|
yPos >= marginTop &&
|
|
138
136
|
yPos <= height - marginBottom) {
|
|
139
137
|
setMousePosition([xPos, yPos]);
|
|
140
|
-
// Find the closest data point
|
|
141
|
-
if (!isHoveringDragHandle && !isDragging) {
|
|
142
|
-
const xDate = x.invert(xPos);
|
|
143
|
-
const bisect = d3.bisector((d) => d.timestamp).left;
|
|
144
|
-
const index = bisect(data, xDate.getTime());
|
|
145
|
-
const dataPoint = data[index];
|
|
146
|
-
if (dataPoint !== undefined) {
|
|
147
|
-
setHoverData(dataPoint);
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
138
|
}
|
|
151
139
|
else {
|
|
152
140
|
setMousePosition(undefined);
|
|
153
|
-
setHoverData(null);
|
|
154
141
|
}
|
|
155
|
-
}, onMouseEnter: () => {
|
|
156
|
-
setIsMouseOverGraph(true);
|
|
157
142
|
}, onMouseLeave: () => {
|
|
158
|
-
|
|
143
|
+
// Only clear hover position, drag is managed by parent
|
|
159
144
|
setMousePosition(undefined);
|
|
160
|
-
setDragStart(undefined);
|
|
161
|
-
setHoverData(null);
|
|
162
145
|
}, onMouseDown: e => {
|
|
163
146
|
// only left mouse button
|
|
164
147
|
if (e.button !== 0) {
|
|
165
148
|
return;
|
|
166
149
|
}
|
|
167
|
-
// X/Y coordinate array relative to
|
|
150
|
+
// X/Y coordinate array relative to element
|
|
168
151
|
const rel = d3.pointer(e);
|
|
169
152
|
const xCoordinate = rel[0];
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
setDragStart(xCoordinateWithoutMargin);
|
|
153
|
+
if (xCoordinate >= 0 && onDragStart !== undefined) {
|
|
154
|
+
onDragStart(xCoordinate);
|
|
173
155
|
}
|
|
174
156
|
e.stopPropagation();
|
|
175
157
|
e.preventDefault();
|
|
176
|
-
},
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
setSelectionBoundsWithScaling([start, end]);
|
|
187
|
-
}
|
|
188
|
-
setDragStart(undefined);
|
|
189
|
-
}, className: "relative", children: [_jsx("div", { style: { height, width: 2, left: mousePosition?.[0] ?? -1 }, className: cx('bg-gray-700/75 dark:bg-gray-200/75 absolute top-0', {
|
|
190
|
-
hidden: mousePosition === undefined || isDragging || isHoveringDragHandle,
|
|
191
|
-
}) }), _jsx(DraggingWindow, { dragStart: dragStart, currentX: mousePosition?.[0] }), _jsx(ZoomWindow, { zoomWindow: zoomWindow, width: width, onZoomWindowChange: setSelectionBoundsWithScaling, setIsHoveringDragHandle: setIsHoveringDragHandle }), mousePosition !== undefined &&
|
|
192
|
-
hoverData !== null &&
|
|
193
|
-
!isDragging &&
|
|
194
|
-
!isHoveringDragHandle &&
|
|
195
|
-
isMouseOverGraph && (_jsx(Tooltip, { x: mousePosition[0], y: mousePosition[1], timestamp: hoverData.timestamp, value: hoverData.value, containerWidth: width })), _jsx("svg", { style: { width: '100%', height: '100%' }, children: _jsx("path", { fill: fill, d: area(data), className: "opacity-80" }) })] }));
|
|
158
|
+
}, children: [_jsx("div", { style: { height, width: 2, left: mousePosition?.[0] ?? -1 }, className: cx('bg-gray-700/75 dark:bg-gray-200/75 absolute top-0', {
|
|
159
|
+
hidden: mousePosition === undefined || isAnyDragActive || isHoveringDragHandle,
|
|
160
|
+
}) }), _jsx(DraggingWindow, { dragStart: dragState?.startX, currentX: dragState?.currentX }), _jsx(ZoomWindow, { zoomWindow: zoomWindow, width: width, onZoomWindowChange: setSelectionBoundsWithScaling, setIsHoveringDragHandle: setIsHoveringDragHandle }), _jsxs("svg", { style: { width: '100%', height: '100%' }, children: [_jsx("rect", { x: marginLeft, y: 0, width: width - marginLeft - marginRight, height: height, fill: fill, fillOpacity: 0.1 }), _jsx("g", { children: data.map((d, i) => {
|
|
161
|
+
const xPosition = x(d.timestamp);
|
|
162
|
+
// Use stepMs for bucket width
|
|
163
|
+
const rectWidth = x(d.timestamp + stepMs) - xPosition;
|
|
164
|
+
// Calculate opacity based on sample count
|
|
165
|
+
const opacity = opacityScale(Number(d.sampleCount ?? 1));
|
|
166
|
+
return (_jsx("rect", { x: xPosition, y: 0, width: rectWidth, height: height, fill: fill, fillOpacity: opacity }, i));
|
|
167
|
+
}) })] })] }));
|
|
196
168
|
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Meta } from '@storybook/react';
|
|
2
|
-
import { NumberDuo } from '
|
|
3
|
-
import { DataPoint } from './
|
|
2
|
+
import { NumberDuo } from '../../utils';
|
|
3
|
+
import { DataPoint } from './SamplesGraph';
|
|
4
4
|
declare const meta: Meta;
|
|
5
5
|
export default meta;
|
|
6
6
|
export declare const ThreeCPUStrips: {
|
|
@@ -18,7 +18,8 @@ export declare const ThreeCPUStrips: {
|
|
|
18
18
|
};
|
|
19
19
|
onSelectedTimeframe: (index: number, bounds: NumberDuo) => void;
|
|
20
20
|
bounds: number[];
|
|
21
|
+
stepMs: number;
|
|
21
22
|
};
|
|
22
23
|
render: (args: any) => JSX.Element;
|
|
23
24
|
};
|
|
24
|
-
//# sourceMappingURL=
|
|
25
|
+
//# sourceMappingURL=SamplesStrips.stories.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SamplesStrips.stories.d.ts","sourceRoot":"","sources":["../../../src/ProfileFlameChart/SamplesStrips/SamplesStrips.stories.tsx"],"names":[],"mappings":"AAgBA,OAAO,EAAC,IAAI,EAAC,MAAM,kBAAkB,CAAC;AAEtC,OAAO,EAAC,SAAS,EAAC,MAAM,aAAa,CAAC;AACtC,OAAO,EAAC,SAAS,EAAC,MAAM,gBAAgB,CAAC;AAqBzC,QAAA,MAAM,IAAI,EAAE,IAGX,CAAC;AACF,eAAe,IAAI,CAAC;AAEpB,eAAO,MAAM,cAAc;;;;;;;;;;;;;qCAKM,MAAM,UAAU,SAAS,KAAG,IAAI;;;;mBAM9B,GAAG,KAAG,GAAG,CAAC,OAAO;CAUnD,CAAC"}
|
|
@@ -13,7 +13,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
13
13
|
// limitations under the License.
|
|
14
14
|
// eslint-disable-next-line import/named
|
|
15
15
|
import { useArgs } from '@storybook/preview-api';
|
|
16
|
-
import {
|
|
16
|
+
import { SamplesStrip } from './index';
|
|
17
17
|
function seededRandom(seed) {
|
|
18
18
|
return () => {
|
|
19
19
|
seed = (seed * 16807) % 2147483647;
|
|
@@ -31,8 +31,8 @@ for (let i = 0; i < 200; i++) {
|
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
const meta = {
|
|
34
|
-
title: 'components/
|
|
35
|
-
component:
|
|
34
|
+
title: 'components/SamplesStrip',
|
|
35
|
+
component: SamplesStrip,
|
|
36
36
|
};
|
|
37
37
|
export default meta;
|
|
38
38
|
export const ThreeCPUStrips = {
|
|
@@ -44,6 +44,7 @@ export const ThreeCPUStrips = {
|
|
|
44
44
|
console.log('onSelectedTimeframe', index, bounds);
|
|
45
45
|
},
|
|
46
46
|
bounds: [mockData[0][0].timestamp, mockData[0][mockData[0].length - 1].timestamp],
|
|
47
|
+
stepMs: 100,
|
|
47
48
|
},
|
|
48
49
|
render: function Component(args) {
|
|
49
50
|
const [, setArgs] = useArgs();
|
|
@@ -51,6 +52,6 @@ export const ThreeCPUStrips = {
|
|
|
51
52
|
args.onSelectedTimeframe(index, bounds);
|
|
52
53
|
setArgs({ ...args, selectedTimeframe: { index, bounds } });
|
|
53
54
|
};
|
|
54
|
-
return _jsx(
|
|
55
|
+
return _jsx(SamplesStrip, { ...args, onSelectedTimeframe: onSelectedTimeframe });
|
|
55
56
|
},
|
|
56
57
|
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { LabelSet } from '@parca/client';
|
|
2
|
-
import { NumberDuo } from '
|
|
3
|
-
import { DataPoint } from './
|
|
2
|
+
import { NumberDuo } from '../../utils';
|
|
3
|
+
import { DataPoint } from './SamplesGraph';
|
|
4
|
+
export type { DataPoint } from './SamplesGraph';
|
|
4
5
|
interface Props {
|
|
5
6
|
cpus: LabelSet[];
|
|
6
7
|
data: DataPoint[][];
|
|
@@ -11,8 +12,8 @@ interface Props {
|
|
|
11
12
|
onSelectedTimeframe: (labels: LabelSet, bounds: NumberDuo | undefined) => void;
|
|
12
13
|
width?: number;
|
|
13
14
|
bounds: NumberDuo;
|
|
15
|
+
stepMs: number;
|
|
14
16
|
}
|
|
15
17
|
export declare const labelSetToString: (labelSet?: LabelSet) => string;
|
|
16
|
-
export declare const
|
|
17
|
-
export {};
|
|
18
|
+
export declare const SamplesStrip: ({ cpus, data, selectedTimeframe, onSelectedTimeframe, width, bounds, stepMs, }: Props) => JSX.Element;
|
|
18
19
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/ProfileFlameChart/SamplesStrips/index.tsx"],"names":[],"mappings":"AAqBA,OAAO,EAAC,QAAQ,EAAC,MAAM,eAAe,CAAC;AAGvC,OAAO,EAAC,SAAS,EAAC,MAAM,aAAa,CAAC;AACtC,OAAO,EAAC,SAAS,EAAe,MAAM,gBAAgB,CAAC;AAEvD,YAAY,EAAC,SAAS,EAAC,MAAM,gBAAgB,CAAC;AAQ9C,UAAU,KAAK;IACb,IAAI,EAAE,QAAQ,EAAE,CAAC;IACjB,IAAI,EAAE,SAAS,EAAE,EAAE,CAAC;IACpB,iBAAiB,CAAC,EAAE;QAClB,MAAM,EAAE,QAAQ,CAAC;QACjB,MAAM,EAAE,SAAS,CAAC;KACnB,CAAC;IACF,mBAAmB,EAAE,CAAC,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,GAAG,SAAS,KAAK,IAAI,CAAC;IAC/E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,SAAS,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,eAAO,MAAM,gBAAgB,GAAI,WAAW,QAAQ,KAAG,MAoBtD,CAAC;AA6FF,eAAO,MAAM,YAAY,GAAI,gFAQ1B,KAAK,KAAG,GAAG,CAAC,OAmId,CAAC"}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { jsx as _jsx, 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 { useMemo, useRef, useState } from 'react';
|
|
15
|
+
import { Icon } from '@iconify/react';
|
|
16
|
+
import cx from 'classnames';
|
|
17
|
+
import * as d3 from 'd3';
|
|
18
|
+
import isEqual from 'fast-deep-equal';
|
|
19
|
+
import { useIntersectionObserver } from 'usehooks-ts';
|
|
20
|
+
import { TimelineGuide } from '../../TimelineGuide';
|
|
21
|
+
import { SamplesGraph } from './SamplesGraph';
|
|
22
|
+
export const labelSetToString = (labelSet) => {
|
|
23
|
+
if (labelSet === undefined) {
|
|
24
|
+
return '{}';
|
|
25
|
+
}
|
|
26
|
+
let str = '{';
|
|
27
|
+
let isFirst = true;
|
|
28
|
+
for (const label of labelSet.labels) {
|
|
29
|
+
if (!isFirst) {
|
|
30
|
+
str += ', ';
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
isFirst = false;
|
|
34
|
+
}
|
|
35
|
+
str += `${label.name}: ${label.value}`;
|
|
36
|
+
}
|
|
37
|
+
str += '}';
|
|
38
|
+
return str;
|
|
39
|
+
};
|
|
40
|
+
const STRIP_HEIGHT = 24;
|
|
41
|
+
const getTimelineGuideHeight = (cpusCount, collapsedCount) => {
|
|
42
|
+
return (STRIP_HEIGHT + 4) * (cpusCount - collapsedCount) + 20 * collapsedCount + 24 - 6;
|
|
43
|
+
};
|
|
44
|
+
const stickyPx = 0;
|
|
45
|
+
const SamplesGraphContainer = ({ isSelected, isCollapsed, cpu, width, onToggleCollapse, data, selectionBounds, setSelectionBounds, color, stepMs, onDragStart, dragState, stripIndex, isAnyDragActive, timeBounds, }) => {
|
|
46
|
+
const labelStr = labelSetToString(cpu);
|
|
47
|
+
const { isIntersecting, ref } = useIntersectionObserver({
|
|
48
|
+
rootMargin: `${stickyPx}px 0px 0px 0px`,
|
|
49
|
+
});
|
|
50
|
+
const isSticky = useMemo(() => {
|
|
51
|
+
return isSelected && isIntersecting;
|
|
52
|
+
}, [isSelected, isIntersecting]);
|
|
53
|
+
return (_jsxs("div", { className: cx('min-h-5', {
|
|
54
|
+
relative: !isSelected,
|
|
55
|
+
'sticky z-30 bg-white dark:bg-black bg-opacity-75': isSelected,
|
|
56
|
+
'!bg-opacity-100': isSticky,
|
|
57
|
+
}), style: { width: width ?? 1468, top: isSelected ? stickyPx : undefined }, ref: ref, children: [_jsxs("div", { className: "text-xs absolute top-0 left-0 flex gap-[2px] items-center bg-white/50 dark:bg-black/50 px-1 rounded-sm cursor-pointer", style: {
|
|
58
|
+
zIndex: 15,
|
|
59
|
+
}, onClick: onToggleCollapse, children: [_jsx(Icon, { icon: isCollapsed ? 'bxs:right-arrow' : 'bxs:down-arrow' }), labelStr] }), !isCollapsed ? (_jsx(SamplesGraph, { data: data, height: STRIP_HEIGHT, width: width ?? 1468, fill: color(labelStr), selectionBounds: selectionBounds, setSelectionBounds: setSelectionBounds, stepMs: stepMs, onDragStart: (startX) => onDragStart(stripIndex, startX), dragState: dragState?.stripIndex === stripIndex ? dragState : undefined, isAnyDragActive: isAnyDragActive, timeBounds: timeBounds })) : null] }, labelStr));
|
|
60
|
+
};
|
|
61
|
+
export const SamplesStrip = ({ cpus, data, selectedTimeframe, onSelectedTimeframe, width, bounds, stepMs, }) => {
|
|
62
|
+
const [collapsedLabels, setCollapsedLabels] = useState(new Set());
|
|
63
|
+
const [dragState, setDragState] = useState(undefined);
|
|
64
|
+
const containerRef = useRef(null);
|
|
65
|
+
const isDragging = dragState !== undefined;
|
|
66
|
+
// Sort cpus and data by label string for consistent ordering across reloads
|
|
67
|
+
const sortedItems = useMemo(() => {
|
|
68
|
+
const items = cpus.map((cpu, i) => ({
|
|
69
|
+
cpu,
|
|
70
|
+
data: data[i],
|
|
71
|
+
label: labelSetToString(cpu),
|
|
72
|
+
}));
|
|
73
|
+
return items.sort((a, b) => a.label.localeCompare(b.label));
|
|
74
|
+
}, [cpus, data]);
|
|
75
|
+
// Deterministic color: hash the label string so the same label always gets the same color
|
|
76
|
+
// regardless of render order.
|
|
77
|
+
const color = useMemo(() => {
|
|
78
|
+
const palette = d3.schemeObservable10;
|
|
79
|
+
const hashStr = (s) => {
|
|
80
|
+
let h = 0;
|
|
81
|
+
for (let i = 0; i < s.length; i++) {
|
|
82
|
+
h = (Math.imul(31, h) + s.charCodeAt(i)) | 0;
|
|
83
|
+
}
|
|
84
|
+
return Math.abs(h);
|
|
85
|
+
};
|
|
86
|
+
return (label) => palette[hashStr(label) % palette.length];
|
|
87
|
+
}, []);
|
|
88
|
+
const handleDragStart = (stripIndex, startX) => {
|
|
89
|
+
setDragState({ stripIndex, startX, currentX: startX });
|
|
90
|
+
};
|
|
91
|
+
const handleMouseMove = (e) => {
|
|
92
|
+
if (dragState === undefined || containerRef.current === null)
|
|
93
|
+
return;
|
|
94
|
+
const rect = containerRef.current.getBoundingClientRect();
|
|
95
|
+
const x = e.clientX - rect.left;
|
|
96
|
+
// Clamp to container bounds
|
|
97
|
+
const clampedX = Math.max(0, Math.min(x, width ?? rect.width));
|
|
98
|
+
setDragState({ ...dragState, currentX: clampedX });
|
|
99
|
+
};
|
|
100
|
+
const handleMouseUp = (e) => {
|
|
101
|
+
if (dragState === undefined || containerRef.current === null)
|
|
102
|
+
return;
|
|
103
|
+
const rect = containerRef.current.getBoundingClientRect();
|
|
104
|
+
const x = e.clientX - rect.left;
|
|
105
|
+
const clampedX = Math.max(0, Math.min(x, width ?? rect.width));
|
|
106
|
+
const { stripIndex, startX } = dragState;
|
|
107
|
+
if (startX !== clampedX) {
|
|
108
|
+
const start = Math.min(startX, clampedX);
|
|
109
|
+
const end = Math.max(startX, clampedX);
|
|
110
|
+
// Convert pixel positions to timestamps
|
|
111
|
+
const innerWidth = width ?? rect.width;
|
|
112
|
+
const startTs = bounds[0] + (start / innerWidth) * (bounds[1] - bounds[0]);
|
|
113
|
+
const endTs = bounds[0] + (end / innerWidth) * (bounds[1] - bounds[0]);
|
|
114
|
+
// Use sortedItems to get the correct cpu for the strip index
|
|
115
|
+
onSelectedTimeframe(sortedItems[stripIndex].cpu, [startTs, endTs]);
|
|
116
|
+
}
|
|
117
|
+
setDragState(undefined);
|
|
118
|
+
};
|
|
119
|
+
const handleMouseLeave = () => {
|
|
120
|
+
setDragState(undefined);
|
|
121
|
+
};
|
|
122
|
+
if (data.length === 0) {
|
|
123
|
+
return (_jsx("span", { className: "flex justify-center my-10", children: "There is no data matching your filter criteria, please try changing the filter." }));
|
|
124
|
+
}
|
|
125
|
+
return (_jsxs("div", { ref: containerRef, className: cx('flex flex-col gap-1 relative my-0', { 'cursor-ew-resize': isDragging }), style: { width: width ?? '100%' }, onMouseMove: handleMouseMove, onMouseUp: handleMouseUp, onMouseLeave: handleMouseLeave, children: [_jsx(TimelineGuide, { bounds: [BigInt(0), BigInt(bounds[1] - bounds[0])], width: width ?? 1468, height: getTimelineGuideHeight(sortedItems.length, collapsedLabels.size), margin: 1 }), sortedItems.map((item, i) => {
|
|
126
|
+
const isCollapsed = collapsedLabels.has(item.label);
|
|
127
|
+
const isSelected = isEqual(item.cpu, selectedTimeframe?.labels);
|
|
128
|
+
return (_jsx(SamplesGraphContainer, { isSelected: isSelected, isCollapsed: isCollapsed, cpu: item.cpu, width: width, data: item.data, onToggleCollapse: () => {
|
|
129
|
+
const newCollapsedLabels = new Set(collapsedLabels);
|
|
130
|
+
if (collapsedLabels.has(item.label)) {
|
|
131
|
+
newCollapsedLabels.delete(item.label);
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
newCollapsedLabels.add(item.label);
|
|
135
|
+
}
|
|
136
|
+
setCollapsedLabels(newCollapsedLabels);
|
|
137
|
+
}, selectionBounds: isSelected ? selectedTimeframe?.bounds : undefined, setSelectionBounds: newBounds => {
|
|
138
|
+
onSelectedTimeframe(item.cpu, newBounds);
|
|
139
|
+
}, color: color, stepMs: stepMs, onDragStart: handleDragStart, dragState: dragState, stripIndex: i, isAnyDragActive: isDragging, timeBounds: bounds }, item.label));
|
|
140
|
+
})] }));
|
|
141
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { QueryServiceClient } from '@parca/client';
|
|
2
|
+
import { ProfileType } from '@parca/parser';
|
|
3
|
+
import { ProfileSource } from '../ProfileSource';
|
|
4
|
+
import type { SamplesData } from '../ProfileView/types/visualization';
|
|
5
|
+
interface ProfileFlameChartProps {
|
|
6
|
+
samplesData?: SamplesData;
|
|
7
|
+
queryClient: QueryServiceClient;
|
|
8
|
+
profileSource: ProfileSource;
|
|
9
|
+
width: number;
|
|
10
|
+
total: bigint;
|
|
11
|
+
filtered: bigint;
|
|
12
|
+
profileType?: ProfileType;
|
|
13
|
+
isHalfScreen: boolean;
|
|
14
|
+
metadataMappingFiles?: string[];
|
|
15
|
+
metadataLoading?: boolean;
|
|
16
|
+
onSwitchToOneMinute?: () => void;
|
|
17
|
+
}
|
|
18
|
+
export declare const ProfileFlameChart: ({ samplesData, queryClient, profileSource, width, total, filtered, profileType, isHalfScreen, metadataMappingFiles, metadataLoading, onSwitchToOneMinute, }: ProfileFlameChartProps) => JSX.Element;
|
|
19
|
+
export default ProfileFlameChart;
|
|
20
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } 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 { useEffect, useMemo, useRef } from 'react';
|
|
15
|
+
import { QueryRequest_ReportType } from '@parca/client';
|
|
16
|
+
import { Button, useParcaContext, useURLState, useURLStateCustom, } from '@parca/components';
|
|
17
|
+
import { Matcher, MatcherTypes, Query } from '@parca/parser';
|
|
18
|
+
import ProfileFlameGraph, { validateFlameChartQuery } from '../ProfileFlameGraph';
|
|
19
|
+
import { boundsFromProfileSource } from '../ProfileFlameGraph/FlameGraphArrow/utils';
|
|
20
|
+
import { MergedProfileSource } from '../ProfileSource';
|
|
21
|
+
import { useQuery } from '../useQuery';
|
|
22
|
+
import { SamplesStrip } from './SamplesStrips';
|
|
23
|
+
const TimeframeStateSerializer = {
|
|
24
|
+
parse: (value) => {
|
|
25
|
+
if (value == null || value === '' || value === 'undefined' || Array.isArray(value)) {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
const [labelPart, boundsPart] = value.split('|');
|
|
30
|
+
if (labelPart != null && boundsPart != null) {
|
|
31
|
+
const labels = labelPart.split(',').map(labelStr => {
|
|
32
|
+
const [name, ...rest] = labelStr.split(':');
|
|
33
|
+
return { name, value: rest.join(':') };
|
|
34
|
+
});
|
|
35
|
+
const [startMs, endMs] = boundsPart.split(',').map(Number);
|
|
36
|
+
if (labels.length > 0 && !isNaN(startMs) && !isNaN(endMs)) {
|
|
37
|
+
return {
|
|
38
|
+
labels: { labels },
|
|
39
|
+
bounds: [startMs, endMs],
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// Ignore parsing errors
|
|
46
|
+
}
|
|
47
|
+
return undefined;
|
|
48
|
+
},
|
|
49
|
+
stringify: (value) => {
|
|
50
|
+
if (value == null) {
|
|
51
|
+
return '';
|
|
52
|
+
}
|
|
53
|
+
const labelsStr = value.labels.labels.map(l => `${l.name}:${l.value}`).join(',');
|
|
54
|
+
return `${labelsStr}|${value.bounds[0]},${value.bounds[1]}`;
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
// Helper to create a filtered profile source with narrowed time bounds
|
|
58
|
+
// and dimension label matchers from the selected strip.
|
|
59
|
+
const createFilteredProfileSource = (profileSource, selectedTimeframe) => {
|
|
60
|
+
if (!(profileSource instanceof MergedProfileSource)) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
// The bounds are in milliseconds, convert to nanoseconds for the profile source
|
|
64
|
+
// Round to integers since BigInt requires integer values
|
|
65
|
+
const mergeFrom = BigInt(Math.round(selectedTimeframe.bounds[0])) * 1000000n;
|
|
66
|
+
const mergeTo = BigInt(Math.round(selectedTimeframe.bounds[1])) * 1000000n;
|
|
67
|
+
// Add dimension labels as additional matchers to the query
|
|
68
|
+
const dimensionMatchers = selectedTimeframe.labels.labels.map(l => new Matcher(l.name, MatcherTypes.MatchEqual, l.value));
|
|
69
|
+
const query = new Query(profileSource.query.profType, [...profileSource.query.matchers, ...dimensionMatchers], '');
|
|
70
|
+
return new MergedProfileSource(mergeFrom, mergeTo, query);
|
|
71
|
+
};
|
|
72
|
+
export const ProfileFlameChart = ({ samplesData, queryClient, profileSource, width, total, filtered, profileType, isHalfScreen, metadataMappingFiles, metadataLoading, onSwitchToOneMinute, }) => {
|
|
73
|
+
const { loader } = useParcaContext();
|
|
74
|
+
const [selectedTimeframe, setSelectedTimeframe] = useURLStateCustom('flamechart_timeframe', TimeframeStateSerializer);
|
|
75
|
+
// Read flamechart dimension from URL state to detect changes
|
|
76
|
+
const [flamechartDimension] = useURLState('flamechart_dimension', {
|
|
77
|
+
alwaysReturnArray: true,
|
|
78
|
+
});
|
|
79
|
+
// Reset selection when the parent time range (profileSource) changes
|
|
80
|
+
const timeBoundsKey = boundsFromProfileSource(profileSource).join(',');
|
|
81
|
+
const prevTimeBoundsKey = useRef(timeBoundsKey);
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
if (prevTimeBoundsKey.current !== timeBoundsKey) {
|
|
84
|
+
prevTimeBoundsKey.current = timeBoundsKey;
|
|
85
|
+
setSelectedTimeframe(undefined);
|
|
86
|
+
}
|
|
87
|
+
}, [timeBoundsKey, setSelectedTimeframe]);
|
|
88
|
+
// Reset selection when the dimension changes
|
|
89
|
+
const dimensionKey = (flamechartDimension ?? []).join(',');
|
|
90
|
+
const prevDimensionKey = useRef(dimensionKey);
|
|
91
|
+
useEffect(() => {
|
|
92
|
+
if (prevDimensionKey.current !== dimensionKey) {
|
|
93
|
+
prevDimensionKey.current = dimensionKey;
|
|
94
|
+
setSelectedTimeframe(undefined);
|
|
95
|
+
}
|
|
96
|
+
}, [dimensionKey, setSelectedTimeframe]);
|
|
97
|
+
// Handle timeframe selection from strips
|
|
98
|
+
const handleSelectedTimeframe = (labels, bounds) => {
|
|
99
|
+
if (bounds === undefined) {
|
|
100
|
+
setSelectedTimeframe(undefined);
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
setSelectedTimeframe({ labels, bounds });
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
// Create filtered profile source when selection exists
|
|
107
|
+
const filteredProfileSource = useMemo(() => {
|
|
108
|
+
if (selectedTimeframe == null)
|
|
109
|
+
return null;
|
|
110
|
+
return createFilteredProfileSource(profileSource, selectedTimeframe);
|
|
111
|
+
}, [profileSource, selectedTimeframe]);
|
|
112
|
+
// Query flamechart data only when a strip selection exists
|
|
113
|
+
const { isLoading: flamechartLoading, response: flamechartResponse, error: flamechartError, } = useQuery(queryClient, filteredProfileSource ?? profileSource, QueryRequest_ReportType.FLAMECHART, {
|
|
114
|
+
skip: selectedTimeframe == null || filteredProfileSource == null,
|
|
115
|
+
});
|
|
116
|
+
const flamechartArrow = flamechartResponse?.report.oneofKind === 'flamegraphArrow'
|
|
117
|
+
? flamechartResponse.report.flamegraphArrow
|
|
118
|
+
: undefined;
|
|
119
|
+
const flamechartTotal = flamechartResponse != null ? BigInt(flamechartResponse.total) : total;
|
|
120
|
+
const flamechartFiltered = flamechartResponse != null ? BigInt(flamechartResponse.filtered) : filtered;
|
|
121
|
+
// Get time bounds from profile source for the strips
|
|
122
|
+
const timeBounds = boundsFromProfileSource(profileSource);
|
|
123
|
+
// Transform samples data for SamplesStrip component
|
|
124
|
+
const stripsData = useMemo(() => {
|
|
125
|
+
if (samplesData?.series == null)
|
|
126
|
+
return { cpus: [], data: [], stepMs: 0 };
|
|
127
|
+
const cpus = samplesData.series.map(s => s.labelset);
|
|
128
|
+
const data = samplesData.series.map(s => s.data);
|
|
129
|
+
const stepMs = samplesData.stepMs ?? 0;
|
|
130
|
+
return { cpus, data, stepMs };
|
|
131
|
+
}, [samplesData?.series, samplesData?.stepMs]);
|
|
132
|
+
const { isValid, isNonDelta, isDurationTooLong } = validateFlameChartQuery(profileSource);
|
|
133
|
+
if (!isValid) {
|
|
134
|
+
if (isDurationTooLong) {
|
|
135
|
+
return (_jsxs("div", { className: "flex flex-col justify-center items-center p-10 text-center gap-4 text-sm", children: [_jsx("span", { children: "Flame chart is unavailable for queries longer than one minute. Try reducing the time range to one minute or selecting a point in the metrics graph." }), onSwitchToOneMinute != null && (_jsx(Button, { variant: "primary", onClick: onSwitchToOneMinute, children: "Switch to last 1 minute" }))] }));
|
|
136
|
+
}
|
|
137
|
+
const message = isNonDelta
|
|
138
|
+
? 'To use the Flame chart, please switch to a Delta profile.'
|
|
139
|
+
: 'Flame chart is unavailable for this query.';
|
|
140
|
+
return (_jsx("div", { className: "flex flex-col justify-center p-10 text-center gap-6 text-sm", children: message }));
|
|
141
|
+
}
|
|
142
|
+
const hasDimension = (flamechartDimension ?? []).length > 0;
|
|
143
|
+
// Show loader while metadata labels are loading (needed for dimension auto-selection)
|
|
144
|
+
if (metadataLoading === true) {
|
|
145
|
+
return _jsx(_Fragment, { children: loader });
|
|
146
|
+
}
|
|
147
|
+
if (!hasDimension) {
|
|
148
|
+
return (_jsx("div", { className: "flex justify-center items-center py-10 text-gray-500 dark:text-gray-400 text-sm", children: "Select a label in the \"Samples group by\" dropdown above to view the samples strips." }));
|
|
149
|
+
}
|
|
150
|
+
if (samplesData?.loading === true) {
|
|
151
|
+
return _jsx(_Fragment, { children: loader });
|
|
152
|
+
}
|
|
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." }))] }));
|
|
154
|
+
};
|
|
155
|
+
export default ProfileFlameChart;
|
|
@@ -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;
|
|
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"}
|