@parca/profile 0.19.109 → 0.19.111

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.
Files changed (42) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/ProfileSelector/index.d.ts.map +1 -1
  3. package/dist/ProfileSelector/index.js +9 -1
  4. package/dist/ProfileSelector/useAutoQuerySelector.js +1 -1
  5. package/dist/ProfileView/components/ProfileFilters/useProfileFilters.d.ts +2 -0
  6. package/dist/ProfileView/components/ProfileFilters/useProfileFilters.d.ts.map +1 -1
  7. package/dist/ProfileView/components/ProfileFilters/useProfileFilters.js +3 -1
  8. package/dist/ProfileView/components/ProfileFilters/useProfileFiltersUrlState.d.ts +1 -0
  9. package/dist/ProfileView/components/ProfileFilters/useProfileFiltersUrlState.d.ts.map +1 -1
  10. package/dist/ProfileView/components/ProfileFilters/useProfileFiltersUrlState.js +17 -3
  11. package/dist/ProfileView/components/ProfileFilters/useProfileFiltersUrlState.test.d.ts +2 -0
  12. package/dist/ProfileView/components/ProfileFilters/useProfileFiltersUrlState.test.d.ts.map +1 -0
  13. package/dist/ProfileView/components/ProfileFilters/useProfileFiltersUrlState.test.js +541 -0
  14. package/dist/QueryControls/index.d.ts.map +1 -1
  15. package/dist/QueryControls/index.js +1 -3
  16. package/dist/SourceView/Highlighter.d.ts +5 -2
  17. package/dist/SourceView/Highlighter.d.ts.map +1 -1
  18. package/dist/SourceView/Highlighter.js +3 -2
  19. package/dist/SourceView/index.d.ts.map +1 -1
  20. package/dist/SourceView/index.js +40 -11
  21. package/dist/hooks/useLabels.d.ts.map +1 -1
  22. package/dist/hooks/useLabels.js +0 -7
  23. package/dist/hooks/useQueryState.d.ts +4 -0
  24. package/dist/hooks/useQueryState.d.ts.map +1 -1
  25. package/dist/hooks/useQueryState.js +29 -5
  26. package/dist/hooks/useQueryState.test.js +72 -8
  27. package/dist/index.d.ts +2 -1
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +2 -1
  30. package/package.json +3 -3
  31. package/src/ProfileSelector/index.tsx +14 -1
  32. package/src/ProfileSelector/useAutoQuerySelector.ts +1 -1
  33. package/src/ProfileView/components/ProfileFilters/useProfileFilters.ts +5 -1
  34. package/src/ProfileView/components/ProfileFilters/useProfileFiltersUrlState.test.tsx +663 -0
  35. package/src/ProfileView/components/ProfileFilters/useProfileFiltersUrlState.ts +24 -3
  36. package/src/QueryControls/index.tsx +1 -3
  37. package/src/SourceView/Highlighter.tsx +6 -5
  38. package/src/SourceView/index.tsx +45 -12
  39. package/src/hooks/useLabels.ts +0 -10
  40. package/src/hooks/useQueryState.test.tsx +90 -11
  41. package/src/hooks/useQueryState.ts +36 -4
  42. package/src/index.tsx +4 -0
