@parca/profile 0.16.408 → 0.16.410

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.410](https://github.com/parca-dev/parca/compare/@parca/profile@0.16.409...@parca/profile@0.16.410) (2024-07-11)
7
+
8
+ **Note:** Version bump only for package @parca/profile
9
+
10
+ ## [0.16.409](https://github.com/parca-dev/parca/compare/@parca/profile@0.16.408...@parca/profile@0.16.409) (2024-07-10)
11
+
12
+ **Note:** Version bump only for package @parca/profile
13
+
6
14
  ## [0.16.408](https://github.com/parca-dev/parca/compare/@parca/profile@0.16.407...@parca/profile@0.16.408) (2024-07-09)
7
15
 
8
16
  **Note:** Version bump only for package @parca/profile
@@ -12,7 +12,6 @@ interface ProfileMetricsGraphProps {
12
12
  profile: ProfileSelection | null;
13
13
  from: number;
14
14
  to: number;
15
- timeRange: DateTimeRange;
16
15
  setTimeRange: (range: DateTimeRange) => void;
17
16
  addLabelMatcher: (labels: {
18
17
  key: string;
@@ -27,10 +26,9 @@ interface ProfileMetricsGraphProps {
27
26
  export interface IQueryRangeState {
28
27
  response: QueryRangeResponse | null;
29
28
  isLoading: boolean;
30
- isRefreshing?: boolean;
31
29
  error: RpcError | null;
32
30
  }
33
- export declare const useQueryRange: (client: QueryServiceClient, queryExpression: string, start: number, end: number, timeRange: DateTimeRange, sumBy?: string[], skip?: boolean) => IQueryRangeState;
34
- declare const ProfileMetricsGraph: ({ queryClient, queryExpression, profile, from, to, setTimeRange, addLabelMatcher, onPointClick, comparing, timeRange, }: ProfileMetricsGraphProps) => JSX.Element;
31
+ export declare const useQueryRange: (client: QueryServiceClient, queryExpression: string, start: number, end: number, sumBy?: string[], skip?: boolean) => IQueryRangeState;
32
+ declare const ProfileMetricsGraph: ({ queryClient, queryExpression, profile, from, to, setTimeRange, addLabelMatcher, onPointClick, comparing, }: ProfileMetricsGraphProps) => JSX.Element;
35
33
  export default ProfileMetricsGraph;
36
34
  //# sourceMappingURL=index.d.ts.map
@@ -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,SAAS,EAAE,aAAa,CAAC;IACzB,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,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,KAAK,EAAE,QAAQ,GAAG,IAAI,CAAC;CACxB;AAcD,eAAO,MAAM,aAAa,WAChB,kBAAkB,mBACT,MAAM,SAChB,MAAM,OACR,MAAM,aACA,aAAa,UACjB,MAAM,EAAE,qBAEd,gBAqFF,CAAC;AAEF,QAAA,MAAM,mBAAmB,4HAWtB,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,OA2GjC,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, useRef, 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,15 +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, timeRange, sumBy = EMPTY_SUM_BY, skip = false) => {
38
- const previousQueryParams = useRef({});
39
- const [isLoading, setLoading] = useState(!skip);
40
- const [state, setState] = useState({
41
- response: null,
42
- isLoading,
43
- error: null,
44
- });
40
+ export const useQueryRange = (client, queryExpression, start, end, sumBy = DEFAULT_EMPTY_SUM_BY, skip = false) => {
45
41
  const metadata = useGrpcMetadata();
46
42
  const { navigateTo } = useParcaContext();
47
43
  const [stepCountStr, setStepCount] = useURLState({ param: 'step_count', navigateTo });
@@ -59,23 +55,11 @@ export const useQueryRange = (client, queryExpression, start, end, timeRange, su
59
55
  setStepCount(defaultStepCount.toString());
60
56
  }
61
57
  }, [stepCountStr, defaultStepCount, setStepCount]);
62
- useEffect(() => {
63
- void (async () => {
64
- if (skip) {
65
- return;
66
- }
67
- if (previousQueryParams.current.queryExpression !== queryExpression ||
68
- previousQueryParams.current.timeRange !== timeRange) {
69
- previousQueryParams.current.queryExpression = queryExpression;
70
- previousQueryParams.current.timeRange = timeRange;
71
- previousQueryParams.current.isRefresh = undefined;
72
- }
73
- else {
74
- previousQueryParams.current.isRefresh = true;
75
- }
76
- setLoading(true);
58
+ const { data, isLoading, error } = useGrpcQuery({
59
+ key: ['query-range', queryExpression, start, end, sumBy.join(','), stepCount, metadata],
60
+ queryFn: async () => {
77
61
  const stepDuration = getStepDuration(start, end, stepCount);
78
- const call = client.queryRange({
62
+ const { response } = await client.queryRange({
79
63
  query: queryExpression,
80
64
  start: Timestamp.fromDate(new Date(start)),
81
65
  end: Timestamp.fromDate(new Date(end)),
@@ -83,27 +67,20 @@ export const useQueryRange = (client, queryExpression, start, end, timeRange, su
83
67
  limit: 0,
84
68
  sumBy,
85
69
  }, { meta: metadata });
86
- call.response
87
- .then(response => {
88
- setState({ response, isLoading: false, error: null });
89
- setLoading(false);
90
- return null;
91
- })
92
- .catch(error => {
93
- setState({ response: null, isLoading: false, error });
94
- setLoading(false);
95
- })
96
- .finally(() => {
97
- if (previousQueryParams.current.isRefresh !== undefined) {
98
- previousQueryParams.current.isRefresh = undefined;
99
- }
100
- });
101
- })();
102
- }, [client, queryExpression, start, end, metadata, timeRange, sumBy, skip, stepCount]);
103
- return { ...state, isLoading, isRefreshing: previousQueryParams.current.isRefresh };
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 };
104
79
  };
105
- const ProfileMetricsGraph = ({ queryClient, queryExpression, profile, from, to, setTimeRange, addLabelMatcher, onPointClick, comparing = false, timeRange, }) => {
106
- const { isLoading: metricsGraphLoading, response, error, } = useQueryRange(queryClient, queryExpression, from, to, timeRange);
80
+ const ProfileMetricsGraph = ({ queryClient, queryExpression, profile, from, to, setTimeRange, addLabelMatcher, onPointClick, comparing = false, }) => {
81
+ const { loading: labelNamesLoading, result: labelNamesResult } = useLabelNames(queryClient, profile?.ProfileSource()?.ProfileType()?.toString() ?? '', from, to);
82
+ const [sumBy, setSumBy] = useSumBy(profile?.ProfileSource()?.ProfileType(), labelNamesResult.response?.labelNames);
83
+ const { isLoading: metricsGraphLoading, response, error, } = useQueryRange(queryClient, queryExpression, from, to, sumBy, labelNamesLoading);
107
84
  const { onError, perf, authenticationErrorMessage, isDarkMode } = useParcaContext();
108
85
  const { width, height, margin, heightStyle } = useMetricsGraphDimensions(comparing);
109
86
  useEffect(() => {
@@ -119,8 +96,9 @@ const ProfileMetricsGraph = ({ queryClient, queryExpression, profile, from, to,
119
96
  }, [perf, response]);
120
97
  const series = response?.series;
121
98
  const dataAvailable = series !== null && series !== undefined && series?.length > 0;
122
- if (metricsGraphLoading) {
123
- return _jsx(MetricsGraphSkeleton, { heightStyle: heightStyle, isDarkMode: isDarkMode });
99
+ const loading = metricsGraphLoading || labelNamesLoading;
100
+ if (!labelNamesLoading && labelNamesResult?.error?.message != null) {
101
+ return (_jsx(ErrorContent, { errorMessage: capitalizeOnlyFirstLetter(labelNamesResult.error.message) }));
124
102
  }
125
103
  if (!metricsGraphLoading && error !== null) {
126
104
  if (authenticationErrorMessage !== undefined && error.code === 'UNAUTHENTICATED') {
@@ -128,19 +106,17 @@ const ProfileMetricsGraph = ({ queryClient, queryExpression, profile, from, to,
128
106
  }
129
107
  return _jsx(ErrorContent, { errorMessage: capitalizeOnlyFirstLetter(error.message) });
130
108
  }
109
+ let sampleUnit = '';
131
110
  if (dataAvailable) {
132
- const handleSampleClick = (timestamp, _value, labels, duration) => {
133
- onPointClick(timestamp, labels, queryExpression, duration);
134
- };
135
- let sampleUnit = '';
136
111
  if (series.every((val, i, arr) => val?.sampleType?.unit === arr[0]?.sampleType?.unit)) {
137
112
  sampleUnit = series[0]?.sampleType?.unit ?? '';
138
113
  }
139
114
  if (sampleUnit === '') {
140
115
  sampleUnit = Query.parse(queryExpression).profileType().sampleUnit;
141
116
  }
142
- 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") }));
143
117
  }
144
- return _jsx(ProfileMetricsEmptyState, { message: "No data found. Try a different query." });
118
+ 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) => {
119
+ onPointClick(timestamp, labels, queryExpression, duration);
120
+ }, addLabelMatcher: addLabelMatcher, sampleUnit: sampleUnit, height: height, width: width, margin: margin })) : (_jsx(ProfileMetricsEmptyState, { message: "No data found. Try a different query." }))] }, "metrics-graph-loaded") }));
145
121
  };
146
122
  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,CA+EvC,CAAC"}
@@ -10,8 +10,9 @@
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, 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;
@@ -31,17 +32,59 @@ 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
+ console.log('labels', labels);
41
+ console.log('userSelectedSumByParam', userSelectedSumByParam);
42
+ const userSelectedSumBy = useMemo(() => {
43
+ if (userSelectedSumByParam?.length === 0) {
44
+ return undefined;
45
+ }
46
+ if (userSelectedSumByParam === '__none__') {
47
+ return [];
48
+ }
49
+ if (typeof userSelectedSumByParam === 'string') {
50
+ return [userSelectedSumByParam];
51
+ }
52
+ return userSelectedSumByParam;
53
+ }, [userSelectedSumByParam]);
54
+ console.log('userSelectedSumBy', userSelectedSumBy);
55
+ const setUserSelectedSumBy = useCallback((sumBy) => {
56
+ if (sumBy.length === 0) {
57
+ setUserSelectedSumByParam('__none__');
58
+ return;
59
+ }
60
+ if (sumBy.length === 1) {
61
+ // Handle this separately to take care of the empty string scenario
62
+ setUserSelectedSumByParam(sumBy[0]);
63
+ return;
64
+ }
65
+ setUserSelectedSumByParam(sumBy);
66
+ }, [setUserSelectedSumByParam]);
35
67
  const [defaultSumBy, setDefaultSumBy] = useState(getDefaultSumBy(profileType, labels));
36
68
  useEffect(() => {
37
69
  setDefaultSumBy(getDefaultSumBy(profileType, labels));
38
70
  }, [profileType, labels]);
39
71
  useEffect(() => {
40
- if (profileType === undefined) {
72
+ if (profileType === undefined || labels === undefined) {
73
+ return;
74
+ }
75
+ if (userSelectedSumBy !== undefined && userSelectedSumBy.length === 0) {
76
+ // User has explicitly selected no sumBy, so don't reset it
41
77
  return;
42
78
  }
79
+ if (userSelectedSumBy !== undefined && userSelectedSumBy.length > 0) {
80
+ // If any of the user selected sumBy is present in the labels, then don't reset it
81
+ if (userSelectedSumBy.some(sumBy => labels?.includes(sumBy))) {
82
+ return;
83
+ }
84
+ }
43
85
  // Reset user selected sumBy if profile type changes
44
- setUserSelectedSumBy(undefined);
45
- }, [profileType]);
86
+ setUserSelectedSumBy(['']);
87
+ // eslint-disable-next-line react-hooks/exhaustive-deps
88
+ }, [profileType, labels]);
46
89
  return [userSelectedSumBy ?? defaultSumBy ?? DEFAULT_EMPTY_SUM_BY, setUserSelectedSumBy];
47
90
  };
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/ProfileSelector/index.tsx"],"names":[],"mappings":"AAeA,OAAO,EAAC,QAAQ,EAAC,MAAM,0BAA0B,CAAC;AAElD,OAAO,EAAQ,oBAAoB,EAAE,kBAAkB,EAAC,MAAM,eAAe,CAAC;AAY9E,OAAO,EAAC,KAAK,gBAAgB,EAAC,MAAM,kBAAkB,CAAC;AAEvD,OAAO,EAAyB,gBAAgB,EAAC,MAAM,IAAI,CAAC;AAO5D,MAAM,WAAW,cAAc;IAC7B,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,UAAU,oBAAoB;IAC5B,WAAW,EAAE,kBAAkB,CAAC;IAChC,cAAc,EAAE,cAAc,CAAC;IAC/B,aAAa,EAAE,CAAC,MAAM,EAAE,gBAAgB,KAAK,IAAI,CAAC;IAClD,WAAW,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,CAAC;IAC7C,YAAY,EAAE,MAAM,IAAI,CAAC;IACzB,mBAAmB,EAAE,MAAM,CAAC;IAC5B,gBAAgB,EAAE,gBAAgB,GAAG,IAAI,CAAC;IAC1C,SAAS,EAAE,OAAO,CAAC;IACnB,UAAU,EAAE,gBAAgB,CAAC;CAC9B;AAED,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,CAAC,EAAE,oBAAoB,CAAC;IAC5B,KAAK,CAAC,EAAE,QAAQ,CAAC;CAClB;AAED,eAAO,MAAM,eAAe,WAAY,kBAAkB,KAAG,mBAkB5D,CAAC;AAEF,QAAA,MAAM,eAAe,6IAUlB,oBAAoB,KAAG,GAAG,CAAC,OA4P7B,CAAC;AAEF,eAAe,eAAe,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/ProfileSelector/index.tsx"],"names":[],"mappings":"AAeA,OAAO,EAAC,QAAQ,EAAC,MAAM,0BAA0B,CAAC;AAElD,OAAO,EAAQ,oBAAoB,EAAE,kBAAkB,EAAC,MAAM,eAAe,CAAC;AAY9E,OAAO,EAAC,KAAK,gBAAgB,EAAC,MAAM,kBAAkB,CAAC;AAEvD,OAAO,EAAyB,gBAAgB,EAAC,MAAM,IAAI,CAAC;AAO5D,MAAM,WAAW,cAAc;IAC7B,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,UAAU,oBAAoB;IAC5B,WAAW,EAAE,kBAAkB,CAAC;IAChC,cAAc,EAAE,cAAc,CAAC;IAC/B,aAAa,EAAE,CAAC,MAAM,EAAE,gBAAgB,KAAK,IAAI,CAAC;IAClD,WAAW,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,CAAC;IAC7C,YAAY,EAAE,MAAM,IAAI,CAAC;IACzB,mBAAmB,EAAE,MAAM,CAAC;IAC5B,gBAAgB,EAAE,gBAAgB,GAAG,IAAI,CAAC;IAC1C,SAAS,EAAE,OAAO,CAAC;IACnB,UAAU,EAAE,gBAAgB,CAAC;CAC9B;AAED,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,CAAC,EAAE,oBAAoB,CAAC;IAC5B,KAAK,CAAC,EAAE,QAAQ,CAAC;CAClB;AAED,eAAO,MAAM,eAAe,WAAY,kBAAkB,KAAG,mBAkB5D,CAAC;AAEF,QAAA,MAAM,eAAe,6IAUlB,oBAAoB,KAAG,GAAG,CAAC,OA2P7B,CAAC;AAEF,eAAe,eAAe,CAAC"}
@@ -149,7 +149,7 @@ const ProfileSelector = ({ queryClient, querySelection, selectProfile, selectQue
149
149
  }, id: "h-matcher-search-button", children: "Search" }) })] }), _jsx("div", { children: comparing && _jsx(IconButton, { onClick: () => closeProfile(), icon: _jsx(CloseIcon, {}) }) })] }), _jsx("div", { className: "rounded bg-white shadow dark:border-gray-500 dark:bg-gray-700", children: _jsx("div", { style: { height: heightStyle }, children: querySelection.expression !== undefined &&
150
150
  querySelection.expression.length > 0 &&
151
151
  querySelection.from !== undefined &&
152
- querySelection.to !== undefined ? (_jsx("div", { className: "p-2", children: _jsx(ProfileMetricsGraph, { queryClient: queryClient, queryExpression: querySelection.expression, from: querySelection.from, to: querySelection.to, profile: profileSelection, comparing: comparing, timeRange: timeRangeSelection, setTimeRange: (range) => {
152
+ querySelection.to !== undefined ? (_jsx("div", { className: "p-2", children: _jsx(ProfileMetricsGraph, { queryClient: queryClient, queryExpression: querySelection.expression, from: querySelection.from, to: querySelection.to, profile: profileSelection, comparing: comparing, setTimeRange: (range) => {
153
153
  const from = range.getFromMs();
154
154
  const to = range.getToMs();
155
155
  let mergedProfileParams = {};
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@parca/profile",
3
- "version": "0.16.408",
3
+ "version": "0.16.410",
4
4
  "description": "Profile viewing libraries",
5
5
  "dependencies": {
6
6
  "@headlessui/react": "^1.7.19",
7
7
  "@iconify/react": "^4.0.0",
8
8
  "@parca/client": "0.16.122",
9
- "@parca/components": "0.16.292",
9
+ "@parca/components": "0.16.293",
10
10
  "@parca/dynamicsize": "0.16.65",
11
11
  "@parca/hooks": "0.0.67",
12
12
  "@parca/icons": "0.16.70",
@@ -73,5 +73,5 @@
73
73
  "access": "public",
74
74
  "registry": "https://registry.npmjs.org/"
75
75
  },
76
- "gitHead": "b7b8d0a8483b3e979dadfc870b389da21d6e8f92"
76
+ "gitHead": "7fc4b0d3e5a29c4a79f8762185ec8b874dfdef2c"
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, useRef, 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;
@@ -60,7 +64,6 @@ interface ProfileMetricsGraphProps {
60
64
  profile: ProfileSelection | null;
61
65
  from: number;
62
66
  to: number;
63
- timeRange: DateTimeRange;
64
67
  setTimeRange: (range: DateTimeRange) => void;
65
68
  addLabelMatcher: (
66
69
  labels: {key: string; value: string} | Array<{key: string; value: string}>
@@ -77,7 +80,6 @@ interface ProfileMetricsGraphProps {
77
80
  export interface IQueryRangeState {
78
81
  response: QueryRangeResponse | null;
79
82
  isLoading: boolean;
80
- isRefreshing?: boolean;
81
83
  error: RpcError | null;
82
84
  }
83
85
 
@@ -91,32 +93,17 @@ const getStepCountFromScreenWidth = (pixelsPerPoint: number): number => {
91
93
  return Math.round(width / pixelsPerPoint);
92
94
  };
93
95
 
94
- const EMPTY_SUM_BY: string[] = [];
95
-
96
96
  export const useQueryRange = (
97
97
  client: QueryServiceClient,
98
98
  queryExpression: string,
99
99
  start: number,
100
100
  end: number,
101
- timeRange: DateTimeRange,
102
- sumBy: string[] = EMPTY_SUM_BY,
101
+ sumBy: string[] = DEFAULT_EMPTY_SUM_BY,
103
102
  skip = false
104
103
  ): IQueryRangeState => {
105
- const previousQueryParams = useRef<{
106
- queryExpression?: string;
107
- timeRange?: DateTimeRange;
108
- isRefresh?: boolean;
109
- }>({});
110
- const [isLoading, setLoading] = useState<boolean>(!skip);
111
- const [state, setState] = useState<IQueryRangeState>({
112
- response: null,
113
- isLoading,
114
- error: null,
115
- });
116
104
  const metadata = useGrpcMetadata();
117
105
  const {navigateTo} = useParcaContext();
118
106
  const [stepCountStr, setStepCount] = useURLState({param: 'step_count', navigateTo});
119
-
120
107
  const defaultStepCount = useMemo(() => {
121
108
  return getStepCountFromScreenWidth(10);
122
109
  }, []);
@@ -135,27 +122,11 @@ export const useQueryRange = (
135
122
  }
136
123
  }, [stepCountStr, defaultStepCount, setStepCount]);
137
124
 
138
- useEffect(() => {
139
- void (async () => {
140
- if (skip) {
141
- return;
142
- }
143
-
144
- if (
145
- previousQueryParams.current.queryExpression !== queryExpression ||
146
- previousQueryParams.current.timeRange !== timeRange
147
- ) {
148
- previousQueryParams.current.queryExpression = queryExpression;
149
- previousQueryParams.current.timeRange = timeRange;
150
- previousQueryParams.current.isRefresh = undefined;
151
- } else {
152
- previousQueryParams.current.isRefresh = true;
153
- }
154
-
155
- setLoading(true);
156
-
125
+ const {data, isLoading, error} = useGrpcQuery<QueryRangeResponse | undefined>({
126
+ key: ['query-range', queryExpression, start, end, sumBy.join(','), stepCount, metadata],
127
+ queryFn: async () => {
157
128
  const stepDuration = getStepDuration(start, end, stepCount);
158
- const call = client.queryRange(
129
+ const {response} = await client.queryRange(
159
130
  {
160
131
  query: queryExpression,
161
132
  start: Timestamp.fromDate(new Date(start)),
@@ -166,26 +137,16 @@ export const useQueryRange = (
166
137
  },
167
138
  {meta: metadata}
168
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
+ });
169
148
 
170
- call.response
171
- .then(response => {
172
- setState({response, isLoading: false, error: null});
173
- setLoading(false);
174
- return null;
175
- })
176
- .catch(error => {
177
- setState({response: null, isLoading: false, error});
178
- setLoading(false);
179
- })
180
- .finally(() => {
181
- if (previousQueryParams.current.isRefresh !== undefined) {
182
- previousQueryParams.current.isRefresh = undefined;
183
- }
184
- });
185
- })();
186
- }, [client, queryExpression, start, end, metadata, timeRange, sumBy, skip, stepCount]);
187
-
188
- return {...state, isLoading, isRefreshing: previousQueryParams.current.isRefresh};
149
+ return {isLoading, error: error as RpcError | null, response: data ?? null};
189
150
  };
190
151
 
191
152
  const ProfileMetricsGraph = ({
@@ -198,13 +159,23 @@ const ProfileMetricsGraph = ({
198
159
  addLabelMatcher,
199
160
  onPointClick,
200
161
  comparing = false,
201
- timeRange,
202
162
  }: ProfileMetricsGraphProps): JSX.Element => {
163
+ const {loading: labelNamesLoading, result: labelNamesResult} = useLabelNames(
164
+ queryClient,
165
+ profile?.ProfileSource()?.ProfileType()?.toString() ?? '',
166
+ from,
167
+ to
168
+ );
169
+ const [sumBy, setSumBy] = useSumBy(
170
+ profile?.ProfileSource()?.ProfileType(),
171
+ labelNamesResult.response?.labelNames
172
+ );
173
+
203
174
  const {
204
175
  isLoading: metricsGraphLoading,
205
176
  response,
206
177
  error,
207
- } = useQueryRange(queryClient, queryExpression, from, to, timeRange);
178
+ } = useQueryRange(queryClient, queryExpression, from, to, sumBy, labelNamesLoading);
208
179
  const {onError, perf, authenticationErrorMessage, isDarkMode} = useParcaContext();
209
180
  const {width, height, margin, heightStyle} = useMetricsGraphDimensions(comparing);
210
181
 
@@ -225,8 +196,12 @@ const ProfileMetricsGraph = ({
225
196
  const series = response?.series;
226
197
  const dataAvailable = series !== null && series !== undefined && series?.length > 0;
227
198
 
228
- if (metricsGraphLoading) {
229
- return <MetricsGraphSkeleton heightStyle={heightStyle} isDarkMode={isDarkMode} />;
199
+ const loading = metricsGraphLoading || labelNamesLoading;
200
+
201
+ if (!labelNamesLoading && labelNamesResult?.error?.message != null) {
202
+ return (
203
+ <ErrorContent errorMessage={capitalizeOnlyFirstLetter(labelNamesResult.error.message)} />
204
+ );
230
205
  }
231
206
 
232
207
  if (!metricsGraphLoading && error !== null) {
@@ -237,52 +212,60 @@ const ProfileMetricsGraph = ({
237
212
  return <ErrorContent errorMessage={capitalizeOnlyFirstLetter(error.message)} />;
238
213
  }
239
214
 
240
- if (dataAvailable) {
241
- const handleSampleClick = (
242
- timestamp: number,
243
- _value: number,
244
- labels: Label[],
245
- duration: number
246
- ): void => {
247
- onPointClick(timestamp, labels, queryExpression, duration);
248
- };
215
+ let sampleUnit = '';
249
216
 
250
- let sampleUnit = '';
217
+ if (dataAvailable) {
251
218
  if (series.every((val, i, arr) => val?.sampleType?.unit === arr[0]?.sampleType?.unit)) {
252
219
  sampleUnit = series[0]?.sampleType?.unit ?? '';
253
220
  }
254
221
  if (sampleUnit === '') {
255
222
  sampleUnit = Query.parse(queryExpression).profileType().sampleUnit;
256
223
  }
224
+ }
257
225
 
258
- return (
259
- <AnimatePresence>
260
- <motion.div
261
- className="h-full w-full relative"
262
- key="metrics-graph-loaded"
263
- initial={{display: 'none', opacity: 0}}
264
- animate={{display: 'block', opacity: 1}}
265
- transition={{duration: 0.5}}
266
- >
226
+ return (
227
+ <AnimatePresence>
228
+ <motion.div
229
+ className="h-full w-full relative"
230
+ key="metrics-graph-loaded"
231
+ initial={{display: 'none', opacity: 0}}
232
+ animate={{display: 'block', opacity: 1}}
233
+ transition={{duration: 0.5}}
234
+ >
235
+ <Toolbar
236
+ sumBy={sumBy}
237
+ setSumBy={setSumBy}
238
+ labels={labelNamesResult.response?.labelNames ?? []}
239
+ />
240
+ {loading ? (
241
+ <MetricsGraphSkeleton heightStyle={heightStyle} isDarkMode={isDarkMode} />
242
+ ) : dataAvailable ? (
267
243
  <MetricsGraph
268
244
  data={series}
269
245
  from={from}
270
246
  to={to}
271
247
  profile={profile as MergedProfileSelection}
272
248
  setTimeRange={setTimeRange}
273
- onSampleClick={handleSampleClick}
249
+ onSampleClick={(
250
+ timestamp: number,
251
+ _value: number,
252
+ labels: Label[],
253
+ duration: number
254
+ ): void => {
255
+ onPointClick(timestamp, labels, queryExpression, duration);
256
+ }}
274
257
  addLabelMatcher={addLabelMatcher}
275
258
  sampleUnit={sampleUnit}
276
259
  height={height}
277
260
  width={width}
278
261
  margin={margin}
279
262
  />
280
- </motion.div>
281
- </AnimatePresence>
282
- );
283
- }
284
-
285
- return <ProfileMetricsEmptyState message="No data found. Try a different query." />;
263
+ ) : (
264
+ <ProfileMetricsEmptyState message="No data found. Try a different query." />
265
+ )}
266
+ </motion.div>
267
+ </AnimatePresence>
268
+ );
286
269
  };
287
270
 
288
271
  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, 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,
@@ -48,7 +49,52 @@ 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
+
58
+ console.log('labels', labels);
59
+
60
+ console.log('userSelectedSumByParam', userSelectedSumByParam);
61
+
62
+ const userSelectedSumBy = useMemo<string[] | undefined>(() => {
63
+ if (userSelectedSumByParam?.length === 0) {
64
+ return undefined;
65
+ }
66
+
67
+ if (userSelectedSumByParam === '__none__') {
68
+ return [];
69
+ }
70
+
71
+ if (typeof userSelectedSumByParam === 'string') {
72
+ return [userSelectedSumByParam];
73
+ }
74
+
75
+ return userSelectedSumByParam;
76
+ }, [userSelectedSumByParam]);
77
+
78
+ console.log('userSelectedSumBy', userSelectedSumBy);
79
+
80
+ const setUserSelectedSumBy = useCallback(
81
+ (sumBy: string[]) => {
82
+ if (sumBy.length === 0) {
83
+ setUserSelectedSumByParam('__none__');
84
+ return;
85
+ }
86
+
87
+ if (sumBy.length === 1) {
88
+ // Handle this separately to take care of the empty string scenario
89
+ setUserSelectedSumByParam(sumBy[0]);
90
+ return;
91
+ }
92
+
93
+ setUserSelectedSumByParam(sumBy);
94
+ },
95
+ [setUserSelectedSumByParam]
96
+ );
97
+
52
98
  const [defaultSumBy, setDefaultSumBy] = useState<string[] | undefined>(
53
99
  getDefaultSumBy(profileType, labels)
54
100
  );
@@ -58,13 +104,27 @@ export const useSumBy = (
58
104
  }, [profileType, labels]);
59
105
 
60
106
  useEffect(() => {
61
- if (profileType === undefined) {
107
+ if (profileType === undefined || labels === undefined) {
62
108
  return;
63
109
  }
64
110
 
111
+ if (userSelectedSumBy !== undefined && userSelectedSumBy.length === 0) {
112
+ // User has explicitly selected no sumBy, so don't reset it
113
+ return;
114
+ }
115
+
116
+ if (userSelectedSumBy !== undefined && userSelectedSumBy.length > 0) {
117
+ // If any of the user selected sumBy is present in the labels, then don't reset it
118
+ if (userSelectedSumBy.some(sumBy => labels?.includes(sumBy))) {
119
+ return;
120
+ }
121
+ }
122
+
65
123
  // Reset user selected sumBy if profile type changes
66
- setUserSelectedSumBy(undefined);
67
- }, [profileType]);
124
+ setUserSelectedSumBy(['']);
125
+
126
+ // eslint-disable-next-line react-hooks/exhaustive-deps
127
+ }, [profileType, labels]);
68
128
 
69
129
  return [userSelectedSumBy ?? defaultSumBy ?? DEFAULT_EMPTY_SUM_BY, setUserSelectedSumBy];
70
130
  };
@@ -288,7 +288,6 @@ const ProfileSelector = ({
288
288
  to={querySelection.to}
289
289
  profile={profileSelection}
290
290
  comparing={comparing}
291
- timeRange={timeRangeSelection}
292
291
  setTimeRange={(range: DateTimeRange) => {
293
292
  const from = range.getFromMs();
294
293
  const to = range.getToMs();