@parca/profile 0.19.12 → 0.19.14
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 +8 -0
- package/dist/GraphTooltipArrow/Content.js +1 -1
- package/dist/GraphTooltipArrow/index.js +2 -2
- package/dist/MatchersInput/index.d.ts +3 -1
- package/dist/MatchersInput/index.d.ts.map +1 -1
- package/dist/MatchersInput/index.js +8 -4
- package/dist/ProfileIcicleGraph/IcicleGraphArrow/ContextMenu.d.ts.map +1 -1
- package/dist/ProfileIcicleGraph/IcicleGraphArrow/ContextMenu.js +12 -3
- package/dist/ProfileIcicleGraph/IcicleGraphArrow/ContextMenuWrapper.d.ts +1 -1
- package/dist/ProfileIcicleGraph/IcicleGraphArrow/ContextMenuWrapper.d.ts.map +1 -1
- package/dist/ProfileIcicleGraph/IcicleGraphArrow/ContextMenuWrapper.js +9 -1
- package/dist/ProfileIcicleGraph/IcicleGraphArrow/IcicleGraphNodes.d.ts +1 -0
- package/dist/ProfileIcicleGraph/IcicleGraphArrow/IcicleGraphNodes.d.ts.map +1 -1
- package/dist/ProfileIcicleGraph/IcicleGraphArrow/IcicleGraphNodes.js +7 -3
- package/dist/ProfileIcicleGraph/IcicleGraphArrow/MemoizedTooltip.d.ts.map +1 -1
- package/dist/ProfileIcicleGraph/IcicleGraphArrow/MemoizedTooltip.js +17 -2
- package/dist/ProfileIcicleGraph/IcicleGraphArrow/index.d.ts +2 -0
- package/dist/ProfileIcicleGraph/IcicleGraphArrow/index.d.ts.map +1 -1
- package/dist/ProfileIcicleGraph/IcicleGraphArrow/index.js +23 -8
- package/dist/ProfileIcicleGraph/index.d.ts +3 -1
- package/dist/ProfileIcicleGraph/index.d.ts.map +1 -1
- package/dist/ProfileIcicleGraph/index.js +6 -4
- package/dist/ProfileSelector/QueryControls.d.ts.map +1 -1
- package/dist/ProfileSelector/QueryControls.js +1 -1
- package/dist/ProfileSelector/index.d.ts.map +1 -1
- package/dist/ProfileSelector/index.js +4 -2
- package/dist/ProfileView/components/DashboardItems/index.d.ts.map +1 -1
- package/dist/ProfileView/components/DashboardItems/index.js +1 -1
- package/dist/ProfileView/components/ShareButton/index.d.ts.map +1 -1
- package/dist/ProfileView/components/ShareButton/index.js +1 -1
- package/dist/ProfileView/components/Toolbars/index.d.ts +0 -2
- package/dist/ProfileView/components/Toolbars/index.d.ts.map +1 -1
- package/dist/ProfileView/components/Toolbars/index.js +4 -5
- package/dist/ProfileView/components/ViewSelector/index.d.ts.map +1 -1
- package/dist/ProfileView/components/ViewSelector/index.js +18 -11
- package/dist/ProfileView/context/DashboardContext.d.ts.map +1 -1
- package/dist/ProfileView/context/DashboardContext.js +5 -0
- package/dist/ProfileView/index.d.ts.map +1 -1
- package/dist/ProfileView/index.js +4 -3
- package/dist/Sandwich/components/CalleesSection.d.ts +1 -2
- package/dist/Sandwich/components/CalleesSection.d.ts.map +1 -1
- package/dist/Sandwich/components/CalleesSection.js +2 -6
- package/dist/Sandwich/components/CallersSection.d.ts +4 -2
- package/dist/Sandwich/components/CallersSection.d.ts.map +1 -1
- package/dist/Sandwich/components/CallersSection.js +45 -9
- package/dist/Sandwich/components/TableSection.js +1 -1
- package/dist/Sandwich/index.d.ts +0 -1
- package/dist/Sandwich/index.d.ts.map +1 -1
- package/dist/Sandwich/index.js +27 -79
- package/dist/SimpleMatchers/index.d.ts +2 -0
- package/dist/SimpleMatchers/index.d.ts.map +1 -1
- package/dist/SimpleMatchers/index.js +16 -6
- package/dist/Table/MoreDropdown.d.ts.map +1 -1
- package/dist/Table/MoreDropdown.js +1 -2
- package/dist/Table/TableContextMenu.d.ts +9 -0
- package/dist/Table/TableContextMenu.d.ts.map +1 -0
- package/dist/Table/TableContextMenu.js +38 -0
- package/dist/Table/TableContextMenuWrapper.d.ts +10 -0
- package/dist/Table/TableContextMenuWrapper.d.ts.map +1 -0
- package/dist/Table/TableContextMenuWrapper.js +30 -0
- package/dist/Table/hooks/useTableConfiguration.d.ts.map +1 -1
- package/dist/Table/hooks/useTableConfiguration.js +2 -20
- package/dist/Table/index.d.ts.map +1 -1
- package/dist/Table/index.js +65 -5
- package/dist/ViewMatchers/index.d.ts +2 -0
- package/dist/ViewMatchers/index.d.ts.map +1 -1
- package/dist/ViewMatchers/index.js +14 -4
- package/dist/contexts/MatchersInputLabelsContext.d.ts +3 -1
- package/dist/contexts/MatchersInputLabelsContext.d.ts.map +1 -1
- package/dist/contexts/MatchersInputLabelsContext.js +3 -3
- package/dist/contexts/SimpleMatchersLabelContext.d.ts +3 -1
- package/dist/contexts/SimpleMatchersLabelContext.d.ts.map +1 -1
- package/dist/contexts/SimpleMatchersLabelContext.js +2 -2
- package/dist/styles.css +1 -1
- package/package.json +3 -3
- package/src/GraphTooltipArrow/Content.tsx +3 -3
- package/src/GraphTooltipArrow/index.tsx +2 -2
- package/src/MatchersInput/index.tsx +17 -4
- package/src/ProfileIcicleGraph/IcicleGraphArrow/ContextMenu.tsx +19 -3
- package/src/ProfileIcicleGraph/IcicleGraphArrow/ContextMenuWrapper.tsx +10 -2
- package/src/ProfileIcicleGraph/IcicleGraphArrow/IcicleGraphNodes.tsx +19 -2
- package/src/ProfileIcicleGraph/IcicleGraphArrow/MemoizedTooltip.tsx +20 -2
- package/src/ProfileIcicleGraph/IcicleGraphArrow/index.tsx +40 -6
- package/src/ProfileIcicleGraph/index.tsx +20 -2
- package/src/ProfileSelector/QueryControls.tsx +6 -0
- package/src/ProfileSelector/index.tsx +12 -2
- package/src/ProfileView/components/DashboardItems/index.tsx +0 -1
- package/src/ProfileView/components/ShareButton/index.tsx +9 -3
- package/src/ProfileView/components/Toolbars/index.tsx +7 -23
- package/src/ProfileView/components/ViewSelector/index.tsx +20 -11
- package/src/ProfileView/context/DashboardContext.tsx +6 -0
- package/src/ProfileView/index.tsx +12 -4
- package/src/Sandwich/components/CalleesSection.tsx +1 -7
- package/src/Sandwich/components/CallersSection.tsx +92 -35
- package/src/Sandwich/components/TableSection.tsx +2 -2
- package/src/Sandwich/index.tsx +20 -107
- package/src/SimpleMatchers/index.tsx +20 -4
- package/src/Table/MoreDropdown.tsx +1 -2
- package/src/Table/TableContextMenu.tsx +70 -0
- package/src/Table/TableContextMenuWrapper.tsx +48 -0
- package/src/Table/hooks/useTableConfiguration.tsx +2 -25
- package/src/Table/index.tsx +84 -5
- package/src/ViewMatchers/index.tsx +17 -3
- package/src/contexts/MatchersInputLabelsContext.tsx +10 -2
- package/src/contexts/SimpleMatchersLabelContext.tsx +5 -1
|
@@ -33,6 +33,8 @@ interface MatchersInputProps {
|
|
|
33
33
|
runQuery: () => void;
|
|
34
34
|
currentQuery: Query;
|
|
35
35
|
profileType: string;
|
|
36
|
+
start?: number;
|
|
37
|
+
end?: number;
|
|
36
38
|
}
|
|
37
39
|
|
|
38
40
|
export interface ILabelNamesResult {
|
|
@@ -55,7 +57,7 @@ export const useLabelNames = (
|
|
|
55
57
|
const metadata = useGrpcMetadata();
|
|
56
58
|
|
|
57
59
|
const {data, isLoading, error} = useGrpcQuery<LabelsResponse>({
|
|
58
|
-
key: ['labelNames', profileType, match?.join(',')],
|
|
60
|
+
key: ['labelNames', profileType, match?.join(','), start, end],
|
|
59
61
|
queryFn: async () => {
|
|
60
62
|
const request: LabelsRequest = {match: match !== undefined ? match : []};
|
|
61
63
|
if (start !== undefined && end !== undefined) {
|
|
@@ -89,14 +91,20 @@ interface UseLabelValues {
|
|
|
89
91
|
export const useLabelValues = (
|
|
90
92
|
client: QueryServiceClient,
|
|
91
93
|
labelName: string,
|
|
92
|
-
profileType: string
|
|
94
|
+
profileType: string,
|
|
95
|
+
start?: number,
|
|
96
|
+
end?: number
|
|
93
97
|
): UseLabelValues => {
|
|
94
98
|
const metadata = useGrpcMetadata();
|
|
95
99
|
|
|
96
100
|
const {data, isLoading, error} = useGrpcQuery<string[]>({
|
|
97
|
-
key: ['labelValues', labelName, profileType],
|
|
101
|
+
key: ['labelValues', labelName, profileType, start, end],
|
|
98
102
|
queryFn: async () => {
|
|
99
103
|
const request: ValuesRequest = {labelName, match: [], profileType};
|
|
104
|
+
if (start !== undefined && end !== undefined) {
|
|
105
|
+
request.start = millisToProtoTimestamp(start);
|
|
106
|
+
request.end = millisToProtoTimestamp(end);
|
|
107
|
+
}
|
|
100
108
|
const {response} = await client.values(request, {meta: metadata});
|
|
101
109
|
return sanitizeLabelValue(response.labelValues);
|
|
102
110
|
},
|
|
@@ -321,7 +329,12 @@ const MatchersInput = ({
|
|
|
321
329
|
|
|
322
330
|
export default function MatchersInputWithProvider(props: MatchersInputProps): JSX.Element {
|
|
323
331
|
return (
|
|
324
|
-
<LabelsProvider
|
|
332
|
+
<LabelsProvider
|
|
333
|
+
queryClient={props.queryClient}
|
|
334
|
+
profileType={props.profileType}
|
|
335
|
+
start={props.start}
|
|
336
|
+
end={props.end}
|
|
337
|
+
>
|
|
325
338
|
<MatchersInput {...props} />
|
|
326
339
|
</LabelsProvider>
|
|
327
340
|
);
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
import {Icon} from '@iconify/react';
|
|
15
15
|
import {Table} from 'apache-arrow';
|
|
16
|
+
import cx from 'classnames';
|
|
16
17
|
import {Item, Menu, Separator, Submenu} from 'react-contexify';
|
|
17
18
|
import {Tooltip} from 'react-tooltip';
|
|
18
19
|
|
|
@@ -144,7 +145,15 @@ const ContextMenu = ({
|
|
|
144
145
|
const nonEmptyValuesToCopy = valuesToCopy.filter(({value}) => value !== '');
|
|
145
146
|
|
|
146
147
|
return (
|
|
147
|
-
<Menu
|
|
148
|
+
<Menu
|
|
149
|
+
id={menuId}
|
|
150
|
+
theme={isDarkMode ? 'dark' : ''}
|
|
151
|
+
className={cx(
|
|
152
|
+
dashboardItems.includes('sandwich')
|
|
153
|
+
? 'min-w-[350px] w-[350px]'
|
|
154
|
+
: 'min-w-[260px] w-fit-content'
|
|
155
|
+
)}
|
|
156
|
+
>
|
|
148
157
|
<Item
|
|
149
158
|
id="view-source-file"
|
|
150
159
|
onClick={handleViewSourceFile}
|
|
@@ -181,14 +190,21 @@ const ContextMenu = ({
|
|
|
181
190
|
<Item
|
|
182
191
|
id="show-in-sandwich"
|
|
183
192
|
onClick={() => {
|
|
193
|
+
if (dashboardItems.includes('sandwich')) {
|
|
194
|
+
setSandwichFunctionName(functionName);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
184
198
|
setSandwichFunctionName(functionName);
|
|
185
|
-
setDashboardItems(['sandwich']);
|
|
199
|
+
setDashboardItems([...dashboardItems, 'sandwich']);
|
|
186
200
|
}}
|
|
187
201
|
>
|
|
188
202
|
<div className="flex w-full items-center gap-2">
|
|
189
203
|
<Icon icon="tdesign:sandwich-filled" />
|
|
190
204
|
<div className="relative">
|
|
191
|
-
|
|
205
|
+
{dashboardItems.includes('sandwich')
|
|
206
|
+
? 'Focus sandwich on this frame.'
|
|
207
|
+
: 'Show in sandwich'}
|
|
192
208
|
<span className="absolute top-[-2px] text-xs lowercase text-red-500">
|
|
193
209
|
alpha
|
|
194
210
|
</span>
|
|
@@ -34,15 +34,23 @@ interface ContextMenuWrapperProps {
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
export interface ContextMenuWrapperRef {
|
|
37
|
-
setRow: (row: number) => void;
|
|
37
|
+
setRow: (row: number, callback?: () => void) => void;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
const ContextMenuWrapper = forwardRef<ContextMenuWrapperRef, ContextMenuWrapperProps>(
|
|
41
41
|
(props, ref) => {
|
|
42
|
+
// Fix for race condition: Always render ContextMenu to maintain component tree stability
|
|
43
|
+
// but use callback timing to ensure correct data is available when menu shows
|
|
42
44
|
const [row, setRow] = useState(0);
|
|
43
45
|
|
|
44
46
|
useImperativeHandle(ref, () => ({
|
|
45
|
-
setRow,
|
|
47
|
+
setRow: (newRow: number, callback?: () => void) => {
|
|
48
|
+
setRow(newRow);
|
|
49
|
+
// Execute callback after state update using requestAnimationFrame
|
|
50
|
+
if (callback != null) {
|
|
51
|
+
requestAnimationFrame(callback);
|
|
52
|
+
}
|
|
53
|
+
},
|
|
46
54
|
}));
|
|
47
55
|
|
|
48
56
|
return <ContextMenu {...props} row={row} />;
|
|
@@ -64,6 +64,7 @@ export interface IcicleNodeProps {
|
|
|
64
64
|
isFlamegraph?: boolean;
|
|
65
65
|
isSandwich?: boolean;
|
|
66
66
|
maxDepth?: number;
|
|
67
|
+
effectiveDepth?: number;
|
|
67
68
|
tooltipId?: string;
|
|
68
69
|
|
|
69
70
|
// Hovering row must only ever be used for highlighting similar nodes, otherwise it will cause performance issues as it causes the full iciclegraph to get rerendered every time the hovering row changes.
|
|
@@ -102,6 +103,7 @@ export const IcicleNode = React.memo(function IcicleNodeNoMemo({
|
|
|
102
103
|
isFlamegraph = false,
|
|
103
104
|
isSandwich = false,
|
|
104
105
|
maxDepth = 0,
|
|
106
|
+
effectiveDepth,
|
|
105
107
|
tooltipId = 'default',
|
|
106
108
|
}: IcicleNodeProps): React.JSX.Element {
|
|
107
109
|
// get the columns to read from
|
|
@@ -115,12 +117,15 @@ export const IcicleNode = React.memo(function IcicleNodeNoMemo({
|
|
|
115
117
|
const tsColumn = table.getChild(FIELD_TIMESTAMP);
|
|
116
118
|
|
|
117
119
|
// get the actual values from the columns
|
|
120
|
+
const binaries = useAppSelector(selectBinaries);
|
|
121
|
+
|
|
118
122
|
const mappingFile: string | null = arrowToString(mappingColumn?.get(row));
|
|
119
123
|
const functionName: string | null = arrowToString(functionNameColumn?.get(row));
|
|
120
124
|
const cumulative = cumulativeColumn?.get(row) !== null ? BigInt(cumulativeColumn?.get(row)) : 0n;
|
|
121
125
|
const diff: bigint | null = diffColumn?.get(row) !== null ? BigInt(diffColumn?.get(row)) : null;
|
|
122
126
|
const filename: string | null = arrowToString(filenameColumn?.get(row));
|
|
123
127
|
const depth: number = depthColumn?.get(row) ?? 0;
|
|
128
|
+
|
|
124
129
|
const valueOffset: bigint =
|
|
125
130
|
valueOffsetColumn?.get(row) !== null && valueOffsetColumn?.get(row) !== undefined
|
|
126
131
|
? BigInt(valueOffsetColumn?.get(row))
|
|
@@ -136,7 +141,6 @@ export const IcicleNode = React.memo(function IcicleNodeNoMemo({
|
|
|
136
141
|
const shouldBeHighlighted =
|
|
137
142
|
functionName != null && hoveringName != null && functionName === hoveringName;
|
|
138
143
|
|
|
139
|
-
const binaries = useAppSelector(selectBinaries);
|
|
140
144
|
const colorResult = useNodeColor({
|
|
141
145
|
isDarkMode: darkMode,
|
|
142
146
|
compareMode,
|
|
@@ -145,6 +149,7 @@ export const IcicleNode = React.memo(function IcicleNodeNoMemo({
|
|
|
145
149
|
colorsMap,
|
|
146
150
|
colorAttribute,
|
|
147
151
|
});
|
|
152
|
+
|
|
148
153
|
const name = useMemo(() => {
|
|
149
154
|
return row === 0 ? 'root' : nodeLabel(table, row, binaries.length > 1);
|
|
150
155
|
}, [table, row, binaries]);
|
|
@@ -156,6 +161,11 @@ export const IcicleNode = React.memo(function IcicleNodeNoMemo({
|
|
|
156
161
|
return {isHighlightEnabled: true, isHighlighted: isSearchMatch(searchString, name)};
|
|
157
162
|
}, [searchString, name]);
|
|
158
163
|
|
|
164
|
+
// Hide frames beyond effective depth limit
|
|
165
|
+
if (effectiveDepth !== undefined && depth > effectiveDepth) {
|
|
166
|
+
return <></>;
|
|
167
|
+
}
|
|
168
|
+
|
|
159
169
|
const selectionOffset =
|
|
160
170
|
valueOffsetColumn?.get(selectedRow) !== null &&
|
|
161
171
|
valueOffsetColumn?.get(selectedRow) !== undefined
|
|
@@ -241,7 +251,14 @@ export const IcicleNode = React.memo(function IcicleNodeNoMemo({
|
|
|
241
251
|
return depth * height;
|
|
242
252
|
};
|
|
243
253
|
|
|
244
|
-
const y = calculateY(
|
|
254
|
+
const y = calculateY(
|
|
255
|
+
isFlamegraph,
|
|
256
|
+
isSandwich,
|
|
257
|
+
isIcicleChart,
|
|
258
|
+
effectiveDepth ?? maxDepth,
|
|
259
|
+
depth,
|
|
260
|
+
height
|
|
261
|
+
);
|
|
245
262
|
|
|
246
263
|
return (
|
|
247
264
|
<>
|
|
@@ -39,11 +39,24 @@ export const MemoizedTooltip = memo(function MemoizedTooltip({
|
|
|
39
39
|
};
|
|
40
40
|
|
|
41
41
|
const eventName = `icicle-tooltip-update-${tooltipId}`;
|
|
42
|
-
|
|
42
|
+
|
|
43
|
+
// Delay to ensure all DOM updates and React renders are complete
|
|
44
|
+
// This fixes the race condition in sandwich view
|
|
45
|
+
const timeoutId = setTimeout(() => {
|
|
46
|
+
window.addEventListener(eventName as any, handleTooltipUpdate as any);
|
|
47
|
+
}, 200);
|
|
48
|
+
|
|
43
49
|
return () => {
|
|
50
|
+
clearTimeout(timeoutId);
|
|
44
51
|
window.removeEventListener(eventName as any, handleTooltipUpdate as any);
|
|
45
52
|
};
|
|
46
|
-
}, [tooltipId]);
|
|
53
|
+
}, [tooltipId, table]);
|
|
54
|
+
|
|
55
|
+
// Re-render when contextElement becomes available (fixes sandwich view timing issue)
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
// Force re-render when contextElement transitions from null to valid element
|
|
58
|
+
// This ensures tooltips work immediately in sandwich view
|
|
59
|
+
}, [contextElement, tooltipId]);
|
|
47
60
|
|
|
48
61
|
if (dockedMetainfo) {
|
|
49
62
|
return (
|
|
@@ -63,6 +76,11 @@ export const MemoizedTooltip = memo(function MemoizedTooltip({
|
|
|
63
76
|
return null;
|
|
64
77
|
}
|
|
65
78
|
|
|
79
|
+
// Fix for sandwich view tooltip issue: Don't render tooltip if contextElement is null
|
|
80
|
+
// This happens when the SVG ref isn't ready yet during initial sandwich view render
|
|
81
|
+
if (contextElement === null) {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
66
84
|
return (
|
|
67
85
|
<GraphTooltipArrow contextElement={contextElement}>
|
|
68
86
|
<GraphTooltipArrowContent
|
|
@@ -11,7 +11,15 @@
|
|
|
11
11
|
// See the License for the specific language governing permissions and
|
|
12
12
|
// limitations under the License.
|
|
13
13
|
|
|
14
|
-
import React, {
|
|
14
|
+
import React, {
|
|
15
|
+
memo,
|
|
16
|
+
useCallback,
|
|
17
|
+
useDeferredValue,
|
|
18
|
+
useEffect,
|
|
19
|
+
useMemo,
|
|
20
|
+
useRef,
|
|
21
|
+
useState,
|
|
22
|
+
} from 'react';
|
|
15
23
|
|
|
16
24
|
import {Dictionary, Table, Vector, tableFromIPC} from 'apache-arrow';
|
|
17
25
|
import {useContextMenu} from 'react-contexify';
|
|
@@ -77,6 +85,8 @@ interface IcicleGraphArrowProps {
|
|
|
77
85
|
isFlamegraph?: boolean;
|
|
78
86
|
isSandwich?: boolean;
|
|
79
87
|
tooltipId?: string;
|
|
88
|
+
maxFrameCount?: number;
|
|
89
|
+
isExpanded?: boolean;
|
|
80
90
|
}
|
|
81
91
|
|
|
82
92
|
export const getMappingColors = (
|
|
@@ -135,6 +145,8 @@ export const IcicleGraphArrow = memo(function IcicleGraphArrow({
|
|
|
135
145
|
isFlamegraph = false,
|
|
136
146
|
isSandwich = false,
|
|
137
147
|
tooltipId = 'default',
|
|
148
|
+
maxFrameCount,
|
|
149
|
+
isExpanded = false,
|
|
138
150
|
}: IcicleGraphArrowProps): React.JSX.Element {
|
|
139
151
|
const [highlightSimilarStacksPreference] = useUserPreference<boolean>(
|
|
140
152
|
USER_PREFERENCES.HIGHLIGHT_SIMILAR_STACKS.key
|
|
@@ -148,6 +160,12 @@ export const IcicleGraphArrow = memo(function IcicleGraphArrow({
|
|
|
148
160
|
}, [arrow]);
|
|
149
161
|
const svg = useRef(null);
|
|
150
162
|
|
|
163
|
+
const [svgElement, setSvgElement] = useState<SVGSVGElement | null>(null);
|
|
164
|
+
|
|
165
|
+
useEffect(() => {
|
|
166
|
+
setSvgElement(svg.current);
|
|
167
|
+
}, [tooltipId]);
|
|
168
|
+
|
|
151
169
|
const [binaryFrameFilter, setBinaryFrameFilter] = useURLState('binary_frame_filter');
|
|
152
170
|
|
|
153
171
|
const [currentSearchString] = useURLState('search_string');
|
|
@@ -215,9 +233,14 @@ export const IcicleGraphArrow = memo(function IcicleGraphArrow({
|
|
|
215
233
|
});
|
|
216
234
|
const displayMenu = useCallback(
|
|
217
235
|
(e: React.MouseEvent, row: number): void => {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
236
|
+
e.preventDefault();
|
|
237
|
+
// Race condition fix: Use callback to ensure context menu shows only after
|
|
238
|
+
// row state has been updated and propagated through the hook chain.
|
|
239
|
+
// This prevents empty function names on first click.
|
|
240
|
+
contextMenuRef.current?.setRow(row, () => {
|
|
241
|
+
show({
|
|
242
|
+
event: e,
|
|
243
|
+
});
|
|
221
244
|
});
|
|
222
245
|
},
|
|
223
246
|
[show]
|
|
@@ -255,7 +278,17 @@ export const IcicleGraphArrow = memo(function IcicleGraphArrow({
|
|
|
255
278
|
|
|
256
279
|
const depthColumn = table.getChild(FIELD_DEPTH);
|
|
257
280
|
const maxDepth = getMaxDepth(depthColumn);
|
|
258
|
-
|
|
281
|
+
|
|
282
|
+
// Apply frame limit if maxFrameCount is provided and not expanded
|
|
283
|
+
const effectiveDepth =
|
|
284
|
+
maxFrameCount !== undefined && !isExpanded ? Math.min(maxDepth, maxFrameCount) : maxDepth;
|
|
285
|
+
|
|
286
|
+
// Use deferred value to prevent UI blocking when expanding frames
|
|
287
|
+
const deferredEffectiveDepth = useDeferredValue(effectiveDepth);
|
|
288
|
+
|
|
289
|
+
const height = isSandwich
|
|
290
|
+
? deferredEffectiveDepth * RowHeight
|
|
291
|
+
: (deferredEffectiveDepth + 1) * RowHeight;
|
|
259
292
|
|
|
260
293
|
// To find the selected row, we must walk the current path and look at which
|
|
261
294
|
// children of the current frame matches the path element exactly. Until the
|
|
@@ -307,7 +340,7 @@ export const IcicleGraphArrow = memo(function IcicleGraphArrow({
|
|
|
307
340
|
profileType={profileType}
|
|
308
341
|
isSandwich={isSandwich}
|
|
309
342
|
/>
|
|
310
|
-
<MemoizedTooltip contextElement={
|
|
343
|
+
<MemoizedTooltip contextElement={svgElement} dockedMetainfo={dockedMetainfo} />
|
|
311
344
|
<svg
|
|
312
345
|
className="font-robotoMono"
|
|
313
346
|
width={width}
|
|
@@ -344,6 +377,7 @@ export const IcicleGraphArrow = memo(function IcicleGraphArrow({
|
|
|
344
377
|
isFlamegraph={isFlamegraph}
|
|
345
378
|
isSandwich={isSandwich}
|
|
346
379
|
maxDepth={maxDepth}
|
|
380
|
+
effectiveDepth={deferredEffectiveDepth}
|
|
347
381
|
tooltipId={tooltipId}
|
|
348
382
|
/>
|
|
349
383
|
))}
|
|
@@ -18,7 +18,12 @@ import {AnimatePresence, motion} from 'framer-motion';
|
|
|
18
18
|
import {useMeasure} from 'react-use';
|
|
19
19
|
|
|
20
20
|
import {FlamegraphArrow} from '@parca/client';
|
|
21
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
FlamegraphSkeleton,
|
|
23
|
+
IcicleGraphSkeleton,
|
|
24
|
+
useParcaContext,
|
|
25
|
+
useURLState,
|
|
26
|
+
} from '@parca/components';
|
|
22
27
|
import {ProfileType} from '@parca/parser';
|
|
23
28
|
import {capitalizeOnlyFirstLetter, divide} from '@parca/utilities';
|
|
24
29
|
|
|
@@ -53,6 +58,8 @@ interface ProfileIcicleGraphProps {
|
|
|
53
58
|
isSandwichIcicleGraph?: boolean;
|
|
54
59
|
isFlamegraph?: boolean;
|
|
55
60
|
tooltipId?: string;
|
|
61
|
+
maxFrameCount?: number;
|
|
62
|
+
isExpanded?: boolean;
|
|
56
63
|
}
|
|
57
64
|
|
|
58
65
|
const ErrorContent = ({errorMessage}: {errorMessage: string | ReactNode}): JSX.Element => {
|
|
@@ -88,6 +95,8 @@ const ProfileIcicleGraph = function ProfileIcicleGraphNonMemo({
|
|
|
88
95
|
isSandwichIcicleGraph = false,
|
|
89
96
|
isFlamegraph = false,
|
|
90
97
|
tooltipId,
|
|
98
|
+
maxFrameCount,
|
|
99
|
+
isExpanded = false,
|
|
91
100
|
}: ProfileIcicleGraphProps): JSX.Element {
|
|
92
101
|
const {onError, authenticationErrorMessage, isDarkMode, iciclechartHelpText} = useParcaContext();
|
|
93
102
|
const {compareMode} = useProfileViewContext();
|
|
@@ -184,10 +193,15 @@ const ProfileIcicleGraph = function ProfileIcicleGraphNonMemo({
|
|
|
184
193
|
? validateIcicleChartQuery(profileSource as MergedProfileSource)
|
|
185
194
|
: {isValid: true, isNonDelta: false, isDurationTooLong: false};
|
|
186
195
|
const isInvalidIcicleChartQuery = isIcicleChart && !isIcicleChartValid;
|
|
196
|
+
|
|
187
197
|
if (isLoading && !isInvalidIcicleChartQuery) {
|
|
188
198
|
return (
|
|
189
199
|
<div className="h-auto overflow-clip">
|
|
190
|
-
|
|
200
|
+
{isFlamegraph ? (
|
|
201
|
+
<FlamegraphSkeleton isHalfScreen={isHalfScreen} isDarkMode={isDarkMode} />
|
|
202
|
+
) : (
|
|
203
|
+
<IcicleGraphSkeleton isHalfScreen={isHalfScreen} isDarkMode={isDarkMode} />
|
|
204
|
+
)}
|
|
191
205
|
</div>
|
|
192
206
|
);
|
|
193
207
|
}
|
|
@@ -268,6 +282,8 @@ const ProfileIcicleGraph = function ProfileIcicleGraphNonMemo({
|
|
|
268
282
|
isFlamegraph={isFlamegraph}
|
|
269
283
|
isSandwich={isSandwichIcicleGraph}
|
|
270
284
|
tooltipId={tooltipId}
|
|
285
|
+
maxFrameCount={maxFrameCount}
|
|
286
|
+
isExpanded={isExpanded}
|
|
271
287
|
/>
|
|
272
288
|
</div>
|
|
273
289
|
</div>
|
|
@@ -295,6 +311,8 @@ const ProfileIcicleGraph = function ProfileIcicleGraphNonMemo({
|
|
|
295
311
|
effectiveCurPathArrow,
|
|
296
312
|
setCurPathArrowWrapper,
|
|
297
313
|
tooltipId,
|
|
314
|
+
maxFrameCount,
|
|
315
|
+
isExpanded,
|
|
298
316
|
]);
|
|
299
317
|
|
|
300
318
|
useEffect(() => {
|
|
@@ -148,6 +148,8 @@ export function QueryControls({
|
|
|
148
148
|
runQuery={setQueryExpression}
|
|
149
149
|
currentQuery={query}
|
|
150
150
|
queryClient={queryClient}
|
|
151
|
+
start={timeRangeSelection.getFromMs()}
|
|
152
|
+
end={timeRangeSelection.getToMs()}
|
|
151
153
|
/>
|
|
152
154
|
) : advancedModeForQueryBrowser ? (
|
|
153
155
|
<MatchersInput
|
|
@@ -156,6 +158,8 @@ export function QueryControls({
|
|
|
156
158
|
currentQuery={query}
|
|
157
159
|
profileType={selectedProfileName}
|
|
158
160
|
queryClient={queryClient}
|
|
161
|
+
start={timeRangeSelection.getFromMs()}
|
|
162
|
+
end={timeRangeSelection.getToMs()}
|
|
159
163
|
/>
|
|
160
164
|
) : (
|
|
161
165
|
<SimpleMatchers
|
|
@@ -166,6 +170,8 @@ export function QueryControls({
|
|
|
166
170
|
profileType={selectedProfileName}
|
|
167
171
|
queryBrowserRef={queryBrowserRef}
|
|
168
172
|
queryClient={queryClient}
|
|
173
|
+
start={timeRangeSelection.getFromMs()}
|
|
174
|
+
end={timeRangeSelection.getToMs()}
|
|
169
175
|
/>
|
|
170
176
|
)}
|
|
171
177
|
</div>
|
|
@@ -170,10 +170,20 @@ const ProfileSelector = ({
|
|
|
170
170
|
return Query.parse(querySelection.expression).profileType();
|
|
171
171
|
}, [querySelection.expression]);
|
|
172
172
|
|
|
173
|
-
const
|
|
173
|
+
const from = timeRangeSelection.getFromMs();
|
|
174
|
+
const to = timeRangeSelection.getToMs();
|
|
175
|
+
|
|
176
|
+
const {loading: labelNamesLoading, result} = useLabelNames(
|
|
177
|
+
queryClient,
|
|
178
|
+
profileType.toString(),
|
|
179
|
+
from,
|
|
180
|
+
to
|
|
181
|
+
);
|
|
174
182
|
const {loading: selectedLabelNamesLoading, result: selectedLabelNamesResult} = useLabelNames(
|
|
175
183
|
queryClient,
|
|
176
|
-
selectedProfileType.toString()
|
|
184
|
+
selectedProfileType.toString(),
|
|
185
|
+
from,
|
|
186
|
+
to
|
|
177
187
|
);
|
|
178
188
|
|
|
179
189
|
const labels = useMemo(() => {
|
|
@@ -155,7 +155,6 @@ export const getDashboardItem = ({
|
|
|
155
155
|
data={topTableData.arrow?.record}
|
|
156
156
|
unit={topTableData.unit}
|
|
157
157
|
profileType={profileSource?.ProfileType()}
|
|
158
|
-
isHalfScreen={isHalfScreen}
|
|
159
158
|
metadataMappingFiles={flamegraphData.metadataMappingFiles}
|
|
160
159
|
profileSource={profileSource}
|
|
161
160
|
queryClient={queryClient}
|
|
@@ -182,11 +182,17 @@ const ShareButton = ({
|
|
|
182
182
|
element={
|
|
183
183
|
<Button
|
|
184
184
|
variant="neutral"
|
|
185
|
-
className="flex items-center gap-2"
|
|
185
|
+
className="flex items-center gap-2 pr-[1.7rem]"
|
|
186
186
|
id="h-share-dropdown-button"
|
|
187
187
|
>
|
|
188
|
-
<
|
|
189
|
-
|
|
188
|
+
<div className="flex items-center gap-2">
|
|
189
|
+
<Icon icon="material-symbols:share" className="w-4 h-4" />
|
|
190
|
+
|
|
191
|
+
<span>Share</span>
|
|
192
|
+
</div>
|
|
193
|
+
<div className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2 text-gray-400">
|
|
194
|
+
<Icon icon="heroicons:chevron-down-20-solid" aria-hidden="true" />
|
|
195
|
+
</div>
|
|
190
196
|
</Button>
|
|
191
197
|
}
|
|
192
198
|
>
|
|
@@ -16,7 +16,7 @@ import {FC} from 'react';
|
|
|
16
16
|
import {Icon} from '@iconify/react';
|
|
17
17
|
|
|
18
18
|
import {QueryServiceClient} from '@parca/client';
|
|
19
|
-
import {Button
|
|
19
|
+
import {Button} from '@parca/components';
|
|
20
20
|
import {ProfileType} from '@parca/parser';
|
|
21
21
|
|
|
22
22
|
import {CurrentPathFrame} from '../../../ProfileIcicleGraph/IcicleGraphArrow/utils';
|
|
@@ -52,8 +52,6 @@ export interface VisualisationToolbarProps {
|
|
|
52
52
|
setGroupByLabels: (labels: string[]) => void;
|
|
53
53
|
showVisualizationSelector?: boolean;
|
|
54
54
|
sandwichFunctionName?: string;
|
|
55
|
-
setSandwichFunctionName: (sandwichFunctionName: string | undefined) => void;
|
|
56
|
-
resetSandwichFunctionName: () => void;
|
|
57
55
|
}
|
|
58
56
|
|
|
59
57
|
export interface TableToolbarProps {
|
|
@@ -150,7 +148,6 @@ export const VisualisationToolbar: FC<VisualisationToolbarProps> = ({
|
|
|
150
148
|
groupByLabels,
|
|
151
149
|
setGroupByLabels,
|
|
152
150
|
profileType,
|
|
153
|
-
preferencesModal,
|
|
154
151
|
profileSource,
|
|
155
152
|
queryClient,
|
|
156
153
|
onDownloadPProf,
|
|
@@ -163,17 +160,13 @@ export const VisualisationToolbar: FC<VisualisationToolbarProps> = ({
|
|
|
163
160
|
currentSearchString,
|
|
164
161
|
clearSelection,
|
|
165
162
|
showVisualizationSelector = true,
|
|
166
|
-
resetSandwichFunctionName,
|
|
167
|
-
sandwichFunctionName,
|
|
168
163
|
}) => {
|
|
169
164
|
const {dashboardItems} = useDashboard();
|
|
170
165
|
|
|
171
166
|
const isTableViz = dashboardItems?.includes('table');
|
|
172
167
|
const isTableVizOnly = dashboardItems?.length === 1 && isTableViz;
|
|
173
168
|
const isGraphViz = dashboardItems?.includes('icicle');
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
const isTableView = isTableVizOnly || isSandwichIcicleGraphViz;
|
|
169
|
+
const isGraphVizOnly = dashboardItems?.length === 1 && isGraphViz;
|
|
177
170
|
|
|
178
171
|
const req = profileSource?.QueryRequest();
|
|
179
172
|
if (req !== null && req !== undefined) {
|
|
@@ -186,7 +179,7 @@ export const VisualisationToolbar: FC<VisualisationToolbarProps> = ({
|
|
|
186
179
|
<>
|
|
187
180
|
<div className="flex w-full justify-between items-end">
|
|
188
181
|
<div className="flex gap-3 items-end">
|
|
189
|
-
{
|
|
182
|
+
{isGraphViz && (
|
|
190
183
|
<>
|
|
191
184
|
<GroupByDropdown
|
|
192
185
|
groupBy={groupBy}
|
|
@@ -203,13 +196,12 @@ export const VisualisationToolbar: FC<VisualisationToolbarProps> = ({
|
|
|
203
196
|
{profileViewExternalSubActions != null ? profileViewExternalSubActions : null}
|
|
204
197
|
</div>
|
|
205
198
|
<div className="flex gap-3">
|
|
206
|
-
{preferencesModal === true && <UserPreferencesModal />}
|
|
207
199
|
<MultiLevelDropdown
|
|
208
200
|
groupBy={groupBy}
|
|
209
201
|
toggleGroupBy={toggleGroupBy}
|
|
210
202
|
profileType={profileType}
|
|
211
203
|
onSelect={() => {}}
|
|
212
|
-
isTableVizOnly={
|
|
204
|
+
isTableVizOnly={isTableVizOnly}
|
|
213
205
|
/>
|
|
214
206
|
|
|
215
207
|
<ShareButton
|
|
@@ -224,13 +216,14 @@ export const VisualisationToolbar: FC<VisualisationToolbarProps> = ({
|
|
|
224
216
|
{showVisualizationSelector ? <ViewSelector profileSource={profileSource} /> : null}
|
|
225
217
|
</div>
|
|
226
218
|
</div>
|
|
227
|
-
|
|
219
|
+
|
|
220
|
+
{isGraphVizOnly && (
|
|
228
221
|
<>
|
|
229
222
|
<Divider />
|
|
230
223
|
<IcicleGraphToolbar curPath={curPath} setNewCurPath={setNewCurPath} />
|
|
231
224
|
</>
|
|
232
225
|
)}
|
|
233
|
-
{
|
|
226
|
+
{isTableVizOnly && (
|
|
234
227
|
<>
|
|
235
228
|
<Divider />
|
|
236
229
|
<TableToolbar
|
|
@@ -242,15 +235,6 @@ export const VisualisationToolbar: FC<VisualisationToolbarProps> = ({
|
|
|
242
235
|
/>
|
|
243
236
|
</>
|
|
244
237
|
)}
|
|
245
|
-
{isSandwichIcicleGraphViz && (
|
|
246
|
-
<>
|
|
247
|
-
<Divider />
|
|
248
|
-
<SandwichIcicleGraphToolbar
|
|
249
|
-
resetSandwichFunctionName={resetSandwichFunctionName}
|
|
250
|
-
sandwichFunctionName={sandwichFunctionName}
|
|
251
|
-
/>
|
|
252
|
-
</>
|
|
253
|
-
)}
|
|
254
238
|
</>
|
|
255
239
|
);
|
|
256
240
|
};
|
|
@@ -29,6 +29,7 @@ const ViewSelector = ({profileSource}: Props): JSX.Element => {
|
|
|
29
29
|
alwaysReturnArray: true,
|
|
30
30
|
}
|
|
31
31
|
);
|
|
32
|
+
const [, setSandwichFunctionName] = useURLState<string | undefined>('sandwich_function_name');
|
|
32
33
|
const {enableSourcesView, enableSandwichView} = useParcaContext();
|
|
33
34
|
|
|
34
35
|
const allItems: Array<{
|
|
@@ -38,8 +39,8 @@ const ViewSelector = ({profileSource}: Props): JSX.Element => {
|
|
|
38
39
|
supportingText?: string;
|
|
39
40
|
disabledText?: string;
|
|
40
41
|
}> = [
|
|
41
|
-
{key: 'table', label: 'Table', canBeSelected: !dashboardItems.includes('table')},
|
|
42
42
|
{key: 'icicle', label: 'icicle', canBeSelected: !dashboardItems.includes('icicle')},
|
|
43
|
+
{key: 'table', label: 'Table', canBeSelected: !dashboardItems.includes('table')},
|
|
43
44
|
{
|
|
44
45
|
key: 'iciclechart',
|
|
45
46
|
label: (
|
|
@@ -105,14 +106,8 @@ const ViewSelector = ({profileSource}: Props): JSX.Element => {
|
|
|
105
106
|
}): InnerAction | undefined => {
|
|
106
107
|
if (dashboardItems.length === 1 && item.key === dashboardItems[0]) return undefined;
|
|
107
108
|
|
|
108
|
-
//
|
|
109
|
-
if (item.key
|
|
110
|
-
return {
|
|
111
|
-
text: 'Add Panel',
|
|
112
|
-
onClick: () => {},
|
|
113
|
-
isDisabled: true, // Custom property to control button state
|
|
114
|
-
};
|
|
115
|
-
}
|
|
109
|
+
// If we already have 2 panels and this item isn't selected, don't show any action
|
|
110
|
+
if (dashboardItems.length >= 2 && !dashboardItems.includes(item.key)) return undefined;
|
|
116
111
|
|
|
117
112
|
return {
|
|
118
113
|
text:
|
|
@@ -120,12 +115,20 @@ const ViewSelector = ({profileSource}: Props): JSX.Element => {
|
|
|
120
115
|
? 'Add Panel'
|
|
121
116
|
: item.canBeSelected
|
|
122
117
|
? 'Add Panel'
|
|
123
|
-
:
|
|
118
|
+
: dashboardItems.includes(item.key)
|
|
119
|
+
? 'Close Panel'
|
|
120
|
+
: 'Add Panel',
|
|
124
121
|
onClick: () => {
|
|
125
122
|
if (item.canBeSelected) {
|
|
126
123
|
setDashboardItems([...dashboardItems, item.key]);
|
|
127
124
|
} else {
|
|
128
|
-
|
|
125
|
+
const newDashboardItems = dashboardItems.filter(v => v !== item.key);
|
|
126
|
+
setDashboardItems(newDashboardItems);
|
|
127
|
+
|
|
128
|
+
// Reset sandwich function name when removing sandwich panel
|
|
129
|
+
if (item.key === 'sandwich') {
|
|
130
|
+
setSandwichFunctionName(undefined);
|
|
131
|
+
}
|
|
129
132
|
}
|
|
130
133
|
},
|
|
131
134
|
isDisabled: dashboardItems.length === 1 && dashboardItems.includes('sandwich'),
|
|
@@ -142,6 +145,12 @@ const ViewSelector = ({profileSource}: Props): JSX.Element => {
|
|
|
142
145
|
|
|
143
146
|
const onSelection = (value: string): void => {
|
|
144
147
|
const isOnlyChart = dashboardItems.length === 1;
|
|
148
|
+
|
|
149
|
+
if (isOnlyChart && value === 'sandwich') {
|
|
150
|
+
setDashboardItems([...dashboardItems, value]);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
145
154
|
if (isOnlyChart) {
|
|
146
155
|
setDashboardItems([value]);
|
|
147
156
|
return;
|
|
@@ -30,10 +30,16 @@ export const DashboardProvider: FC<PropsWithChildren> = ({children}) => {
|
|
|
30
30
|
const [dashboardItems, setDashboardItems] = useURLState<string[]>('dashboard_items', {
|
|
31
31
|
alwaysReturnArray: true,
|
|
32
32
|
});
|
|
33
|
+
const [, setSandwichFunctionName] = useURLState<string | undefined>('sandwich_function_name');
|
|
33
34
|
|
|
34
35
|
const handleClosePanel = (visualizationType: VisualizationType): void => {
|
|
35
36
|
const newDashboardItems = dashboardItems.filter(item => item !== visualizationType);
|
|
36
37
|
setDashboardItems(newDashboardItems);
|
|
38
|
+
|
|
39
|
+
// Reset sandwich function name when closing sandwich panel
|
|
40
|
+
if (visualizationType === 'sandwich') {
|
|
41
|
+
setSandwichFunctionName(undefined);
|
|
42
|
+
}
|
|
37
43
|
};
|
|
38
44
|
|
|
39
45
|
const isMultiPanelView = dashboardItems.length > 1;
|