@@ -35,28 +35,57 @@ export const SourceView = React.memo(function SourceView({ data, loading, total,
35
35
  id: MENU_ID,
36
36
  });
37
37
  const { startLine, endLine } = useLineRange();
38
- const [cumulative, flat] = useMemo(() => {
38
+ const sourceTable = useMemo(() => {
39
39
  if (data === undefined) {
40
- return [null, null];
40
+ return null;
41
41
  }
42
42
  const table = tableFromIPC(data.record);
43
- const cumulative = table.getChild('cumulative');
44
- const flat = table.getChild('flat');
45
- return [cumulative, flat];
43
+ return {
44
+ numRows: table.numRows,
45
+ lineNumbers: table.getChild('line_number'),
46
+ cumulative: table.getChild('cumulative'),
47
+ flat: table.getChild('flat'),
48
+ };
46
49
  }, [data]);
50
+ const getLineData = useCallback((lineNumber) => {
51
+ if (sourceTable === null || sourceTable.lineNumbers === null) {
52
+ return undefined;
53
+ }
54
+ const { numRows, lineNumbers, cumulative, flat } = sourceTable;
55
+ let lo = 0;
56
+ let hi = numRows - 1;
57
+ while (lo <= hi) {
58
+ const mid = (lo + hi) >>> 1;
59
+ const midVal = Number(lineNumbers.get(mid));
60
+ if (midVal === lineNumber) {
61
+ return {
62
+ cumulative: cumulative?.get(mid) ?? 0n,
63
+ flat: flat?.get(mid) ?? 0n,
64
+ };
65
+ }
66
+ if (midVal < lineNumber) {
67
+ lo = mid + 1;
68
+ }
69
+ else {
70
+ hi = mid - 1;
71
+ }
72
+ }
73
+ return undefined;
74
+ }, [sourceTable]);
47
75
  const getProfileDataForLine = useCallback((line, newLine) => {
48
- if (cumulative == null && flat == null) {
76
+ const data = getLineData(line);
77
+ if (data === undefined) {
49
78
  return undefined;
50
79
  }
51
- if (cumulative?.get(line - 1) === 0n && flat?.get(line - 1) === 0n) {
80
+ if (data.cumulative === 0n && data.flat === 0n) {
52
81
  return undefined;
53
82
  }
54
83
  return {
55
84
  line: newLine,
56
- cumulative: Number(cumulative?.get(line - 1) ?? 0),
57
- flat: Number(flat?.get(line - 1) ?? 0),
85
+ cumulative: Number(data.cumulative),
86
+ flat: Number(data.flat),
58
87
  };
59
- }, [cumulative, flat]);
88
+ }, [getLineData]);
60
89
  const [selectedCode, profileData] = useMemo(() => {
61
90
  if (startLine === -1 && endLine === -1) {
62
91
  return ['', []];
@@ -97,6 +126,6 @@ export const SourceView = React.memo(function SourceView({ data, loading, total,
97
126
  event,
98
127
  });
99
128
  };
100
- return (_jsx(AnimatePresence, { children: _jsxs(motion.div, { className: "h-full w-full", initial: { display: 'none', opacity: 0 }, animate: { display: 'block', opacity: 1 }, transition: { duration: 0.5 }, children: [_jsx(Highlighter, { file: sourceFileName, content: data.source, renderer: profileAwareRenderer(cumulative, flat, total, filtered, onContextMenu) }), sourceViewContextMenuItems.length > 0 ? (_jsx(Menu, { id: MENU_ID, children: sourceViewContextMenuItems.map(item => (_jsx(Item, { onClick: () => item.action(selectedCode, profileData), children: item.label }, item.id))) })) : null] }, "source-view-loaded") }));
129
+ return (_jsx(AnimatePresence, { children: _jsxs(motion.div, { className: "h-full w-full", initial: { display: 'none', opacity: 0 }, animate: { display: 'block', opacity: 1 }, transition: { duration: 0.5 }, children: [_jsx(Highlighter, { file: sourceFileName, content: data.source, renderer: profileAwareRenderer(getLineData, total, filtered, onContextMenu) }), sourceViewContextMenuItems.length > 0 ? (_jsx(Menu, { id: MENU_ID, children: sourceViewContextMenuItems.map(item => (_jsx(Item, { onClick: () => item.action(selectedCode, profileData), children: item.label }, item.id))) })) : null] }, "source-view-loaded") }));
101
130
  });
102
131
  export default SourceView;
@@ -1 +1 @@
1
- {"version":3,"file":"useLabels.d.ts","sourceRoot":"","sources":["../../src/hooks/useLabels.ts"],"names":[],"mappings":"AAeA,OAAO,EAAgB,cAAc,EAAE,kBAAkB,EAAgB,MAAM,eAAe,CAAC;AAM/F,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,KAAK,CAAC,EAAE,KAAK,CAAC;CACf;AAED,UAAU,cAAc;IACtB,MAAM,EAAE,kBAAkB,CAAC;IAC3B,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9B;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,EAAE,cAAc,CAAC;IAC1B,KAAK,CAAC,EAAE,KAAK,CAAC;CACf;AAED,UAAU,aAAa;IACrB,MAAM,EAAE,iBAAiB,CAAC;IAC1B,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9B;AAED,eAAO,MAAM,aAAa,GACxB,QAAQ,kBAAkB,EAC1B,aAAa,MAAM,EACnB,QAAQ,MAAM,EACd,MAAM,MAAM,EACZ,QAAQ,MAAM,EAAE,KACf,aAkCF,CAAC;AAEF,eAAO,MAAM,cAAc,GACzB,QAAQ,kBAAkB,EAC1B,WAAW,MAAM,EACjB,aAAa,MAAM,EACnB,QAAQ,MAAM,EACd,MAAM,MAAM,KACX,cAmCF,CAAC"}
1
+ {"version":3,"file":"useLabels.d.ts","sourceRoot":"","sources":["../../src/hooks/useLabels.ts"],"names":[],"mappings":"AAaA,OAAO,EAAgB,cAAc,EAAE,kBAAkB,EAAgB,MAAM,eAAe,CAAC;AAM/F,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,KAAK,CAAC,EAAE,KAAK,CAAC;CACf;AAED,UAAU,cAAc;IACtB,MAAM,EAAE,kBAAkB,CAAC;IAC3B,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9B;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,EAAE,cAAc,CAAC;IAC1B,KAAK,CAAC,EAAE,KAAK,CAAC;CACf;AAED,UAAU,aAAa;IACrB,MAAM,EAAE,iBAAiB,CAAC;IAC1B,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9B;AAED,eAAO,MAAM,aAAa,GACxB,QAAQ,kBAAkB,EAC1B,aAAa,MAAM,EACnB,QAAQ,MAAM,EACd,MAAM,MAAM,EACZ,QAAQ,MAAM,EAAE,KACf,aA8BF,CAAC;AAEF,eAAO,MAAM,cAAc,GACzB,QAAQ,kBAAkB,EAC1B,WAAW,MAAM,EACjB,aAAa,MAAM,EACnB,QAAQ,MAAM,EACd,MAAM,MAAM,KACX,cA+BF,CAAC"}
@@ -10,7 +10,6 @@
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 } from 'react';
14
13
  import { useGrpcMetadata } from '@parca/components';
15
14
  import { millisToProtoTimestamp, sanitizeLabelValue } from '@parca/utilities';
16
15
  import useGrpcQuery from '../useGrpcQuery';
@@ -35,9 +34,6 @@ export const useLabelNames = (client, profileType, start, end, match) => {
35
34
  keepPreviousData: false,
36
35
  },
37
36
  });
38
- useEffect(() => {
39
- console.log('Label names query result:', { data, error, isLoading });
40
- }, [data, error, isLoading]);
41
37
  return {
42
38
  result: { response: data, error: error },
43
39
  loading: isLoading,
@@ -67,9 +63,6 @@ export const useLabelValues = (client, labelName, profileType, start, end) => {
67
63
  keepPreviousData: false,
68
64
  },
69
65
  });
