@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 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;AAI5D,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;AAcD,eAAO,MAAM,aAAa,WAChB,kBAAkB,mBACT,MAAM,SAChB,MAAM,OACR,MAAM,UACJ,MAAM,EAAE,qBAEd,gBAgEF,CAAC;AAEF,QAAA,MAAM,mBAAmB,iHAUtB,wBAAwB,KAAG,GAAG,CAAC,OAoFjC,CAAC;AAEF,eAAe,mBAAmB,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, useState } from 'react';
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 EMPTY_SUM_BY = [];
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
- useEffect(() => {
62
- void (async () => {
63
- if (skip) {
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 call = client.queryRange({
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
- call.response
77
- .then(response => {
78
- setState({ response, isLoading: false, error: null });
79
- setLoading(false);
80
- return null;
81
- })
82
- .catch(error => {
83
- setState({ response: null, isLoading: false, error });
84
- setLoading(false);
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 { isLoading: metricsGraphLoading, response, error, } = useQueryRange(queryClient, queryExpression, from, to);
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
- if (metricsGraphLoading) {
108
- return _jsx(MetricsGraphSkeleton, { heightStyle: heightStyle, isDarkMode: isDarkMode });
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(ProfileMetricsEmptyState, { message: "No data found. Try a different query." });
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":"AAeA,OAAO,EAAC,WAAW,EAAC,MAAM,eAAe,CAAC;AA+B1C,eAAO,MAAM,QAAQ,gBACN,WAAW,GAAG,SAAS,UAC5B,MAAM,EAAE,GAAG,SAAS,KAC3B,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,IAAI,CAoBvC,CAAC"}
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
- const DEFAULT_EMPTY_SUM_BY = [];
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 undefined;
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 [userSelectedSumBy, setUserSelectedSumBy] = useState(undefined);
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(undefined);
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.409",
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": "897320af0eaf5565a62ff1ad05b463b426a915fa"
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, useState} from 'react';
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[] = EMPTY_SUM_BY,
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
- useEffect(() => {
131
- void (async () => {
132
- if (skip) {
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 call = client.queryRange(
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
- call.response
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
- if (metricsGraphLoading) {
204
- return <MetricsGraphSkeleton heightStyle={heightStyle} isDarkMode={isDarkMode} />;
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
- return (
234
- <AnimatePresence>
235
- <motion.div
236
- className="h-full w-full relative"
237
- key="metrics-graph-loaded"
238
- initial={{display: 'none', opacity: 0}}
239
- animate={{display: 'block', opacity: 1}}
240
- transition={{duration: 0.5}}
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={handleSampleClick}
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
- </motion.div>
256
- </AnimatePresence>
257
- );
258
- }
259
-
260
- return <ProfileMetricsEmptyState message="No data found. Try a different query." />;
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 undefined;
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 [userSelectedSumBy, setUserSelectedSumBy] = useState<string[] | undefined>(undefined);
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 (profileType === undefined) {
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(undefined);
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];