@parca/profile 0.16.409 → 0.16.411
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/ProfileMetricsGraph/index.d.ts.map +1 -1
- package/dist/ProfileMetricsGraph/index.js +33 -39
- package/dist/ProfileMetricsGraph/useSumBy.d.ts +1 -0
- package/dist/ProfileMetricsGraph/useSumBy.d.ts.map +1 -1
- package/dist/ProfileMetricsGraph/useSumBy.js +39 -6
- package/package.json +2 -2
- package/src/ProfileMetricsGraph/index.tsx +72 -63
- package/src/ProfileMetricsGraph/useSumBy.ts +53 -6
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.16.411](https://github.com/parca-dev/parca/compare/@parca/profile@0.16.410...@parca/profile@0.16.411) (2024-07-15)
|
|
7
|
+
|
|
8
|
+
**Note:** Version bump only for package @parca/profile
|
|
9
|
+
|
|
10
|
+
## [0.16.410](https://github.com/parca-dev/parca/compare/@parca/profile@0.16.409...@parca/profile@0.16.410) (2024-07-11)
|
|
11
|
+
|
|
12
|
+
**Note:** Version bump only for package @parca/profile
|
|
13
|
+
|
|
6
14
|
## [0.16.409](https://github.com/parca-dev/parca/compare/@parca/profile@0.16.408...@parca/profile@0.16.409) (2024-07-10)
|
|
7
15
|
|
|
8
16
|
**Note:** Version bump only for package @parca/profile
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/ProfileMetricsGraph/index.tsx"],"names":[],"mappings":"AAeA,OAAO,EAAC,QAAQ,EAAC,MAAM,0BAA0B,CAAC;AAGlD,OAAO,EAAW,KAAK,EAAE,kBAAkB,EAAE,kBAAkB,EAAY,MAAM,eAAe,CAAC;AACjG,OAAO,EACL,aAAa,EAKd,MAAM,mBAAmB,CAAC;AAI3B,OAAO,EAAyB,gBAAgB,EAAC,MAAM,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/ProfileMetricsGraph/index.tsx"],"names":[],"mappings":"AAeA,OAAO,EAAC,QAAQ,EAAC,MAAM,0BAA0B,CAAC;AAGlD,OAAO,EAAW,KAAK,EAAE,kBAAkB,EAAE,kBAAkB,EAAY,MAAM,eAAe,CAAC;AACjG,OAAO,EACL,aAAa,EAKd,MAAM,mBAAmB,CAAC;AAI3B,OAAO,EAAyB,gBAAgB,EAAC,MAAM,IAAI,CAAC;AAQ5D,UAAU,6BAA6B;IACrC,OAAO,EAAE,MAAM,CAAC;CACjB;AAaD,eAAO,MAAM,wBAAwB,gBAAe,6BAA6B,KAAG,GAAG,CAAC,OAMvF,CAAC;AAEF,UAAU,wBAAwB;IAChC,WAAW,EAAE,kBAAkB,CAAC;IAChC,eAAe,EAAE,MAAM,CAAC;IACxB,OAAO,EAAE,gBAAgB,GAAG,IAAI,CAAC;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,YAAY,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CAAC;IAC7C,eAAe,EAAE,CACf,MAAM,EAAE;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAC,GAAG,KAAK,CAAC;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAC,CAAC,KACvE,IAAI,CAAC;IACV,YAAY,EAAE,CACZ,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,KAAK,EAAE,EACf,eAAe,EAAE,MAAM,EACvB,QAAQ,EAAE,MAAM,KACb,IAAI,CAAC;IACV,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,kBAAkB,GAAG,IAAI,CAAC;IACpC,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,EAAE,QAAQ,GAAG,IAAI,CAAC;CACxB;AAYD,eAAO,MAAM,aAAa,WAChB,kBAAkB,mBACT,MAAM,SAChB,MAAM,OACR,MAAM,UACJ,MAAM,EAAE,qBAEd,gBA+CF,CAAC;AAEF,QAAA,MAAM,mBAAmB,iHAUtB,wBAAwB,KAAG,GAAG,CAAC,OA4GjC,CAAC;AAEF,eAAe,mBAAmB,CAAC"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
// Copyright 2022 The Parca Authors
|
|
3
3
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
4
|
// you may not use this file except in compliance with the License.
|
|
@@ -11,14 +11,18 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
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 { useEffect, useMemo
|
|
14
|
+
import { useEffect, useMemo } from 'react';
|
|
15
15
|
import { AnimatePresence, motion } from 'framer-motion';
|
|
16
16
|
import { Duration, Timestamp } from '@parca/client';
|
|
17
17
|
import { MetricsGraphSkeleton, useGrpcMetadata, useParcaContext, useURLState, } from '@parca/components';
|
|
18
18
|
import { Query } from '@parca/parser';
|
|
19
19
|
import { capitalizeOnlyFirstLetter, getStepDuration } from '@parca/utilities';
|
|
20
|
+
import { useLabelNames } from '../MatchersInput';
|
|
20
21
|
import MetricsGraph from '../MetricsGraph';
|
|
21
22
|
import { useMetricsGraphDimensions } from '../MetricsGraph/useMetricsGraphDimensions';
|
|
23
|
+
import useGrpcQuery from '../useGrpcQuery';
|
|
24
|
+
import { Toolbar } from './Toolbar';
|
|
25
|
+
import { DEFAULT_EMPTY_SUM_BY, useSumBy } from './useSumBy';
|
|
22
26
|
const ErrorContent = ({ errorMessage }) => {
|
|
23
27
|
return (_jsx("div", { className: "relative rounded border border-red-400 bg-red-100 px-4 py-3 text-red-700", role: "alert", children: _jsx("span", { className: "block sm:inline", children: errorMessage }) }));
|
|
24
28
|
};
|
|
@@ -33,14 +37,7 @@ const getStepCountFromScreenWidth = (pixelsPerPoint) => {
|
|
|
33
37
|
width = width - (20 + 24 + 68) * 2;
|
|
34
38
|
return Math.round(width / pixelsPerPoint);
|
|
35
39
|
};
|
|
36
|
-
const
|
|
37
|
-
export const useQueryRange = (client, queryExpression, start, end, sumBy = EMPTY_SUM_BY, skip = false) => {
|
|
38
|
-
const [isLoading, setLoading] = useState(!skip);
|
|
39
|
-
const [state, setState] = useState({
|
|
40
|
-
response: null,
|
|
41
|
-
isLoading,
|
|
42
|
-
error: null,
|
|
43
|
-
});
|
|
40
|
+
export const useQueryRange = (client, queryExpression, start, end, sumBy = DEFAULT_EMPTY_SUM_BY, skip = false) => {
|
|
44
41
|
const metadata = useGrpcMetadata();
|
|
45
42
|
const { navigateTo } = useParcaContext();
|
|
46
43
|
const [stepCountStr, setStepCount] = useURLState({ param: 'step_count', navigateTo });
|
|
@@ -58,14 +55,11 @@ export const useQueryRange = (client, queryExpression, start, end, sumBy = EMPTY
|
|
|
58
55
|
setStepCount(defaultStepCount.toString());
|
|
59
56
|
}
|
|
60
57
|
}, [stepCountStr, defaultStepCount, setStepCount]);
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
|
-
setLoading(true);
|
|
58
|
+
const { data, isLoading, error } = useGrpcQuery({
|
|
59
|
+
key: ['query-range', queryExpression, start, end, sumBy.join(','), stepCount, metadata],
|
|
60
|
+
queryFn: async () => {
|
|
67
61
|
const stepDuration = getStepDuration(start, end, stepCount);
|
|
68
|
-
const
|
|
62
|
+
const { response } = await client.queryRange({
|
|
69
63
|
query: queryExpression,
|
|
70
64
|
start: Timestamp.fromDate(new Date(start)),
|
|
71
65
|
end: Timestamp.fromDate(new Date(end)),
|
|
@@ -73,22 +67,23 @@ export const useQueryRange = (client, queryExpression, start, end, sumBy = EMPTY
|
|
|
73
67
|
limit: 0,
|
|
74
68
|
sumBy,
|
|
75
69
|
}, { meta: metadata });
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
});
|
|
86
|
-
})();
|
|
87
|
-
}, [client, queryExpression, start, end, metadata, sumBy, skip, stepCount]);
|
|
88
|
-
return { ...state, isLoading };
|
|
70
|
+
return response;
|
|
71
|
+
},
|
|
72
|
+
options: {
|
|
73
|
+
retry: false,
|
|
74
|
+
enabled: !skip && sumBy !== DEFAULT_EMPTY_SUM_BY,
|
|
75
|
+
staleTime: 1000 * 60 * 5, // 5 minutes
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
return { isLoading, error: error, response: data ?? null };
|
|
89
79
|
};
|
|
90
80
|
const ProfileMetricsGraph = ({ queryClient, queryExpression, profile, from, to, setTimeRange, addLabelMatcher, onPointClick, comparing = false, }) => {
|
|
91
|
-
const
|
|
81
|
+
const profileType = useMemo(() => {
|
|
82
|
+
return Query.parse(queryExpression).profileType();
|
|
83
|
+
}, [queryExpression]);
|
|
84
|
+
const { loading: labelNamesLoading, result: labelNamesResult } = useLabelNames(queryClient, profileType.toString() ?? '', from, to);
|
|
85
|
+
const [sumBy, setSumBy] = useSumBy(profileType, labelNamesResult.response?.labelNames);
|
|
86
|
+
const { isLoading: metricsGraphLoading, response, error, } = useQueryRange(queryClient, queryExpression, from, to, sumBy, labelNamesLoading);
|
|
92
87
|
const { onError, perf, authenticationErrorMessage, isDarkMode } = useParcaContext();
|
|
93
88
|
const { width, height, margin, heightStyle } = useMetricsGraphDimensions(comparing);
|
|
94
89
|
useEffect(() => {
|
|
@@ -104,8 +99,9 @@ const ProfileMetricsGraph = ({ queryClient, queryExpression, profile, from, to,
|
|
|
104
99
|
}, [perf, response]);
|
|
105
100
|
const series = response?.series;
|
|
106
101
|
const dataAvailable = series !== null && series !== undefined && series?.length > 0;
|
|
107
|
-
|
|
108
|
-
|
|
102
|
+
const loading = metricsGraphLoading || labelNamesLoading;
|
|
103
|
+
if (!labelNamesLoading && labelNamesResult?.error?.message != null) {
|
|
104
|
+
return (_jsx(ErrorContent, { errorMessage: capitalizeOnlyFirstLetter(labelNamesResult.error.message) }));
|
|
109
105
|
}
|
|
110
106
|
if (!metricsGraphLoading && error !== null) {
|
|
111
107
|
if (authenticationErrorMessage !== undefined && error.code === 'UNAUTHENTICATED') {
|
|
@@ -113,19 +109,17 @@ const ProfileMetricsGraph = ({ queryClient, queryExpression, profile, from, to,
|
|
|
113
109
|
}
|
|
114
110
|
return _jsx(ErrorContent, { errorMessage: capitalizeOnlyFirstLetter(error.message) });
|
|
115
111
|
}
|
|
112
|
+
let sampleUnit = '';
|
|
116
113
|
if (dataAvailable) {
|
|
117
|
-
const handleSampleClick = (timestamp, _value, labels, duration) => {
|
|
118
|
-
onPointClick(timestamp, labels, queryExpression, duration);
|
|
119
|
-
};
|
|
120
|
-
let sampleUnit = '';
|
|
121
114
|
if (series.every((val, i, arr) => val?.sampleType?.unit === arr[0]?.sampleType?.unit)) {
|
|
122
115
|
sampleUnit = series[0]?.sampleType?.unit ?? '';
|
|
123
116
|
}
|
|
124
117
|
if (sampleUnit === '') {
|
|
125
118
|
sampleUnit = Query.parse(queryExpression).profileType().sampleUnit;
|
|
126
119
|
}
|
|
127
|
-
return (_jsx(AnimatePresence, { children: _jsx(motion.div, { className: "h-full w-full relative", initial: { display: 'none', opacity: 0 }, animate: { display: 'block', opacity: 1 }, transition: { duration: 0.5 }, children: _jsx(MetricsGraph, { data: series, from: from, to: to, profile: profile, setTimeRange: setTimeRange, onSampleClick: handleSampleClick, addLabelMatcher: addLabelMatcher, sampleUnit: sampleUnit, height: height, width: width, margin: margin }) }, "metrics-graph-loaded") }));
|
|
128
120
|
}
|
|
129
|
-
return _jsx(
|
|
121
|
+
return (_jsx(AnimatePresence, { children: _jsxs(motion.div, { className: "h-full w-full relative", initial: { display: 'none', opacity: 0 }, animate: { display: 'block', opacity: 1 }, transition: { duration: 0.5 }, children: [_jsx(Toolbar, { sumBy: sumBy, setSumBy: setSumBy, labels: labelNamesResult.response?.labelNames ?? [] }), loading ? (_jsx(MetricsGraphSkeleton, { heightStyle: heightStyle, isDarkMode: isDarkMode })) : dataAvailable ? (_jsx(MetricsGraph, { data: series, from: from, to: to, profile: profile, setTimeRange: setTimeRange, onSampleClick: (timestamp, _value, labels, duration) => {
|
|
122
|
+
onPointClick(timestamp, labels, queryExpression, duration);
|
|
123
|
+
}, addLabelMatcher: addLabelMatcher, sampleUnit: sampleUnit, height: height, width: width, margin: margin })) : (_jsx(ProfileMetricsEmptyState, { message: "No data found. Try a different query." }))] }, "metrics-graph-loaded") }));
|
|
130
124
|
};
|
|
131
125
|
export default ProfileMetricsGraph;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
import { ProfileType } from '@parca/parser';
|
|
2
|
+
export declare const DEFAULT_EMPTY_SUM_BY: string[];
|
|
2
3
|
export declare const useSumBy: (profileType: ProfileType | undefined, labels: string[] | undefined) => [string[], (labels: string[]) => void];
|
|
3
4
|
//# sourceMappingURL=useSumBy.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useSumBy.d.ts","sourceRoot":"","sources":["../../src/ProfileMetricsGraph/useSumBy.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"useSumBy.d.ts","sourceRoot":"","sources":["../../src/ProfileMetricsGraph/useSumBy.ts"],"names":[],"mappings":"AAgBA,OAAO,EAAC,WAAW,EAAC,MAAM,eAAe,CAAC;AAE1C,eAAO,MAAM,oBAAoB,EAAE,MAAM,EAAO,CAAC;AA6BjD,eAAO,MAAM,QAAQ,gBACN,WAAW,GAAG,SAAS,UAC5B,MAAM,EAAE,GAAG,SAAS,KAC3B,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,IAAI,CAkEvC,CAAC"}
|
|
@@ -10,14 +10,15 @@
|
|
|
10
10
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
11
11
|
// See the License for the specific language governing permissions and
|
|
12
12
|
// limitations under the License.
|
|
13
|
-
import { useEffect, useState } from 'react';
|
|
14
|
-
|
|
13
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
14
|
+
import { useParcaContext, useURLState } from '@parca/components';
|
|
15
|
+
export const DEFAULT_EMPTY_SUM_BY = [];
|
|
15
16
|
const getDefaultSumBy = (profile, labels) => {
|
|
16
17
|
if (profile === undefined || labels === undefined) {
|
|
17
18
|
return undefined;
|
|
18
19
|
}
|
|
19
20
|
if (!profile.delta) {
|
|
20
|
-
return
|
|
21
|
+
return [];
|
|
21
22
|
}
|
|
22
23
|
if (labels.includes('comm')) {
|
|
23
24
|
return ['comm'];
|
|
@@ -31,17 +32,49 @@ const getDefaultSumBy = (profile, labels) => {
|
|
|
31
32
|
return undefined;
|
|
32
33
|
};
|
|
33
34
|
export const useSumBy = (profileType, labels) => {
|
|
34
|
-
const
|
|
35
|
+
const { navigateTo } = useParcaContext();
|
|
36
|
+
const [userSelectedSumByParam, setUserSelectedSumByParam] = useURLState({
|
|
37
|
+
param: 'sum_by',
|
|
38
|
+
navigateTo,
|
|
39
|
+
});
|
|
40
|
+
const previousProfileType = useRef(profileType);
|
|
41
|
+
const userSelectedSumBy = useMemo(() => {
|
|
42
|
+
if (userSelectedSumByParam?.length === 0) {
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
if (userSelectedSumByParam === '__none__') {
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
if (typeof userSelectedSumByParam === 'string') {
|
|
49
|
+
return [userSelectedSumByParam];
|
|
50
|
+
}
|
|
51
|
+
return userSelectedSumByParam;
|
|
52
|
+
}, [userSelectedSumByParam]);
|
|
53
|
+
const setUserSelectedSumBy = useCallback((sumBy) => {
|
|
54
|
+
if (sumBy.length === 0) {
|
|
55
|
+
setUserSelectedSumByParam('__none__');
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (sumBy.length === 1) {
|
|
59
|
+
// Handle this separately to take care of the empty string scenario
|
|
60
|
+
setUserSelectedSumByParam(sumBy[0]);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
setUserSelectedSumByParam(sumBy);
|
|
64
|
+
}, [setUserSelectedSumByParam]);
|
|
35
65
|
const [defaultSumBy, setDefaultSumBy] = useState(getDefaultSumBy(profileType, labels));
|
|
36
66
|
useEffect(() => {
|
|
37
67
|
setDefaultSumBy(getDefaultSumBy(profileType, labels));
|
|
38
68
|
}, [profileType, labels]);
|
|
39
69
|
useEffect(() => {
|
|
40
|
-
if (profileType === undefined
|
|
70
|
+
if (profileType === undefined ||
|
|
71
|
+
profileType.toString() === previousProfileType.current?.toString()) {
|
|
41
72
|
return;
|
|
42
73
|
}
|
|
43
74
|
// Reset user selected sumBy if profile type changes
|
|
44
|
-
setUserSelectedSumBy(
|
|
75
|
+
setUserSelectedSumBy(['']);
|
|
76
|
+
previousProfileType.current = profileType;
|
|
77
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
45
78
|
}, [profileType]);
|
|
46
79
|
return [userSelectedSumBy ?? defaultSumBy ?? DEFAULT_EMPTY_SUM_BY, setUserSelectedSumBy];
|
|
47
80
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@parca/profile",
|
|
3
|
-
"version": "0.16.
|
|
3
|
+
"version": "0.16.411",
|
|
4
4
|
"description": "Profile viewing libraries",
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"@headlessui/react": "^1.7.19",
|
|
@@ -73,5 +73,5 @@
|
|
|
73
73
|
"access": "public",
|
|
74
74
|
"registry": "https://registry.npmjs.org/"
|
|
75
75
|
},
|
|
76
|
-
"gitHead": "
|
|
76
|
+
"gitHead": "8885ee532a664069e946e488f60b24cdac9c80f0"
|
|
77
77
|
}
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
// See the License for the specific language governing permissions and
|
|
12
12
|
// limitations under the License.
|
|
13
13
|
|
|
14
|
-
import {useEffect, useMemo
|
|
14
|
+
import {useEffect, useMemo} from 'react';
|
|
15
15
|
|
|
16
16
|
import {RpcError} from '@protobuf-ts/runtime-rpc';
|
|
17
17
|
import {AnimatePresence, motion} from 'framer-motion';
|
|
@@ -28,8 +28,12 @@ import {Query} from '@parca/parser';
|
|
|
28
28
|
import {capitalizeOnlyFirstLetter, getStepDuration} from '@parca/utilities';
|
|
29
29
|
|
|
30
30
|
import {MergedProfileSelection, ProfileSelection} from '..';
|
|
31
|
+
import {useLabelNames} from '../MatchersInput';
|
|
31
32
|
import MetricsGraph from '../MetricsGraph';
|
|
32
33
|
import {useMetricsGraphDimensions} from '../MetricsGraph/useMetricsGraphDimensions';
|
|
34
|
+
import useGrpcQuery from '../useGrpcQuery';
|
|
35
|
+
import {Toolbar} from './Toolbar';
|
|
36
|
+
import {DEFAULT_EMPTY_SUM_BY, useSumBy} from './useSumBy';
|
|
33
37
|
|
|
34
38
|
interface ProfileMetricsEmptyStateProps {
|
|
35
39
|
message: string;
|
|
@@ -89,26 +93,17 @@ const getStepCountFromScreenWidth = (pixelsPerPoint: number): number => {
|
|
|
89
93
|
return Math.round(width / pixelsPerPoint);
|
|
90
94
|
};
|
|
91
95
|
|
|
92
|
-
const EMPTY_SUM_BY: string[] = [];
|
|
93
|
-
|
|
94
96
|
export const useQueryRange = (
|
|
95
97
|
client: QueryServiceClient,
|
|
96
98
|
queryExpression: string,
|
|
97
99
|
start: number,
|
|
98
100
|
end: number,
|
|
99
|
-
sumBy: string[] =
|
|
101
|
+
sumBy: string[] = DEFAULT_EMPTY_SUM_BY,
|
|
100
102
|
skip = false
|
|
101
103
|
): IQueryRangeState => {
|
|
102
|
-
const [isLoading, setLoading] = useState<boolean>(!skip);
|
|
103
|
-
const [state, setState] = useState<IQueryRangeState>({
|
|
104
|
-
response: null,
|
|
105
|
-
isLoading,
|
|
106
|
-
error: null,
|
|
107
|
-
});
|
|
108
104
|
const metadata = useGrpcMetadata();
|
|
109
105
|
const {navigateTo} = useParcaContext();
|
|
110
106
|
const [stepCountStr, setStepCount] = useURLState({param: 'step_count', navigateTo});
|
|
111
|
-
|
|
112
107
|
const defaultStepCount = useMemo(() => {
|
|
113
108
|
return getStepCountFromScreenWidth(10);
|
|
114
109
|
}, []);
|
|
@@ -127,16 +122,11 @@ export const useQueryRange = (
|
|
|
127
122
|
}
|
|
128
123
|
}, [stepCountStr, defaultStepCount, setStepCount]);
|
|
129
124
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
return;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
setLoading(true);
|
|
137
|
-
|
|
125
|
+
const {data, isLoading, error} = useGrpcQuery<QueryRangeResponse | undefined>({
|
|
126
|
+
key: ['query-range', queryExpression, start, end, sumBy.join(','), stepCount, metadata],
|
|
127
|
+
queryFn: async () => {
|
|
138
128
|
const stepDuration = getStepDuration(start, end, stepCount);
|
|
139
|
-
const
|
|
129
|
+
const {response} = await client.queryRange(
|
|
140
130
|
{
|
|
141
131
|
query: queryExpression,
|
|
142
132
|
start: Timestamp.fromDate(new Date(start)),
|
|
@@ -147,21 +137,16 @@ export const useQueryRange = (
|
|
|
147
137
|
},
|
|
148
138
|
{meta: metadata}
|
|
149
139
|
);
|
|
140
|
+
return response;
|
|
141
|
+
},
|
|
142
|
+
options: {
|
|
143
|
+
retry: false,
|
|
144
|
+
enabled: !skip && sumBy !== DEFAULT_EMPTY_SUM_BY,
|
|
145
|
+
staleTime: 1000 * 60 * 5, // 5 minutes
|
|
146
|
+
},
|
|
147
|
+
});
|
|
150
148
|
|
|
151
|
-
|
|
152
|
-
.then(response => {
|
|
153
|
-
setState({response, isLoading: false, error: null});
|
|
154
|
-
setLoading(false);
|
|
155
|
-
return null;
|
|
156
|
-
})
|
|
157
|
-
.catch(error => {
|
|
158
|
-
setState({response: null, isLoading: false, error});
|
|
159
|
-
setLoading(false);
|
|
160
|
-
});
|
|
161
|
-
})();
|
|
162
|
-
}, [client, queryExpression, start, end, metadata, sumBy, skip, stepCount]);
|
|
163
|
-
|
|
164
|
-
return {...state, isLoading};
|
|
149
|
+
return {isLoading, error: error as RpcError | null, response: data ?? null};
|
|
165
150
|
};
|
|
166
151
|
|
|
167
152
|
const ProfileMetricsGraph = ({
|
|
@@ -175,11 +160,23 @@ const ProfileMetricsGraph = ({
|
|
|
175
160
|
onPointClick,
|
|
176
161
|
comparing = false,
|
|
177
162
|
}: ProfileMetricsGraphProps): JSX.Element => {
|
|
163
|
+
const profileType = useMemo(() => {
|
|
164
|
+
return Query.parse(queryExpression).profileType();
|
|
165
|
+
}, [queryExpression]);
|
|
166
|
+
|
|
167
|
+
const {loading: labelNamesLoading, result: labelNamesResult} = useLabelNames(
|
|
168
|
+
queryClient,
|
|
169
|
+
profileType.toString() ?? '',
|
|
170
|
+
from,
|
|
171
|
+
to
|
|
172
|
+
);
|
|
173
|
+
const [sumBy, setSumBy] = useSumBy(profileType, labelNamesResult.response?.labelNames);
|
|
174
|
+
|
|
178
175
|
const {
|
|
179
176
|
isLoading: metricsGraphLoading,
|
|
180
177
|
response,
|
|
181
178
|
error,
|
|
182
|
-
} = useQueryRange(queryClient, queryExpression, from, to);
|
|
179
|
+
} = useQueryRange(queryClient, queryExpression, from, to, sumBy, labelNamesLoading);
|
|
183
180
|
const {onError, perf, authenticationErrorMessage, isDarkMode} = useParcaContext();
|
|
184
181
|
const {width, height, margin, heightStyle} = useMetricsGraphDimensions(comparing);
|
|
185
182
|
|
|
@@ -200,8 +197,12 @@ const ProfileMetricsGraph = ({
|
|
|
200
197
|
const series = response?.series;
|
|
201
198
|
const dataAvailable = series !== null && series !== undefined && series?.length > 0;
|
|
202
199
|
|
|
203
|
-
|
|
204
|
-
|
|
200
|
+
const loading = metricsGraphLoading || labelNamesLoading;
|
|
201
|
+
|
|
202
|
+
if (!labelNamesLoading && labelNamesResult?.error?.message != null) {
|
|
203
|
+
return (
|
|
204
|
+
<ErrorContent errorMessage={capitalizeOnlyFirstLetter(labelNamesResult.error.message)} />
|
|
205
|
+
);
|
|
205
206
|
}
|
|
206
207
|
|
|
207
208
|
if (!metricsGraphLoading && error !== null) {
|
|
@@ -212,52 +213,60 @@ const ProfileMetricsGraph = ({
|
|
|
212
213
|
return <ErrorContent errorMessage={capitalizeOnlyFirstLetter(error.message)} />;
|
|
213
214
|
}
|
|
214
215
|
|
|
216
|
+
let sampleUnit = '';
|
|
217
|
+
|
|
215
218
|
if (dataAvailable) {
|
|
216
|
-
const handleSampleClick = (
|
|
217
|
-
timestamp: number,
|
|
218
|
-
_value: number,
|
|
219
|
-
labels: Label[],
|
|
220
|
-
duration: number
|
|
221
|
-
): void => {
|
|
222
|
-
onPointClick(timestamp, labels, queryExpression, duration);
|
|
223
|
-
};
|
|
224
|
-
|
|
225
|
-
let sampleUnit = '';
|
|
226
219
|
if (series.every((val, i, arr) => val?.sampleType?.unit === arr[0]?.sampleType?.unit)) {
|
|
227
220
|
sampleUnit = series[0]?.sampleType?.unit ?? '';
|
|
228
221
|
}
|
|
229
222
|
if (sampleUnit === '') {
|
|
230
223
|
sampleUnit = Query.parse(queryExpression).profileType().sampleUnit;
|
|
231
224
|
}
|
|
225
|
+
}
|
|
232
226
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
227
|
+
return (
|
|
228
|
+
<AnimatePresence>
|
|
229
|
+
<motion.div
|
|
230
|
+
className="h-full w-full relative"
|
|
231
|
+
key="metrics-graph-loaded"
|
|
232
|
+
initial={{display: 'none', opacity: 0}}
|
|
233
|
+
animate={{display: 'block', opacity: 1}}
|
|
234
|
+
transition={{duration: 0.5}}
|
|
235
|
+
>
|
|
236
|
+
<Toolbar
|
|
237
|
+
sumBy={sumBy}
|
|
238
|
+
setSumBy={setSumBy}
|
|
239
|
+
labels={labelNamesResult.response?.labelNames ?? []}
|
|
240
|
+
/>
|
|
241
|
+
{loading ? (
|
|
242
|
+
<MetricsGraphSkeleton heightStyle={heightStyle} isDarkMode={isDarkMode} />
|
|
243
|
+
) : dataAvailable ? (
|
|
242
244
|
<MetricsGraph
|
|
243
245
|
data={series}
|
|
244
246
|
from={from}
|
|
245
247
|
to={to}
|
|
246
248
|
profile={profile as MergedProfileSelection}
|
|
247
249
|
setTimeRange={setTimeRange}
|
|
248
|
-
onSampleClick={
|
|
250
|
+
onSampleClick={(
|
|
251
|
+
timestamp: number,
|
|
252
|
+
_value: number,
|
|
253
|
+
labels: Label[],
|
|
254
|
+
duration: number
|
|
255
|
+
): void => {
|
|
256
|
+
onPointClick(timestamp, labels, queryExpression, duration);
|
|
257
|
+
}}
|
|
249
258
|
addLabelMatcher={addLabelMatcher}
|
|
250
259
|
sampleUnit={sampleUnit}
|
|
251
260
|
height={height}
|
|
252
261
|
width={width}
|
|
253
262
|
margin={margin}
|
|
254
263
|
/>
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
264
|
+
) : (
|
|
265
|
+
<ProfileMetricsEmptyState message="No data found. Try a different query." />
|
|
266
|
+
)}
|
|
267
|
+
</motion.div>
|
|
268
|
+
</AnimatePresence>
|
|
269
|
+
);
|
|
261
270
|
};
|
|
262
271
|
|
|
263
272
|
export default ProfileMetricsGraph;
|
|
@@ -11,11 +11,12 @@
|
|
|
11
11
|
// See the License for the specific language governing permissions and
|
|
12
12
|
// limitations under the License.
|
|
13
13
|
|
|
14
|
-
import {useEffect, useState} from 'react';
|
|
14
|
+
import {useCallback, useEffect, useMemo, useRef, useState} from 'react';
|
|
15
15
|
|
|
16
|
+
import {useParcaContext, useURLState} from '@parca/components';
|
|
16
17
|
import {ProfileType} from '@parca/parser';
|
|
17
18
|
|
|
18
|
-
const DEFAULT_EMPTY_SUM_BY: string[] = [];
|
|
19
|
+
export const DEFAULT_EMPTY_SUM_BY: string[] = [];
|
|
19
20
|
|
|
20
21
|
const getDefaultSumBy = (
|
|
21
22
|
profile: ProfileType | undefined,
|
|
@@ -26,7 +27,7 @@ const getDefaultSumBy = (
|
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
if (!profile.delta) {
|
|
29
|
-
return
|
|
30
|
+
return [];
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
if (labels.includes('comm')) {
|
|
@@ -48,7 +49,47 @@ export const useSumBy = (
|
|
|
48
49
|
profileType: ProfileType | undefined,
|
|
49
50
|
labels: string[] | undefined
|
|
50
51
|
): [string[], (labels: string[]) => void] => {
|
|
51
|
-
const
|
|
52
|
+
const {navigateTo} = useParcaContext();
|
|
53
|
+
const [userSelectedSumByParam, setUserSelectedSumByParam] = useURLState({
|
|
54
|
+
param: 'sum_by',
|
|
55
|
+
navigateTo,
|
|
56
|
+
});
|
|
57
|
+
const previousProfileType = useRef<ProfileType | undefined>(profileType);
|
|
58
|
+
|
|
59
|
+
const userSelectedSumBy = useMemo<string[] | undefined>(() => {
|
|
60
|
+
if (userSelectedSumByParam?.length === 0) {
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (userSelectedSumByParam === '__none__') {
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (typeof userSelectedSumByParam === 'string') {
|
|
69
|
+
return [userSelectedSumByParam];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return userSelectedSumByParam;
|
|
73
|
+
}, [userSelectedSumByParam]);
|
|
74
|
+
|
|
75
|
+
const setUserSelectedSumBy = useCallback(
|
|
76
|
+
(sumBy: string[]) => {
|
|
77
|
+
if (sumBy.length === 0) {
|
|
78
|
+
setUserSelectedSumByParam('__none__');
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (sumBy.length === 1) {
|
|
83
|
+
// Handle this separately to take care of the empty string scenario
|
|
84
|
+
setUserSelectedSumByParam(sumBy[0]);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
setUserSelectedSumByParam(sumBy);
|
|
89
|
+
},
|
|
90
|
+
[setUserSelectedSumByParam]
|
|
91
|
+
);
|
|
92
|
+
|
|
52
93
|
const [defaultSumBy, setDefaultSumBy] = useState<string[] | undefined>(
|
|
53
94
|
getDefaultSumBy(profileType, labels)
|
|
54
95
|
);
|
|
@@ -58,12 +99,18 @@ export const useSumBy = (
|
|
|
58
99
|
}, [profileType, labels]);
|
|
59
100
|
|
|
60
101
|
useEffect(() => {
|
|
61
|
-
if (
|
|
102
|
+
if (
|
|
103
|
+
profileType === undefined ||
|
|
104
|
+
profileType.toString() === previousProfileType.current?.toString()
|
|
105
|
+
) {
|
|
62
106
|
return;
|
|
63
107
|
}
|
|
64
108
|
|
|
65
109
|
// Reset user selected sumBy if profile type changes
|
|
66
|
-
setUserSelectedSumBy(
|
|
110
|
+
setUserSelectedSumBy(['']);
|
|
111
|
+
previousProfileType.current = profileType;
|
|
112
|
+
|
|
113
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
67
114
|
}, [profileType]);
|
|
68
115
|
|
|
69
116
|
return [userSelectedSumBy ?? defaultSumBy ?? DEFAULT_EMPTY_SUM_BY, setUserSelectedSumBy];
|