70
- useEffect(() => {
71
- console.log('Label values query result:', { data, error, isLoading, labelName });
72
- }, [data, error, isLoading, labelName]);
73
66
  return {
74
67
  result: { response: data ?? [], error: error },
75
68
  loading: isLoading,
@@ -8,6 +8,7 @@ interface UseQueryStateOptions {
8
8
  defaultFrom?: number;
9
9
  defaultTo?: number;
10
10
  comparing?: boolean;
11
+ onProfileTypeChange?: () => void;
11
12
  }
12
13
  interface UseQueryStateReturn {
13
14
  querySelection: QuerySelection;
@@ -28,6 +29,9 @@ interface UseQueryStateReturn {
28
29
  sumByLoading: boolean;
29
30
  draftParsedQuery: Query | null;
30
31
  parsedQuery: Query | null;
32
+ setExpressionParam: (value: string | undefined) => void;
33
+ setSumByParam: (value: string | undefined) => void;
34
+ setGroupByParam: (value: string[] | undefined) => void;
31
35
  }
32
36
  export declare const useQueryState: (options?: UseQueryStateOptions) => UseQueryStateReturn;
33
37
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"useQueryState.d.ts","sourceRoot":"","sources":["../../src/hooks/useQueryState.ts"],"names":[],"mappings":"AAgBA,OAAO,EAAC,KAAK,EAAC,MAAM,eAAe,CAAC;AAEpC,OAAO,EAAC,cAAc,EAAC,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAC,gBAAgB,EAA8B,aAAa,EAAC,MAAM,kBAAkB,CAAC;AAK7F,UAAU,oBAAoB;IAC5B,MAAM,CAAC,EAAE,IAAI,GAAG,IAAI,CAAC;IACrB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,UAAU,mBAAmB;IAE3B,cAAc,EAAE,cAAc,CAAC;IAG/B,cAAc,EAAE,cAAc,CAAC;IAG/B,kBAAkB,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,IAAI,CAAC;IACjD,iBAAiB,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7E,aAAa,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,SAAS,KAAK,IAAI,CAAC;IACrD,mBAAmB,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,IAAI,CAAC;IACnD,gBAAgB,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IAG7C,WAAW,EAAE,CAAC,kBAAkB,CAAC,EAAE;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAC;QAAC,aAAa,EAAE,MAAM,CAAA;KAAC,KAAK,IAAI,CAAC;IAG9F,gBAAgB,EAAE,gBAAgB,GAAG,IAAI,CAAC;IAG1C,aAAa,EAAE,aAAa,GAAG,IAAI,CAAC;IAGpC,mBAAmB,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;IAGhF,YAAY,EAAE,OAAO,CAAC;IAGtB,gBAAgB,EAAE,KAAK,GAAG,IAAI,CAAC;IAG/B,WAAW,EAAE,KAAK,GAAG,IAAI,CAAC;CAC3B;AAED,eAAO,MAAM,aAAa,GAAI,UAAS,oBAAyB,KAAG,mBAgZlE,CAAC"}
1
+ {"version":3,"file":"useQueryState.d.ts","sourceRoot":"","sources":["../../src/hooks/useQueryState.ts"],"names":[],"mappings":"AAgBA,OAAO,EAAC,KAAK,EAAC,MAAM,eAAe,CAAC;AAEpC,OAAO,EAAC,cAAc,EAAC,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAC,gBAAgB,EAA8B,aAAa,EAAC,MAAM,kBAAkB,CAAC;AAK7F,UAAU,oBAAoB;IAC5B,MAAM,CAAC,EAAE,IAAI,GAAG,IAAI,CAAC;IACrB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,mBAAmB,CAAC,EAAE,MAAM,IAAI,CAAC;CAClC;AAED,UAAU,mBAAmB;IAE3B,cAAc,EAAE,cAAc,CAAC;IAG/B,cAAc,EAAE,cAAc,CAAC;IAG/B,kBAAkB,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,IAAI,CAAC;IACjD,iBAAiB,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7E,aAAa,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,SAAS,KAAK,IAAI,CAAC;IACrD,mBAAmB,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,IAAI,CAAC;IACnD,gBAAgB,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IAG7C,WAAW,EAAE,CAAC,kBAAkB,CAAC,EAAE;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAC;QAAC,aAAa,EAAE,MAAM,CAAA;KAAC,KAAK,IAAI,CAAC;IAG9F,gBAAgB,EAAE,gBAAgB,GAAG,IAAI,CAAC;IAG1C,aAAa,EAAE,aAAa,GAAG,IAAI,CAAC;IAGpC,mBAAmB,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;IAGhF,YAAY,EAAE,OAAO,CAAC;IAGtB,gBAAgB,EAAE,KAAK,GAAG,IAAI,CAAC;IAG/B,WAAW,EAAE,KAAK,GAAG,IAAI,CAAC;IAE1B,kBAAkB,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,KAAK,IAAI,CAAC;IACxD,aAAa,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,KAAK,IAAI,CAAC;IACnD,eAAe,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,SAAS,KAAK,IAAI,CAAC;CACxD;AAED,eAAO,MAAM,aAAa,GAAI,UAAS,oBAAyB,KAAG,mBA2alE,CAAC"}
@@ -20,7 +20,7 @@ import { DEFAULT_EMPTY_SUM_BY, sumByToParam, useSumBy, useSumByFromParams } from
20
20
  export const useQueryState = (options = {}) => {
21
21
  const { queryServiceClient: queryClient } = useParcaContext();
22
22
  const { suffix = '', defaultExpression = '', defaultTimeSelection = 'relative:minute|15', // Default to 15 minutes relative
23
- defaultFrom, defaultTo, comparing = false, } = options;
23
+ defaultFrom, defaultTo, comparing = false, onProfileTypeChange, } = options;
24
24
  const batchUpdates = useURLStateBatch();
25
25
  const resetFlameGraphState = useResetFlameGraphState();
26
26
  const resetStateOnProfileTypeChange = useResetStateOnProfileTypeChange();
@@ -38,6 +38,9 @@ export const useQueryState = (options = {}) => {
38
38
  defaultValue: defaultTimeSelection,
39
39
  });
40
40
  const [sumByParam, setSumByParam] = useURLState(`sum_by${suffix}`);
41
+ const [, setGroupByParam] = useURLState('group_by', {
42
+ alwaysReturnArray: true,
43
+ });
41
44
  const [mergeFrom, setMergeFromState] = useURLState(`merge_from${suffix}`);
42
45
  const [mergeTo, setMergeToState] = useURLState(`merge_to${suffix}`);
43
46
  // ProfileSelection URL state hooks - reuses merge_from/merge_to but adds selection
@@ -54,7 +57,11 @@ export const useQueryState = (options = {}) => {
54
57
  try {
55
58
  return Query.parse(draftExpression ?? '');
56
59
  }
57
- catch {
60
+ catch (error) {
61
+ console.warn('Failed to parse draft expression', {
62
+ expression: draftExpression,
63
+ error: error instanceof Error ? error.message : String(error),
64
+ });
58
65
  return Query.parse('');
59
66
  }
60
67
  }, [draftExpression]);
@@ -62,7 +69,11 @@ export const useQueryState = (options = {}) => {
62
69
  try {
63
70
  return Query.parse(expression ?? '');
64
71
  }
65
- catch {
72
+ catch (error) {
73
+ console.warn('Failed to parse expression', {
74
+ expression,
75
+ error: error instanceof Error ? error.message : String(error),
76
+ });
66
77
  return Query.parse('');
67
78
  }
68
79
  }, [expression]);
@@ -232,6 +243,7 @@ export const useQueryState = (options = {}) => {
232
243
  if (draftProfileType.toString() !==
233
244
  Query.parse(querySelection.expression).profileType().toString()) {
234
245
  resetStateOnProfileTypeChange();
246
+ onProfileTypeChange?.();
235
247
  }
236
248
  });
237
249
  }, [
@@ -254,6 +266,7 @@ export const useQueryState = (options = {}) => {
254
266
  setSelectionParam,
255
267
  resetFlameGraphState,
256
268
  resetStateOnProfileTypeChange,
269
+ onProfileTypeChange,
257
270
  draftProfileType,
258
271
  querySelection.expression,
259
272
  ]);
@@ -290,7 +303,11 @@ export const useQueryState = (options = {}) => {
290
303
  try {
291
304
  return Query.parse(draftSelection.expression ?? '');
292
305
  }
293
- catch {
306
+ catch (error) {
307
+ console.warn('Failed to parse draft selection expression', {
308
+ expression: draftSelection.expression,
309
+ error: error instanceof Error ? error.message : String(error),
310
+ });
294
311
  return Query.parse('');
295
312
  }
296
313
  }, [draftSelection.expression]);
@@ -298,7 +315,11 @@ export const useQueryState = (options = {}) => {
298
315
  try {
299
316
  return Query.parse(querySelection.expression ?? '');
300
317
  }
301
- catch {
318
+ catch (error) {
319
+ console.warn('Failed to parse query selection expression', {
320
+ expression: querySelection.expression,
321
+ error: error instanceof Error ? error.message : String(error),
322
+ });
302
323
  return Query.parse('');
303
324
  }
304
325
  }, [querySelection.expression]);
@@ -323,5 +344,8 @@ export const useQueryState = (options = {}) => {
323
344
  sumByLoading: isDraftSumByLoading || sumBySelectionLoading,
324
345
  draftParsedQuery,
325
346
  parsedQuery,
347
+ setExpressionParam: setExpressionState,
348
+ setSumByParam,
349
+ setGroupByParam,
326
350
  };
327
351
  };
@@ -1,6 +1,21 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
+ // Copyright 2022 The Parca Authors
3
+ // Licensed under the Apache License, Version 2.0 (the "License");
4
+ // you may not use this file except in compliance with the License.
5
+ // You may obtain a copy of the License at
6
+ //
7
+ // http://www.apache.org/licenses/LICENSE-2.0
8
+ //
9
+ // Unless required by applicable law or agreed to in writing, software
10
+ // distributed under the License is distributed on an "AS IS" BASIS,
11
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ // See the License for the specific language governing permissions and
13
+ // limitations under the License.
14
+ import { act } from 'react';
2
15
  // eslint-disable-next-line import/named
3
- import { act, renderHook, waitFor } from '@testing-library/react';
16
+ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
17
+ // eslint-disable-next-line import/named
18
+ import { renderHook, waitFor } from '@testing-library/react';
4
19
  import { beforeEach, describe, expect, it, vi } from 'vitest';
5
20
  import { URLStateProvider } from '@parca/components';
6
21
  import { useQueryState } from './useQueryState';
@@ -72,9 +87,38 @@ vi.mock('../useSumBy', async () => {
72
87
  },
73
88
  };
74
89
  });
90
+ // Track profile types loading state for tests
91
+ let mockProfileTypesLoading = false;
92
+ let mockProfileTypesData;
93
+ // Mock useProfileTypes to control loading state in tests
94
+ vi.mock('../ProfileSelector', async () => {
95
+ const actual = await vi.importActual('../ProfileSelector');
96
+ return {
97
+ ...actual,
98
+ useProfileTypes: () => ({
99
+ loading: mockProfileTypesLoading,
100
+ data: mockProfileTypesData,
101
+ error: null,
102
+ }),
103
+ };
104
+ });
105
+ // Helper to set profile types loading state for tests
106
+ const setProfileTypesLoading = (loading) => {
107
+ mockProfileTypesLoading = loading;
108
+ };
109
+ const setProfileTypesData = (data) => {
110
+ mockProfileTypesData = data;
111
+ };
75
112
  // Helper to create wrapper with URLStateProvider
76
113
  const createWrapper = (paramPreferences = {}) => {
77
- const Wrapper = ({ children }) => (_jsx(URLStateProvider, { navigateTo: mockNavigateTo, paramPreferences: paramPreferences, children: children }));
114
+ const queryClient = new QueryClient({
115
+ defaultOptions: {
116
+ queries: {
117
+ retry: false,
118
+ },
119
+ },
120
+ });
121
+ const Wrapper = ({ children }) => (_jsx(QueryClientProvider, { client: queryClient, children: _jsx(URLStateProvider, { navigateTo: mockNavigateTo, paramPreferences: paramPreferences, children: children }) }));
78
122
  Wrapper.displayName = 'URLStateProviderWrapper';
79
123
  return Wrapper;
80
124
  };
@@ -86,17 +130,20 @@ describe('useQueryState', () => {
86
130
  writable: true,
87
131
  });
88
132
  mockLocation.search = '';
133
+ // Reset profile types mock state
134
+ setProfileTypesLoading(false);
135
+ setProfileTypesData(undefined);
89
136
  });
90
137
  describe('Basic functionality', () => {
91
138
  it('should initialize with default values', () => {
92
139
  const { result } = renderHook(() => useQueryState({
93
- defaultExpression: 'process_cpu{}',
140
+ defaultExpression: 'process_cpu:cpu:nanoseconds:cpu:nanoseconds{}',
94
141
  defaultTimeSelection: 'relative:hour|1',
95
142
  defaultFrom: 1000,
96
143
  defaultTo: 2000,
97
144
  }), { wrapper: createWrapper() });
98
145
  const { querySelection } = result.current;
99
- expect(querySelection.expression).toBe('process_cpu{}');
146
+ expect(querySelection.expression).toBe('process_cpu:cpu:nanoseconds:cpu:nanoseconds{}');
100
147
  expect(querySelection.timeSelection).toBe('relative:hour|1');
101
148
  // From/to should be calculated from the range
102
149
  expect(querySelection.from).toBeDefined();
@@ -395,12 +442,28 @@ describe('useQueryState', () => {
395
442
  });
396
443
  });
397
444
  describe('Edge cases', () => {
398
- it('should handle invalid expression gracefully', () => {
445
+ it('should handle invalid expression gracefully and log warning', () => {
446
+ const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => { });
399
447
  const { result } = renderHook(() => useQueryState({
400
448
  defaultExpression: 'invalid{{}expression',
401
449
  }), { wrapper: createWrapper() });
402
- // Should not throw error
450
+ // Should not throw error - invalid expressions are caught and logged
451
+ expect(() => result.current.querySelection).not.toThrow();
452
+ // Should fall back to empty expression
453
+ expect(result.current.querySelection.expression).toBe('invalid{{}expression');
454
+ // Should log a warning about the parse failure
455
+ expect(consoleSpy).toHaveBeenCalledWith('Failed to parse expression', expect.objectContaining({
456
+ expression: 'invalid{{}expression',
457
+ }));
458
+ consoleSpy.mockRestore();
459
+ });
460
+ it('should handle empty expression gracefully', () => {
461
+ const { result } = renderHook(() => useQueryState({
462
+ defaultExpression: '',
463
+ }), { wrapper: createWrapper() });
464
+ // Should not throw error with empty expression
403
465
  expect(() => result.current.querySelection).not.toThrow();
466
+ expect(result.current.querySelection.expression).toBe('');
404
467
  });
405
468
  it('should clear merge params for non-delta profiles', async () => {
406
469
  mockLocation.search =
@@ -896,7 +959,8 @@ describe('useQueryState', () => {
896
959
  });
897
960
  });
898
961
  it('should preserve other URL params when setting ProfileSelection', async () => {
899
- mockLocation.search = '?expression_a=process_cpu{}&other_param=value&unrelated=test';
962
+ mockLocation.search =
963
+ '?expression_a=process_cpu:cpu:nanoseconds:cpu:nanoseconds{}&other_param=value&unrelated=test';
900
964
  const { result } = renderHook(() => useQueryState({ suffix: '_a' }), { wrapper: createWrapper() });
901
965
  const mockQuery = {
902
966
  toString: () => 'process_cpu:cpu:nanoseconds:cpu:nanoseconds{pod="test"}',
@@ -911,7 +975,7 @@ describe('useQueryState', () => {
911
975
  // ProfileSelection params should be set
912
976
  expect(params.selection_a).toBe('process_cpu:cpu:nanoseconds:cpu:nanoseconds{pod="test"}');
913
977
  // Other params should be preserved
914
- expect(params.expression_a).toBe('process_cpu{}');
978
+ expect(params.expression_a).toBe('process_cpu:cpu:nanoseconds:cpu:nanoseconds{}');
915
979
  expect(params.other_param).toBe('value');
916
980
  expect(params.unrelated).toBe('test');
917
981
  });
package/dist/index.d.ts CHANGED
@@ -11,7 +11,7 @@ import { useQueryState } from './hooks/useQueryState';
11
11
  export { useMetricsGraphDimensions } from './MetricsGraph/useMetricsGraphDimensions';
12
12
  export * from './ProfileFlameGraph';
13
13
  export * from './ProfileSource';
14
- export { convertToProtoFilters, convertFromProtoFilters, } from './ProfileView/components/ProfileFilters/useProfileFilters';
14
+ export { convertToProtoFilters, convertFromProtoFilters, useProfileFilters, type ProfileFilter, } from './ProfileView/components/ProfileFilters/useProfileFilters';
15
15
  export * from './ProfileView';
16
16
  export * from './ProfileViewWithData';
17
17
  export * from './utils';
@@ -23,5 +23,6 @@ export { QueryControls } from './QueryControls';
23
23
  export { default as ProfileFilters } from './ProfileView/components/ProfileFilters';
24
24
  export { useProfileFiltersUrlState } from './ProfileView/components/ProfileFilters/useProfileFiltersUrlState';
25
25
  export declare const DEFAULT_PROFILE_EXPLORER_PARAM_VALUES: ParamPreferences;
26
+ export { useProfileTypes } from './ProfileSelector';
26
27
  export { ProfileExplorer, ProfileTypeSelector, CustomSelect, SelectWithRefresh, useLabelNames, MetricsGraph, type ContextMenuItemOrSubmenu, type Series, LabelsQueryProvider, useLabelsQueryProvider, UnifiedLabelsProvider, useUnifiedLabels, useQueryState, type LabelsQueryProviderContextType, };
27
28
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.tsx"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,mBAAmB,CAAC;AAExD,OAAO,YAAY,EAAE,EAAC,KAAK,wBAAwB,EAAE,KAAK,MAAM,EAAC,MAAM,gBAAgB,CAAC;AACxF,OAAO,eAAe,MAAM,mBAAmB,CAAC;AAChD,OAAO,mBAAmB,MAAM,uBAAuB,CAAC;AACxD,OAAO,EAAC,iBAAiB,EAAC,MAAM,qBAAqB,CAAC;AACtD,OAAO,YAAY,MAAM,yBAAyB,CAAC;AACnD,OAAO,EACL,mBAAmB,EACnB,sBAAsB,EACtB,KAAK,8BAA8B,EACpC,MAAM,gCAAgC,CAAC;AACxC,OAAO,EAAC,qBAAqB,EAAE,gBAAgB,EAAC,MAAM,iCAAiC,CAAC;AACxF,OAAO,EAAC,aAAa,EAAC,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAC,aAAa,EAAC,MAAM,uBAAuB,CAAC;AAEpD,OAAO,EAAC,yBAAyB,EAAC,MAAM,0CAA0C,CAAC;AAEnF,cAAc,qBAAqB,CAAC;AACpC,cAAc,iBAAiB,CAAC;AAChC,OAAO,EACL,qBAAqB,EACrB,uBAAuB,GACxB,MAAM,2DAA2D,CAAC;AACnE,cAAc,eAAe,CAAC;AAC9B,cAAc,uBAAuB,CAAC;AACtC,cAAc,SAAS,CAAC;AACxB,cAAc,uBAAuB,CAAC;AACtC,cAAc,cAAc,CAAC;AAC7B,cAAc,uBAAuB,CAAC;AACtC,cAAc,YAAY,CAAC;AAC3B,OAAO,EAAC,aAAa,EAAC,MAAM,iBAAiB,CAAC;AAE9C,OAAO,EAAC,OAAO,IAAI,cAAc,EAAC,MAAM,yCAAyC,CAAC;AAClF,OAAO,EAAC,yBAAyB,EAAC,MAAM,mEAAmE,CAAC;AAE5G,eAAO,MAAM,qCAAqC,EAAE,gBAQnD,CAAC;AAEF,OAAO,EACL,eAAe,EACf,mBAAmB,EACnB,YAAY,EACZ,iBAAiB,EACjB,aAAa,EACb,YAAY,EACZ,KAAK,wBAAwB,EAC7B,KAAK,MAAM,EACX,mBAAmB,EACnB,sBAAsB,EACtB,qBAAqB,EACrB,gBAAgB,EAChB,aAAa,EACb,KAAK,8BAA8B,GACpC,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.tsx"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,mBAAmB,CAAC;AAExD,OAAO,YAAY,EAAE,EAAC,KAAK,wBAAwB,EAAE,KAAK,MAAM,EAAC,MAAM,gBAAgB,CAAC;AACxF,OAAO,eAAe,MAAM,mBAAmB,CAAC;AAChD,OAAO,mBAAmB,MAAM,uBAAuB,CAAC;AACxD,OAAO,EAAC,iBAAiB,EAAC,MAAM,qBAAqB,CAAC;AACtD,OAAO,YAAY,MAAM,yBAAyB,CAAC;AACnD,OAAO,EACL,mBAAmB,EACnB,sBAAsB,EACtB,KAAK,8BAA8B,EACpC,MAAM,gCAAgC,CAAC;AACxC,OAAO,EAAC,qBAAqB,EAAE,gBAAgB,EAAC,MAAM,iCAAiC,CAAC;AACxF,OAAO,EAAC,aAAa,EAAC,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAC,aAAa,EAAC,MAAM,uBAAuB,CAAC;AAEpD,OAAO,EAAC,yBAAyB,EAAC,MAAM,0CAA0C,CAAC;AAEnF,cAAc,qBAAqB,CAAC;AACpC,cAAc,iBAAiB,CAAC;AAChC,OAAO,EACL,qBAAqB,EACrB,uBAAuB,EACvB,iBAAiB,EACjB,KAAK,aAAa,GACnB,MAAM,2DAA2D,CAAC;AACnE,cAAc,eAAe,CAAC;AAC9B,cAAc,uBAAuB,CAAC;AACtC,cAAc,SAAS,CAAC;AACxB,cAAc,uBAAuB,CAAC;AACtC,cAAc,cAAc,CAAC;AAC7B,cAAc,uBAAuB,CAAC;AACtC,cAAc,YAAY,CAAC;AAC3B,OAAO,EAAC,aAAa,EAAC,MAAM,iBAAiB,CAAC;AAE9C,OAAO,EAAC,OAAO,IAAI,cAAc,EAAC,MAAM,yCAAyC,CAAC;AAClF,OAAO,EAAC,yBAAyB,EAAC,MAAM,mEAAmE,CAAC;AAE5G,eAAO,MAAM,qCAAqC,EAAE,gBAQnD,CAAC;AAEF,OAAO,EAAC,eAAe,EAAC,MAAM,mBAAmB,CAAC;AAElD,OAAO,EACL,eAAe,EACf,mBAAmB,EACnB,YAAY,EACZ,iBAAiB,EACjB,aAAa,EACb,YAAY,EACZ,KAAK,wBAAwB,EAC7B,KAAK,MAAM,EACX,mBAAmB,EACnB,sBAAsB,EACtB,qBAAqB,EACrB,gBAAgB,EAChB,aAAa,EACb,KAAK,8BAA8B,GACpC,CAAC"}
package/dist/index.js CHANGED
@@ -22,7 +22,7 @@ import { useQueryState } from './hooks/useQueryState';
22
22
  export { useMetricsGraphDimensions } from './MetricsGraph/useMetricsGraphDimensions';
23
23
  export * from './ProfileFlameGraph';
24
24
  export * from './ProfileSource';
25
- export { convertToProtoFilters, convertFromProtoFilters, } from './ProfileView/components/ProfileFilters/useProfileFilters';
25
+ export { convertToProtoFilters, convertFromProtoFilters, useProfileFilters, } from './ProfileView/components/ProfileFilters/useProfileFilters';
26
26
  export * from './ProfileView';
27
27
  export * from './ProfileViewWithData';
28
28
  export * from './utils';
@@ -42,4 +42,5 @@ export const DEFAULT_PROFILE_EXPLORER_PARAM_VALUES = {
42
42
  splitOnCommas: true,
43
43
  },
44
44
  };
45
+ export { useProfileTypes } from './ProfileSelector';
45
46
  export { ProfileExplorer, ProfileTypeSelector, CustomSelect, SelectWithRefresh, useLabelNames, MetricsGraph, LabelsQueryProvider, useLabelsQueryProvider, UnifiedLabelsProvider, useUnifiedLabels, useQueryState, };
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@parca/profile",
3
- "version": "0.19.109",
3
+ "version": "0.19.111",
4
4
  "description": "Profile viewing libraries",
5
5
  "dependencies": {
6
6
  "@floating-ui/react": "^0.27.12",
7
7
  "@headlessui/react": "^1.7.19",
8
8
  "@iconify/react": "^4.0.0",
9
9
  "@parca/client": "0.17.16",
10
- "@parca/components": "0.16.395",
10
+ "@parca/components": "0.16.396",
11
11
  "@parca/dynamicsize": "0.16.72",
12
12
  "@parca/hooks": "0.0.116",
13
13
  "@parca/icons": "0.16.79",
@@ -84,5 +84,5 @@
84
84
  "access": "public",
85
85
  "registry": "https://registry.npmjs.org/"
86
86
  },
87
- "gitHead": "02333e7ff5910eca5a0f85ba42908dfcce1c19c5"
87
+ "gitHead": "77f4ccf0e86c3d2c62f8a24d2d5316b78f24dec7"
88
88
  }
@@ -30,6 +30,10 @@ import {TEST_IDS, testId} from '@parca/test-utils';
30
30
  import {millisToProtoTimestamp, type NavigateFunction} from '@parca/utilities';
31
31
 
32
32
  import {useMetricsGraphDimensions} from '../MetricsGraph/useMetricsGraphDimensions';
33
+ import {
34
+ ProfileFilter,
35
+ useProfileFilters,
36
+ } from '../ProfileView/components/ProfileFilters/useProfileFilters';
33
37
  import {QueryControls} from '../QueryControls';
34
38
  import {LabelsQueryProvider, useLabelsQueryProvider} from '../contexts/LabelsQueryProvider';
35
39
  import {UnifiedLabelsProvider} from '../contexts/UnifiedLabelsContext';
@@ -119,6 +123,15 @@ const ProfileSelector = ({
119
123
  const [queryBrowserMode, setQueryBrowserMode] = useURLState('query_browser_mode');
120
124
  const batchUpdates = useURLStateBatch();
121
125
 
126
+ const profileFilterDefaults = viewComponent?.profileFilterDefaults as ProfileFilter[] | undefined;
127
+ const {forceApplyFilters} = useProfileFilters();
128
+
129
+ const handleProfileTypeChange = useCallback(() => {
130
+ if (profileFilterDefaults != null && profileFilterDefaults.length > 0) {
131
+ forceApplyFilters(profileFilterDefaults);
132
+ }
133
+ }, [forceApplyFilters, profileFilterDefaults]);
134
+
122
135
  // Use the new useQueryState hook - reads directly from URL params
123
136
  const {
124
137
  querySelection,
@@ -133,7 +146,7 @@ const ProfileSelector = ({
133
146
  setProfileSelection,
134
147
  sumByLoading,
135
148
  draftParsedQuery,
136
- } = useQueryState({suffix});
149
+ } = useQueryState({suffix, onProfileTypeChange: handleProfileTypeChange});
137
150
 
138
151
  // Use draft state for local state instead of committed state
139
152
  const [timeRangeSelection, setTimeRangeSelection] = useState(
@@ -137,7 +137,7 @@ export const useAutoQuerySelector = ({
137
137
  }
138
138
  dispatch(setAutoQuery('true'));
139
139
  let profileType = profileTypesData.types.find(
140
- type => type.name === 'parca_agent' && type.delta
140
+ type => type.name === 'parca_agent' && type.sampleType === 'samples' && type.delta
141
141
  );
142
142
  if (profileType == null) {
143
143
  profileType = profileTypesData.types.find(
@@ -237,8 +237,10 @@ export const useProfileFilters = (): {
237
237
  removeFilter: (id: string) => void;
238
238
  updateFilter: (id: string, updates: Partial<ProfileFilter>) => void;
239
239
  resetFilters: () => void;
240
+ setAppliedFilters: (filters: ProfileFilter[]) => void;
241
+ forceApplyFilters: (filters: ProfileFilter[]) => void;
240
242
  } => {
241
- const {appliedFilters, setAppliedFilters} = useProfileFiltersUrlState();
243
+ const {appliedFilters, setAppliedFilters, forceApplyFilters} = useProfileFiltersUrlState();
242
244
  const resetFlameGraphState = useResetFlameGraphState();
243
245
 
244
246
  const [localFilters, setLocalFilters] = useState<ProfileFilter[]>(appliedFilters ?? []);
@@ -422,5 +424,7 @@ export const useProfileFilters = (): {
422
424
  removeFilter,
423
425
  updateFilter,
424
426
  resetFilters,
427
+ setAppliedFilters,
428
+ forceApplyFilters,
425
429
  };
426
430
  };