@parca/profile 0.16.342 → 0.16.344

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 (31) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/MetricsGraph/index.d.ts +1 -1
  3. package/dist/MetricsGraph/index.js +2 -2
  4. package/dist/ProfileIcicleGraph/ActionButtons/GroupByDropdown.js +1 -1
  5. package/dist/ProfileIcicleGraph/ActionButtons/RuntimeFilterDropdown.js +1 -1
  6. package/dist/ProfileIcicleGraph/ActionButtons/SortBySelect.js +2 -2
  7. package/dist/ProfileIcicleGraph/index.js +1 -1
  8. package/dist/ProfileMetricsGraph/index.d.ts +1 -1
  9. package/dist/ProfileMetricsGraph/index.js +2 -2
  10. package/dist/ProfileSelector/index.js +3 -5
  11. package/dist/ProfileTypeSelector/index.js +1 -1
  12. package/dist/ProfileView/FilterByFunctionButton.js +1 -1
  13. package/dist/ProfileView/ViewSelector.d.ts +2 -1
  14. package/dist/ProfileView/ViewSelector.js +2 -2
  15. package/dist/ProfileView/VisualizationPanel.js +1 -1
  16. package/dist/ProfileView/index.js +2 -2
  17. package/dist/components/DiffLegend.js +1 -1
  18. package/package.json +3 -3
  19. package/src/MetricsGraph/index.tsx +3 -2
  20. package/src/ProfileIcicleGraph/ActionButtons/GroupByDropdown.tsx +2 -5
  21. package/src/ProfileIcicleGraph/ActionButtons/RuntimeFilterDropdown.tsx +2 -5
  22. package/src/ProfileIcicleGraph/ActionButtons/SortBySelect.tsx +1 -1
  23. package/src/ProfileIcicleGraph/index.tsx +1 -1
  24. package/src/ProfileMetricsGraph/index.tsx +13 -3
  25. package/src/ProfileSelector/index.tsx +9 -6
  26. package/src/ProfileTypeSelector/index.tsx +1 -1
  27. package/src/ProfileView/FilterByFunctionButton.tsx +1 -1
  28. package/src/ProfileView/ViewSelector.tsx +4 -0
  29. package/src/ProfileView/VisualizationPanel.tsx +6 -1
  30. package/src/ProfileView/index.tsx +2 -1
  31. package/src/components/DiffLegend.tsx +1 -1
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.344](https://github.com/parca-dev/parca/compare/@parca/profile@0.16.343...@parca/profile@0.16.344) (2024-02-21)
7
+
8
+ **Note:** Version bump only for package @parca/profile
9
+
10
+ ## [0.16.343](https://github.com/parca-dev/parca/compare/@parca/profile@0.16.342...@parca/profile@0.16.343) (2024-02-20)
11
+
12
+ **Note:** Version bump only for package @parca/profile
13
+
6
14
  ## [0.16.342](https://github.com/parca-dev/parca/compare/@parca/profile@0.16.341...@parca/profile@0.16.342) (2024-02-14)
7
15
 
8
16
  **Note:** Version bump only for package @parca/profile
@@ -7,7 +7,7 @@ interface Props {
7
7
  from: number;
8
8
  to: number;
9
9
  profile: MergedProfileSelection | null;
10
- onSampleClick: (timestamp: number, value: number, labels: Label[]) => void;
10
+ onSampleClick: (timestamp: number, value: number, labels: Label[], duration: number) => void;
11
11
  addLabelMatcher: (labels: {
12
12
  key: string;
13
13
  value: string;
@@ -139,8 +139,8 @@ export const RawMetricsGraph = ({ data, from, to, profile, onSampleClick, addLab
139
139
  };
140
140
  const openClosestProfile = () => {
141
141
  if (highlighted != null) {
142
- onSampleClick(Math.round(highlighted.timestamp), highlighted.value, sanitizeHighlightedValues(highlighted.labels) // When a user clicks on any sample in the graph, replace single `\` in the `labelValues` string with doubles `\\` if available.
143
- );
142
+ onSampleClick(Math.round(highlighted.timestamp), highlighted.value, sanitizeHighlightedValues(highlighted.labels), // When a user clicks on any sample in the graph, replace single `\` in the `labelValues` string with doubles `\\` if available.
143
+ highlighted.duration);
144
144
  }
145
145
  };
146
146
  const onMouseUp = (e) => {
@@ -52,6 +52,6 @@ const GroupByDropdown = ({ groupBy, toggleGroupBy, }) => {
52
52
  : groupBy.length === 1
53
53
  ? groupByOptions.find(option => option.value === groupBy[0])?.label
54
54
  : 'Multiple';
55
- return (_jsxs("div", { className: "relative", children: [_jsx("label", { className: "text-sm", children: "Group" }), _jsxs(Menu, { as: "div", className: "relative text-left", children: [_jsxs(Menu.Button, { id: "h-group-by-filter", className: "relative w-max cursor-default rounded-md border bg-white py-2 pl-3 pr-[1.7rem] text-left text-sm shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 dark:border-gray-600 dark:bg-gray-900 sm:text-sm", children: [_jsx("span", { className: "block overflow-x-hidden text-ellipsis", children: label }), _jsx("span", { className: "pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2 text-gray-400", children: _jsx(Icon, { icon: "heroicons:chevron-down-20-solid", "aria-hidden": "true" }) })] }), _jsx(Transition, { as: "div", leave: "transition ease-in duration-100", leaveFrom: "opacity-100", leaveTo: "opacity-0", children: _jsx(Menu.Items, { className: "absolute left-0 z-10 mt-1 min-w-[400px] overflow-auto rounded-md bg-gray-50 py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none dark:border-gray-600 dark:bg-gray-900 dark:ring-white dark:ring-opacity-20 sm:text-sm", children: _jsx("div", { className: "p-4", children: _jsx("fieldset", { children: _jsx("div", { className: "space-y-5", children: groupByOptions.map(({ value, label, description, disabled }) => (_jsxs("div", { className: "relative flex items-start", children: [_jsx("div", { className: "flex h-6 items-center", children: _jsx("input", { id: value, name: value, type: "checkbox", disabled: disabled, className: "h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-600", checked: groupBy.includes(value), onChange: () => toggleGroupBy(value) }) }), _jsxs("div", { className: "ml-3 text-sm leading-6", children: [_jsx("label", { htmlFor: value, className: "font-medium text-gray-900 dark:text-gray-200", children: label }), _jsx("p", { className: "text-gray-500 dark:text-gray-400", children: description })] })] }, value))) }) }) }) }) })] })] }));
55
+ return (_jsxs("div", { className: "relative", children: [_jsx("label", { className: "text-sm", children: "Group" }), _jsxs(Menu, { as: "div", className: "relative text-left", id: "h-group-by-filter", children: [_jsxs(Menu.Button, { className: "relative w-max cursor-default rounded-md border bg-white py-2 pl-3 pr-[1.7rem] text-left text-sm shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 dark:border-gray-600 dark:bg-gray-900 sm:text-sm", children: [_jsx("span", { className: "block overflow-x-hidden text-ellipsis", children: label }), _jsx("span", { className: "pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2 text-gray-400", children: _jsx(Icon, { icon: "heroicons:chevron-down-20-solid", "aria-hidden": "true" }) })] }), _jsx(Transition, { as: "div", leave: "transition ease-in duration-100", leaveFrom: "opacity-100", leaveTo: "opacity-0", children: _jsx(Menu.Items, { className: "absolute left-0 z-10 mt-1 min-w-[400px] overflow-auto rounded-md bg-gray-50 py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none dark:border-gray-600 dark:bg-gray-900 dark:ring-white dark:ring-opacity-20 sm:text-sm", children: _jsx("div", { className: "p-4", children: _jsx("fieldset", { children: _jsx("div", { className: "space-y-5", children: groupByOptions.map(({ value, label, description, disabled }) => (_jsxs("div", { className: "relative flex items-start", children: [_jsx("div", { className: "flex h-6 items-center", children: _jsx("input", { id: value, name: value, type: "checkbox", disabled: disabled, className: "h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-600", checked: groupBy.includes(value), onChange: () => toggleGroupBy(value) }) }), _jsxs("div", { className: "ml-3 text-sm leading-6", children: [_jsx("label", { htmlFor: value, className: "font-medium text-gray-900 dark:text-gray-200", children: label }), _jsx("p", { className: "text-gray-500 dark:text-gray-400", children: description })] })] }, value))) }) }) }) }) })] })] }));
56
56
  };
57
57
  export default GroupByDropdown;
@@ -18,6 +18,6 @@ const RuntimeToggle = ({ id, state, toggle, label, description, }) => {
18
18
  return (_jsxs("div", { className: "relative flex items-start", children: [_jsx("div", { className: "flex h-6 items-center", children: _jsx("input", { id: id, name: id, type: "checkbox", className: "h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-600", checked: state, onChange: () => toggle() }) }), _jsxs("div", { className: "ml-3 text-sm leading-6", children: [_jsx("label", { htmlFor: id, className: "font-medium text-gray-900 dark:text-gray-200", children: label }), _jsx("p", { className: "text-gray-500 dark:text-gray-400", children: description })] })] }, id));
19
19
  };
20
20
  const RuntimeFilterDropdown = ({ showRuntimeRuby, toggleShowRuntimeRuby, showRuntimePython, toggleShowRuntimePython, showInterpretedOnly, toggleShowInterpretedOnly, }) => {
21
- return (_jsxs("div", { children: [_jsx("label", { className: "text-sm", children: "Runtimes" }), _jsxs(Menu, { as: "div", className: "relative text-left", children: [_jsx("div", { children: _jsxs(Menu.Button, { className: "relative w-full cursor-default rounded-md border bg-white py-2 pl-3 pr-[1.7rem] text-left text-sm shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 dark:border-gray-600 dark:bg-gray-900 sm:text-sm", id: "h-runtimes-filter", children: [_jsx("span", { className: "block overflow-x-hidden text-ellipsis", children: "Runtimes" }), _jsx("span", { className: "pointer-events-none absolute inset-y-0 right-0 ml-3 flex items-center pr-2 text-gray-400", children: _jsx(Icon, { icon: "heroicons:chevron-down-20-solid", "aria-hidden": "true" }) })] }) }), _jsx(Transition, { as: Fragment, leave: "transition ease-in duration-100", leaveFrom: "opacity-100", leaveTo: "opacity-0", children: _jsx(Menu.Items, { className: "absolute left-0 z-10 mt-1 min-w-[400px] overflow-auto rounded-md bg-gray-50 py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none dark:border-gray-600 dark:bg-gray-900 dark:ring-white dark:ring-opacity-20 sm:text-sm", children: _jsx("div", { className: "p-4", children: _jsx("fieldset", { children: _jsxs("div", { className: "space-y-5", children: [_jsx(RuntimeToggle, { id: "show-runtime-ruby", state: showRuntimeRuby, toggle: toggleShowRuntimeRuby, label: "Ruby", description: "Show Ruby runtime functions." }), _jsx(RuntimeToggle, { id: "show-runtime-python", state: showRuntimePython, toggle: toggleShowRuntimePython, label: "Python", description: "Show Python runtime functions." }), _jsx(RuntimeToggle, { id: "show-interpreted-only", state: showInterpretedOnly, toggle: toggleShowInterpretedOnly, label: "Interpreted Only", description: "Show only interpreted functions." })] }) }) }) }) })] })] }));
21
+ return (_jsxs("div", { children: [_jsx("label", { className: "text-sm", children: "Runtimes" }), _jsxs(Menu, { as: "div", className: "relative text-left", id: "h-runtimes-filter", children: [_jsx("div", { children: _jsxs(Menu.Button, { className: "relative w-full cursor-default rounded-md border bg-white py-2 pl-3 pr-[1.7rem] text-left text-sm shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 dark:border-gray-600 dark:bg-gray-900 sm:text-sm", children: [_jsx("span", { className: "block overflow-x-hidden text-ellipsis", children: "Runtimes" }), _jsx("span", { className: "pointer-events-none absolute inset-y-0 right-0 ml-3 flex items-center pr-2 text-gray-400", children: _jsx(Icon, { icon: "heroicons:chevron-down-20-solid", "aria-hidden": "true" }) })] }) }), _jsx(Transition, { as: Fragment, leave: "transition ease-in duration-100", leaveFrom: "opacity-100", leaveTo: "opacity-0", children: _jsx(Menu.Items, { className: "absolute left-0 z-10 mt-1 min-w-[400px] overflow-auto rounded-md bg-gray-50 py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none dark:border-gray-600 dark:bg-gray-900 dark:ring-white dark:ring-opacity-20 sm:text-sm", children: _jsx("div", { className: "p-4", children: _jsx("fieldset", { children: _jsxs("div", { className: "space-y-5", children: [_jsx(RuntimeToggle, { id: "show-runtime-ruby", state: showRuntimeRuby, toggle: toggleShowRuntimeRuby, label: "Ruby", description: "Show Ruby runtime functions." }), _jsx(RuntimeToggle, { id: "show-runtime-python", state: showRuntimePython, toggle: toggleShowRuntimePython, label: "Python", description: "Show Python runtime functions." }), _jsx(RuntimeToggle, { id: "show-interpreted-only", state: showInterpretedOnly, toggle: toggleShowInterpretedOnly, label: "Interpreted Only", description: "Show only interpreted functions." })] }) }) }) }) })] })] }));
22
22
  };
23
23
  export default RuntimeFilterDropdown;
@@ -14,7 +14,7 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
14
14
  import { Select } from '@parca/components';
15
15
  import { FIELD_CUMULATIVE, FIELD_DIFF, FIELD_FUNCTION_NAME } from '../IcicleGraphArrow';
16
16
  const SortBySelect = ({ sortBy, setSortBy, compareMode, }) => {
17
- return (_jsxs("div", { children: [_jsx("label", { className: "text-sm", children: "Sort" }), _jsx(Select, { id: "h-sort-by-filter", className: "!px-3", items: [
17
+ return (_jsxs("div", { children: [_jsx("label", { className: "text-sm", children: "Sort" }), _jsx(Select, { className: "!px-3", items: [
18
18
  {
19
19
  key: FIELD_FUNCTION_NAME,
20
20
  disabled: false,
@@ -39,6 +39,6 @@ const SortBySelect = ({ sortBy, setSortBy, compareMode, }) => {
39
39
  expanded: (_jsx(_Fragment, { children: _jsx("span", { children: "Diff" }) })),
40
40
  },
41
41
  },
42
- ], selectedKey: sortBy, onSelection: key => setSortBy(key), placeholder: 'Sort By', primary: false, disabled: false })] }));
42
+ ], selectedKey: sortBy, onSelection: key => setSortBy(key), placeholder: 'Sort By', primary: false, disabled: false, id: "h-sort-by-filter" })] }));
43
43
  };
44
44
  export default SortBySelect;
@@ -134,6 +134,6 @@ const ProfileIcicleGraph = function ProfileIcicleGraphNonMemo({ graph, arrow, to
134
134
  if (isTrimmed) {
135
135
  console.info(`Trimmed ${trimmedFormatted} (${trimmedPercentage}%) too small values.`);
136
136
  }
137
- return (_jsx(AnimatePresence, { children: _jsxs(motion.div, { className: "relative h-full w-full", initial: { opacity: 0 }, animate: { opacity: 1 }, transition: { duration: 0.5 }, children: [compareMode ? _jsx(DiffLegend, {}) : null, _jsxs("div", { className: "min-h-48", children: [graph !== undefined && (_jsx(IcicleGraph, { width: width, graph: graph, total: total, filtered: filtered, curPath: curPath, setCurPath: setNewCurPath, sampleUnit: sampleUnit, navigateTo: navigateTo })), arrow !== undefined && (_jsx(IcicleGraphArrow, { width: width, arrow: arrow, total: total, filtered: filtered, curPath: curPath, setCurPath: setNewCurPath, sampleUnit: sampleUnit, navigateTo: navigateTo, sortBy: storeSortBy }))] }), _jsxs("p", { className: "my-2 text-xs", children: ["Showing ", totalFormatted, ' ', isFiltered ? (_jsxs("span", { children: ["(", filteredPercentage, "%) filtered of ", totalUnfilteredFormatted, ' '] })) : (_jsx(_Fragment, {})), "values.", ' '] })] }, "icicle-graph-loaded") }));
137
+ return (_jsx(AnimatePresence, { children: _jsxs(motion.div, { className: "relative h-full w-full", initial: { opacity: 0 }, animate: { opacity: 1 }, transition: { duration: 0.5 }, children: [compareMode ? _jsx(DiffLegend, {}) : null, _jsxs("div", { className: "min-h-48", id: "h-icicle-graph", children: [graph !== undefined && (_jsx(IcicleGraph, { width: width, graph: graph, total: total, filtered: filtered, curPath: curPath, setCurPath: setNewCurPath, sampleUnit: sampleUnit, navigateTo: navigateTo })), arrow !== undefined && (_jsx(IcicleGraphArrow, { width: width, arrow: arrow, total: total, filtered: filtered, curPath: curPath, setCurPath: setNewCurPath, sampleUnit: sampleUnit, navigateTo: navigateTo, sortBy: storeSortBy }))] }), _jsxs("p", { className: "my-2 text-xs", children: ["Showing ", totalFormatted, ' ', isFiltered ? (_jsxs("span", { children: ["(", filteredPercentage, "%) filtered of ", totalUnfilteredFormatted, ' '] })) : (_jsx(_Fragment, {})), "values.", ' '] })] }, "icicle-graph-loaded") }));
138
138
  };
139
139
  export default ProfileIcicleGraph;
@@ -21,7 +21,7 @@ interface ProfileMetricsGraphProps {
21
21
  key: string;
22
22
  value: string;
23
23
  }>) => void;
24
- onPointClick: (timestamp: number, labels: Label[], queryExpression: string) => void;
24
+ onPointClick: (timestamp: number, labels: Label[], queryExpression: string, duration: number) => void;
25
25
  comparing?: boolean;
26
26
  }
27
27
  export interface IQueryRangeState {
@@ -88,8 +88,8 @@ const ProfileMetricsGraph = ({ queryClient, queryExpression, profile, from, to,
88
88
  return _jsx(ErrorContent, { errorMessage: capitalizeOnlyFirstLetter(error.message) });
89
89
  }
90
90
  if (dataAvailable) {
91
- const handleSampleClick = (timestamp, _value, labels) => {
92
- onPointClick(timestamp, labels, queryExpression);
91
+ const handleSampleClick = (timestamp, _value, labels, duration) => {
92
+ onPointClick(timestamp, labels, queryExpression, duration);
93
93
  };
94
94
  let sampleUnit = '';
95
95
  if (series.every((val, i, arr) => val?.sampleType?.unit === arr[0]?.sampleType?.unit)) {
@@ -15,7 +15,6 @@ import { useEffect, useState } from 'react';
15
15
  import { Button, ButtonGroup, DateTimeRange, DateTimeRangePicker, IconButton, useGrpcMetadata, useParcaContext, } from '@parca/components';
16
16
  import { CloseIcon } from '@parca/icons';
17
17
  import { Query } from '@parca/parser';
18
- import { getStepDuration, getStepDurationInMilliseconds } from '@parca/utilities';
19
18
  import { MergedProfileSelection } from '..';
20
19
  import MatchersInput from '../MatchersInput/index';
21
20
  import { useMetricsGraphDimensions } from '../MetricsGraph/useMetricsGraphDimensions';
@@ -177,7 +176,7 @@ const ProfileSelector = ({ queryClient, querySelection, selectProfile, selectQue
177
176
  timeSelection: range.getRangeKey(),
178
177
  ...mergedProfileParams,
179
178
  });
180
- }, addLabelMatcher: addLabelMatcher, onPointClick: (timestamp, labels, queryExpression) => {
179
+ }, addLabelMatcher: addLabelMatcher, onPointClick: (timestamp, labels, queryExpression, duration) => {
181
180
  // TODO: Pass the query object via click rather than queryExpression
182
181
  let query = Query.parse(queryExpression);
183
182
  labels.forEach(l => {
@@ -186,11 +185,10 @@ const ProfileSelector = ({ queryClient, querySelection, selectProfile, selectQue
186
185
  query = newQuery;
187
186
  }
188
187
  });
189
- const stepDuration = getStepDuration(querySelection.from, querySelection.to);
190
- const stepDurationInMilliseconds = getStepDurationInMilliseconds(stepDuration);
188
+ const durationInMilliseconds = duration / 1000000; // duration is in nanoseconds
191
189
  const mergeFrom = timestamp;
192
190
  const mergeTo = query.profileType().delta
193
- ? mergeFrom + stepDurationInMilliseconds
191
+ ? mergeFrom + durationInMilliseconds
194
192
  : mergeFrom;
195
193
  selectProfile(new MergedProfileSelection(mergeFrom, mergeTo, query));
196
194
  } }) })) : (_jsx(_Fragment, { children: profileSelection == null ? (_jsx("div", { className: "p-2", children: _jsx(ProfileMetricsEmptyState, { message: `Please select a profile type and click "Search" to begin.` }) })) : null })) }) })] }));
@@ -122,6 +122,6 @@ const ProfileTypeSelector = ({ profileTypesData, loading = false, error, selecte
122
122
  key: name,
123
123
  element: profileSelectElement(name, flexibleKnownProfilesDetection),
124
124
  }));
125
- return (_jsx(Select, { items: profileLabels, selectedKey: selectedKey, onSelection: onSelection, placeholder: "Select profile type...", loading: loading, className: "bg-white" }));
125
+ return (_jsx(Select, { items: profileLabels, selectedKey: selectedKey, onSelection: onSelection, placeholder: "Select profile type...", loading: loading, className: "bg-white h-profile-type-dropdown" }));
126
126
  };
127
127
  export default ProfileTypeSelector;
@@ -29,6 +29,6 @@ const FilterByFunctionButton = ({ navigateTo, }) => {
29
29
  setStoreValue(localValue);
30
30
  }
31
31
  }, [localValue, isClearAction, setStoreValue]);
32
- return (_jsx(Input, { placeholder: "Filter by function", id: "h-filter-by-function", className: "text-sm", onAction: onAction, onChange: e => setLocalValue(e.target.value), value: localValue ?? '', onBlur: () => setLocalValue(storeValue), actionIcon: isClearAction ? _jsx(Icon, { icon: "ep:circle-close" }) : _jsx(Icon, { icon: "ep:arrow-right" }) }));
32
+ return (_jsx(Input, { placeholder: "Filter by function", className: "text-sm", onAction: onAction, onChange: e => setLocalValue(e.target.value), value: localValue ?? '', onBlur: () => setLocalValue(storeValue), actionIcon: isClearAction ? _jsx(Icon, { icon: "ep:circle-close" }) : _jsx(Icon, { icon: "ep:arrow-right" }), id: "h-filter-by-function" }));
33
33
  };
34
34
  export default FilterByFunctionButton;
@@ -9,6 +9,7 @@ interface Props {
9
9
  addView?: boolean;
10
10
  disabled?: boolean;
11
11
  icon?: JSX.Element;
12
+ id?: string;
12
13
  }
13
- declare const ViewSelector: ({ defaultValue, navigateTo, position, placeholderText, primary, addView, disabled, icon, }: Props) => JSX.Element;
14
+ declare const ViewSelector: ({ defaultValue, navigateTo, position, placeholderText, primary, addView, disabled, icon, id, }: Props) => JSX.Element;
14
15
  export default ViewSelector;
@@ -13,7 +13,7 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
13
13
  // limitations under the License.
14
14
  import { Select, useParcaContext, useURLState } from '@parca/components';
15
15
  import { useUIFeatureFlag } from '@parca/hooks';
16
- const ViewSelector = ({ defaultValue, navigateTo, position, placeholderText, primary = false, addView = false, disabled = false, icon, }) => {
16
+ const ViewSelector = ({ defaultValue, navigateTo, position, placeholderText, primary = false, addView = false, disabled = false, icon, id, }) => {
17
17
  const [callgraphEnabled] = useUIFeatureFlag('callgraph');
18
18
  const [dashboardItems = ['icicle'], setDashboardItems] = useURLState({
19
19
  param: 'dashboard_items',
@@ -65,6 +65,6 @@ const ViewSelector = ({ defaultValue, navigateTo, position, placeholderText, pri
65
65
  : [dashboardItems[0], value];
66
66
  setDashboardItems(newDashboardItems);
67
67
  };
68
- return (_jsx(Select, { items: items, selectedKey: defaultValue, onSelection: onSelection, placeholder: placeholderText ?? 'Select view type...', primary: primary, disabled: disabled, icon: icon }));
68
+ return (_jsx(Select, { className: "h-view-selector", items: items, selectedKey: defaultValue, onSelection: onSelection, placeholder: placeholderText ?? 'Select view type...', primary: primary, disabled: disabled, icon: icon, id: id }));
69
69
  };
70
70
  export default ViewSelector;
@@ -20,7 +20,7 @@ import ViewSelector from './ViewSelector';
20
20
  export const VisualizationPanel = React.memo(function VisualizationPanel({ dashboardItem, index, isMultiPanelView, handleClosePanel, navigateTo, dragHandleProps, getDashboardItemByType, }) {
21
21
  const [actionButtons, setActionButtons] = useState(_jsx(_Fragment, {}));
22
22
  const { flamegraphHint } = useParcaContext();
23
- return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "flex w-full items-center justify-end gap-2 pb-2 min-h-[78px]", children: [_jsxs("div", { className: cx('flex w-full justify-between flex-col-reverse md:flex-row', isMultiPanelView && dashboardItem === 'icicle' ? 'items-end gap-x-2' : 'items-end'), children: [_jsxs("div", { className: "flex items-center", children: [_jsx("div", { className: cx(isMultiPanelView ? '' : 'hidden', 'flex items-center'), ...dragHandleProps, children: _jsx(Icon, { className: "text-xl", icon: "material-symbols:drag-indicator" }) }), _jsx("div", { className: "flex gap-2", children: actionButtons })] }), _jsxs("div", { className: cx('flex flex-row items-center gap-4', isMultiPanelView && dashboardItem === 'icicle' && 'pb-[10px]'), children: [_jsx(ViewSelector, { defaultValue: dashboardItem, navigateTo: navigateTo, position: index }), dashboardItem === 'icicle' && flamegraphHint != null ? (_jsx("div", { className: "px-2", children: flamegraphHint })) : null] })] }), isMultiPanelView && (_jsx(IconButton, { className: "py-0", onClick: () => handleClosePanel(dashboardItem), icon: _jsx(CloseIcon, {}) }))] }), getDashboardItemByType({
23
+ return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "flex w-full items-center justify-end gap-2 pb-2 min-h-[78px]", children: [_jsxs("div", { className: cx('flex w-full justify-between flex-col-reverse md:flex-row', isMultiPanelView && dashboardItem === 'icicle' ? 'items-end gap-x-2' : 'items-end'), children: [_jsxs("div", { className: "flex items-center", children: [_jsx("div", { className: cx(isMultiPanelView ? '' : 'hidden', 'flex items-center'), ...dragHandleProps, children: _jsx(Icon, { className: "text-xl", icon: "material-symbols:drag-indicator" }) }), _jsx("div", { className: "flex gap-2", children: actionButtons })] }), _jsxs("div", { className: cx('flex flex-row items-center gap-4', isMultiPanelView && dashboardItem === 'icicle' && 'pb-[10px]'), children: [_jsx(ViewSelector, { id: "h-switch-viz", defaultValue: dashboardItem, navigateTo: navigateTo, position: index }), dashboardItem === 'icicle' && flamegraphHint != null ? (_jsx("div", { className: "px-2", children: flamegraphHint })) : null] })] }), isMultiPanelView && (_jsx(IconButton, { className: "py-0", onClick: () => handleClosePanel(dashboardItem), icon: _jsx(CloseIcon, {}) }))] }), getDashboardItemByType({
24
24
  type: dashboardItem,
25
25
  isHalfScreen: isMultiPanelView,
26
26
  setActionButtons,
@@ -156,10 +156,10 @@ export const ProfileView = ({ total, filtered, flamegraphData, topTableData, cal
156
156
  'items-center': hasProfileSource,
157
157
  }), children: [_jsxs("div", { children: [hasProfileSource && (_jsxs("div", { className: "max-w-[300px]", children: [_jsx("div", { className: "text-sm font-medium capitalize", children: headerParts.length > 0 ? headerParts[0].replace(/"/g, '') : '' }), _jsx("div", { className: "text-xs", children: headerParts.length > 1
158
158
  ? headerParts[headerParts.length - 1].replace(/"/g, '')
159
- : '' })] })), profileViewExternalMainActions != null ? profileViewExternalMainActions : null] }), _jsxs("div", { className: "lg:flex flex-wrap items-center gap-2 md:justify-end hidden", children: [_jsx(FilterByFunctionButton, { navigateTo: navigateTo }), profileViewExternalSubActions != null ? profileViewExternalSubActions : null, _jsx(UserPreferences, { customButton: _jsxs(Button, { className: "gap-2", variant: "neutral", id: "h-viz-preferences", children: ["Preferences", _jsx(Icon, { icon: "pajamas:preferences", width: 20 })] }) }), profileSource !== undefined && queryClient !== undefined ? (_jsx(ProfileShareButton, { queryRequest: profileSource.QueryRequest(), queryClient: queryClient })) : null, _jsxs(Button, { id: "h-download-pprof", className: "gap-2", variant: "neutral", onClick: e => {
159
+ : '' })] })), profileViewExternalMainActions != null ? profileViewExternalMainActions : null] }), _jsxs("div", { className: "lg:flex flex-wrap items-center gap-2 md:justify-end hidden", children: [_jsx(FilterByFunctionButton, { navigateTo: navigateTo }), profileViewExternalSubActions != null ? profileViewExternalSubActions : null, _jsx(UserPreferences, { customButton: _jsxs(Button, { className: "gap-2", variant: "neutral", id: "h-viz-preferences", children: ["Preferences", _jsx(Icon, { icon: "pajamas:preferences", width: 20 })] }) }), profileSource !== undefined && queryClient !== undefined ? (_jsx(ProfileShareButton, { queryRequest: profileSource.QueryRequest(), queryClient: queryClient })) : null, _jsxs(Button, { className: "gap-2", variant: "neutral", onClick: e => {
160
160
  e.preventDefault();
161
161
  onDownloadPProf();
162
- }, disabled: pprofDownloading, children: [pprofDownloading != null && pprofDownloading ? 'Downloading...' : 'Download pprof', _jsx(Icon, { icon: "material-symbols:download", width: 20 })] }), _jsx(ViewSelector, { defaultValue: "", navigateTo: navigateTo, position: -1, placeholderText: "Add panel", icon: _jsx(Icon, { icon: "material-symbols:add", width: 20 }), addView: true, disabled: isMultiPanelView || dashboardItems.length < 1 })] })] }), _jsx("div", { className: "w-full", ref: ref, children: _jsx(DragDropContext, { onDragEnd: onDragEnd, children: _jsx(Droppable, { droppableId: "droppable", direction: "horizontal", children: provided => (_jsx("div", { ref: provided.innerRef, className: cx('grid w-full gap-2', isMultiPanelView ? 'grid-cols-2' : 'grid-cols-1'), ...provided.droppableProps, children: dashboardItems.map((dashboardItem, index) => {
162
+ }, disabled: pprofDownloading, id: "h-download-pprof", children: [pprofDownloading != null && pprofDownloading ? 'Downloading...' : 'Download pprof', _jsx(Icon, { icon: "material-symbols:download", width: 20 })] }), _jsx(ViewSelector, { defaultValue: "", navigateTo: navigateTo, position: -1, placeholderText: "Add panel", icon: _jsx(Icon, { icon: "material-symbols:add", width: 20 }), addView: true, disabled: isMultiPanelView || dashboardItems.length < 1, id: "h-add-panel" })] })] }), _jsx("div", { className: "w-full", ref: ref, children: _jsx(DragDropContext, { onDragEnd: onDragEnd, children: _jsx(Droppable, { droppableId: "droppable", direction: "horizontal", children: provided => (_jsx("div", { ref: provided.innerRef, className: cx('grid w-full gap-2', isMultiPanelView ? 'grid-cols-2' : 'grid-cols-1'), ...provided.droppableProps, children: dashboardItems.map((dashboardItem, index) => {
163
163
  return (_jsx(Draggable, { draggableId: dashboardItem, index: index, isDragDisabled: !isMultiPanelView, children: (provided, snapshot) => (_createElement("div", { ref: provided.innerRef, ...provided.draggableProps, key: dashboardItem, className: cx('w-full rounded p-2 shadow dark:border dark:border-gray-700 dark:bg-gray-700 min-h-96', snapshot.isDragging
164
164
  ? 'bg-gray-200 dark:bg-gray-500'
165
165
  : 'bg-white dark:bg-gray-700') },
@@ -45,6 +45,6 @@ const DiffLegend = () => {
45
45
  const handleMouseLeave = () => {
46
46
  setShowLegendTooltip(false);
47
47
  };
48
- return (_jsxs("div", { className: "mt-1 mb-2 hidden md:block", children: [_jsxs("div", { ref: setReferenceElement, className: "flex items-center justify-center", children: [_jsx("span", { children: "Better" }), _jsx(DiffLegendBar, { onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave }), _jsx("span", { children: "Worse" })] }), _jsx(Popover, { className: "relative", children: () => (_jsx(Transition, { show: showLegendTooltip, as: Fragment, enter: "transition ease-out duration-200", enterFrom: "opacity-0 translate-y-1", enterTo: "opacity-100 translate-y-0", leave: "transition ease-in duration-150", leaveFrom: "opacity-100 translate-y-0", leaveTo: "opacity-0 translate-y-1", children: _jsx(Popover.Panel, { ref: setPopperElement, style: styles.popper, ...attributes.popper, children: _jsx("div", { className: "overflow-hidden rounded-lg shadow-lg ring-1 ring-black ring-opacity-5", children: _jsxs("div", { className: "bg-gray-50 p-4 dark:bg-gray-800", children: [_jsx("div", { className: "flex items-center justify-center" }), _jsx("span", { className: "block text-sm text-gray-500 dark:text-gray-50", children: "This is a differential icicle graph, where a purple-colored node means unchanged, and the darker the red, the worse the node got, and the darker the green, the better the node got." })] }) }) }) })) })] }));
48
+ return (_jsxs("div", { className: "mt-1 mb-2 hidden md:block", id: "h-diff-legend", children: [_jsxs("div", { ref: setReferenceElement, className: "flex items-center justify-center", children: [_jsx("span", { children: "Better" }), _jsx(DiffLegendBar, { onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave }), _jsx("span", { children: "Worse" })] }), _jsx(Popover, { className: "relative", children: () => (_jsx(Transition, { show: showLegendTooltip, as: Fragment, enter: "transition ease-out duration-200", enterFrom: "opacity-0 translate-y-1", enterTo: "opacity-100 translate-y-0", leave: "transition ease-in duration-150", leaveFrom: "opacity-100 translate-y-0", leaveTo: "opacity-0 translate-y-1", children: _jsx(Popover.Panel, { ref: setPopperElement, style: styles.popper, ...attributes.popper, children: _jsx("div", { className: "overflow-hidden rounded-lg shadow-lg ring-1 ring-black ring-opacity-5", children: _jsxs("div", { className: "bg-gray-50 p-4 dark:bg-gray-800", children: [_jsx("div", { className: "flex items-center justify-center" }), _jsx("span", { className: "block text-sm text-gray-500 dark:text-gray-50", children: "This is a differential icicle graph, where a purple-colored node means unchanged, and the darker the red, the worse the node got, and the darker the green, the better the node got." })] }) }) }) })) })] }));
49
49
  };
50
50
  export default DiffLegend;
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@parca/profile",
3
- "version": "0.16.342",
3
+ "version": "0.16.344",
4
4
  "description": "Profile viewing libraries",
5
5
  "dependencies": {
6
6
  "@parca/client": "^0.16.103",
7
- "@parca/components": "^0.16.252",
7
+ "@parca/components": "^0.16.253",
8
8
  "@parca/dynamicsize": "^0.16.60",
9
9
  "@parca/hooks": "^0.0.41",
10
10
  "@parca/parser": "^0.16.68",
@@ -51,5 +51,5 @@
51
51
  "access": "public",
52
52
  "registry": "https://registry.npmjs.org/"
53
53
  },
54
- "gitHead": "64e853a94dc382feae3481f3f64efa47e8f5709c"
54
+ "gitHead": "419fad9b80e702bafd775521f69a812bc5e81124"
55
55
  }
@@ -40,7 +40,7 @@ interface Props {
40
40
  from: number;
41
41
  to: number;
42
42
  profile: MergedProfileSelection | null;
43
- onSampleClick: (timestamp: number, value: number, labels: Label[]) => void;
43
+ onSampleClick: (timestamp: number, value: number, labels: Label[], duration: number) => void;
44
44
  addLabelMatcher: (
45
45
  labels: {key: string; value: string} | Array<{key: string; value: string}>
46
46
  ) => void;
@@ -260,7 +260,8 @@ export const RawMetricsGraph = ({
260
260
  onSampleClick(
261
261
  Math.round(highlighted.timestamp),
262
262
  highlighted.value,
263
- sanitizeHighlightedValues(highlighted.labels) // When a user clicks on any sample in the graph, replace single `\` in the `labelValues` string with doubles `\\` if available.
263
+ sanitizeHighlightedValues(highlighted.labels), // When a user clicks on any sample in the graph, replace single `\` in the `labelValues` string with doubles `\\` if available.
264
+ highlighted.duration
264
265
  );
265
266
  }
266
267
  };
@@ -72,11 +72,8 @@ const GroupByDropdown = ({
72
72
  return (
73
73
  <div className="relative">
74
74
  <label className="text-sm">Group</label>
75
- <Menu as="div" className="relative text-left">
76
- <Menu.Button
77
- id="h-group-by-filter"
78
- className="relative w-max cursor-default rounded-md border bg-white py-2 pl-3 pr-[1.7rem] text-left text-sm shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 dark:border-gray-600 dark:bg-gray-900 sm:text-sm"
79
- >
75
+ <Menu as="div" className="relative text-left" id="h-group-by-filter">
76
+ <Menu.Button className="relative w-max cursor-default rounded-md border bg-white py-2 pl-3 pr-[1.7rem] text-left text-sm shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 dark:border-gray-600 dark:bg-gray-900 sm:text-sm">
80
77
  <span className="block overflow-x-hidden text-ellipsis">{label}</span>
81
78
  <span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2 text-gray-400">
82
79
  <Icon icon="heroicons:chevron-down-20-solid" aria-hidden="true" />
@@ -69,12 +69,9 @@ const RuntimeFilterDropdown = ({
69
69
  return (
70
70
  <div>
71
71
  <label className="text-sm">Runtimes</label>
72
- <Menu as="div" className="relative text-left">
72
+ <Menu as="div" className="relative text-left" id="h-runtimes-filter">
73
73
  <div>
74
- <Menu.Button
75
- className="relative w-full cursor-default rounded-md border bg-white py-2 pl-3 pr-[1.7rem] text-left text-sm shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 dark:border-gray-600 dark:bg-gray-900 sm:text-sm"
76
- id="h-runtimes-filter"
77
- >
74
+ <Menu.Button className="relative w-full cursor-default rounded-md border bg-white py-2 pl-3 pr-[1.7rem] text-left text-sm shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 dark:border-gray-600 dark:bg-gray-900 sm:text-sm">
78
75
  <span className="block overflow-x-hidden text-ellipsis">Runtimes</span>
79
76
  <span className="pointer-events-none absolute inset-y-0 right-0 ml-3 flex items-center pr-2 text-gray-400">
80
77
  <Icon icon="heroicons:chevron-down-20-solid" aria-hidden="true" />
@@ -28,7 +28,6 @@ const SortBySelect = ({
28
28
  <div>
29
29
  <label className="text-sm">Sort</label>
30
30
  <Select
31
- id="h-sort-by-filter"
32
31
  className="!px-3"
33
32
  items={[
34
33
  {
@@ -73,6 +72,7 @@ const SortBySelect = ({
73
72
  placeholder={'Sort By'}
74
73
  primary={false}
75
74
  disabled={false}
75
+ id="h-sort-by-filter"
76
76
  />
77
77
  </div>
78
78
  );
@@ -325,7 +325,7 @@ const ProfileIcicleGraph = function ProfileIcicleGraphNonMemo({
325
325
  transition={{duration: 0.5}}
326
326
  >
327
327
  {compareMode ? <DiffLegend /> : null}
328
- <div className="min-h-48">
328
+ <div className="min-h-48" id="h-icicle-graph">
329
329
  {graph !== undefined && (
330
330
  <IcicleGraph
331
331
  width={width}
@@ -64,7 +64,12 @@ interface ProfileMetricsGraphProps {
64
64
  addLabelMatcher: (
65
65
  labels: {key: string; value: string} | Array<{key: string; value: string}>
66
66
  ) => void;
67
- onPointClick: (timestamp: number, labels: Label[], queryExpression: string) => void;
67
+ onPointClick: (
68
+ timestamp: number,
69
+ labels: Label[],
70
+ queryExpression: string,
71
+ duration: number
72
+ ) => void;
68
73
  comparing?: boolean;
69
74
  }
70
75
 
@@ -168,8 +173,13 @@ const ProfileMetricsGraph = ({
168
173
  }
169
174
 
170
175
  if (dataAvailable) {
171
- const handleSampleClick = (timestamp: number, _value: number, labels: Label[]): void => {
172
- onPointClick(timestamp, labels, queryExpression);
176
+ const handleSampleClick = (
177
+ timestamp: number,
178
+ _value: number,
179
+ labels: Label[],
180
+ duration: number
181
+ ): void => {
182
+ onPointClick(timestamp, labels, queryExpression, duration);
173
183
  };
174
184
 
175
185
  let sampleUnit = '';
@@ -15,7 +15,7 @@ import React, {useEffect, useState} from 'react';
15
15
 
16
16
  import {RpcError} from '@protobuf-ts/runtime-rpc';
17
17
 
18
- import {ProfileTypesResponse, QueryServiceClient} from '@parca/client';
18
+ import {Label, ProfileTypesResponse, QueryServiceClient} from '@parca/client';
19
19
  import {
20
20
  Button,
21
21
  ButtonGroup,
@@ -27,7 +27,6 @@ import {
27
27
  } from '@parca/components';
28
28
  import {CloseIcon} from '@parca/icons';
29
29
  import {Query} from '@parca/parser';
30
- import {getStepDuration, getStepDurationInMilliseconds} from '@parca/utilities';
31
30
 
32
31
  import {MergedProfileSelection, ProfileSelection} from '..';
33
32
  import MatchersInput from '../MatchersInput/index';
@@ -327,7 +326,12 @@ const ProfileSelector = ({
327
326
  });
328
327
  }}
329
328
  addLabelMatcher={addLabelMatcher}
330
- onPointClick={(timestamp, labels, queryExpression) => {
329
+ onPointClick={(
330
+ timestamp: number,
331
+ labels: Label[],
332
+ queryExpression: string,
333
+ duration: number
334
+ ) => {
331
335
  // TODO: Pass the query object via click rather than queryExpression
332
336
  let query = Query.parse(queryExpression);
333
337
  labels.forEach(l => {
@@ -337,11 +341,10 @@ const ProfileSelector = ({
337
341
  }
338
342
  });
339
343
 
340
- const stepDuration = getStepDuration(querySelection.from, querySelection.to);
341
- const stepDurationInMilliseconds = getStepDurationInMilliseconds(stepDuration);
344
+ const durationInMilliseconds = duration / 1000000; // duration is in nanoseconds
342
345
  const mergeFrom = timestamp;
343
346
  const mergeTo = query.profileType().delta
344
- ? mergeFrom + stepDurationInMilliseconds
347
+ ? mergeFrom + durationInMilliseconds
345
348
  : mergeFrom;
346
349
  selectProfile(new MergedProfileSelection(mergeFrom, mergeTo, query));
347
350
  }}
@@ -177,7 +177,7 @@ const ProfileTypeSelector = ({
177
177
  onSelection={onSelection}
178
178
  placeholder="Select profile type..."
179
179
  loading={loading}
180
- className="bg-white"
180
+ className="bg-white h-profile-type-dropdown"
181
181
  />
182
182
  );
183
183
  };
@@ -42,13 +42,13 @@ const FilterByFunctionButton = ({
42
42
  return (
43
43
  <Input
44
44
  placeholder="Filter by function"
45
- id="h-filter-by-function"
46
45
  className="text-sm"
47
46
  onAction={onAction}
48
47
  onChange={e => setLocalValue(e.target.value)}
49
48
  value={localValue ?? ''}
50
49
  onBlur={() => setLocalValue(storeValue as string)}
51
50
  actionIcon={isClearAction ? <Icon icon="ep:circle-close" /> : <Icon icon="ep:arrow-right" />}
51
+ id="h-filter-by-function"
52
52
  />
53
53
  );
54
54
  };
@@ -24,6 +24,7 @@ interface Props {
24
24
  addView?: boolean;
25
25
  disabled?: boolean;
26
26
  icon?: JSX.Element;
27
+ id?: string;
27
28
  }
28
29
 
29
30
  const ViewSelector = ({
@@ -35,6 +36,7 @@ const ViewSelector = ({
35
36
  addView = false,
36
37
  disabled = false,
37
38
  icon,
39
+ id,
38
40
  }: Props): JSX.Element => {
39
41
  const [callgraphEnabled] = useUIFeatureFlag('callgraph');
40
42
  const [dashboardItems = ['icicle'], setDashboardItems] = useURLState({
@@ -110,6 +112,7 @@ const ViewSelector = ({
110
112
 
111
113
  return (
112
114
  <Select
115
+ className="h-view-selector"
113
116
  items={items}
114
117
  selectedKey={defaultValue}
115
118
  onSelection={onSelection}
@@ -117,6 +120,7 @@ const ViewSelector = ({
117
120
  primary={primary}
118
121
  disabled={disabled}
119
122
  icon={icon}
123
+ id={id}
120
124
  />
121
125
  );
122
126
  };
@@ -73,7 +73,12 @@ export const VisualizationPanel = React.memo(function VisualizationPanel({
73
73
  isMultiPanelView && dashboardItem === 'icicle' && 'pb-[10px]'
74
74
  )}
75
75
  >
76
- <ViewSelector defaultValue={dashboardItem} navigateTo={navigateTo} position={index} />
76
+ <ViewSelector
77
+ id="h-switch-viz"
78
+ defaultValue={dashboardItem}
79
+ navigateTo={navigateTo}
80
+ position={index}
81
+ />
77
82
 
78
83
  {dashboardItem === 'icicle' && flamegraphHint != null ? (
79
84
  <div className="px-2">{flamegraphHint}</div>
@@ -383,7 +383,6 @@ export const ProfileView = ({
383
383
  />
384
384
  ) : null}
385
385
  <Button
386
- id="h-download-pprof"
387
386
  className="gap-2"
388
387
  variant="neutral"
389
388
  onClick={e => {
@@ -391,6 +390,7 @@ export const ProfileView = ({
391
390
  onDownloadPProf();
392
391
  }}
393
392
  disabled={pprofDownloading}
393
+ id="h-download-pprof"
394
394
  >
395
395
  {pprofDownloading != null && pprofDownloading ? 'Downloading...' : 'Download pprof'}
396
396
  <Icon icon="material-symbols:download" width={20} />
@@ -403,6 +403,7 @@ export const ProfileView = ({
403
403
  icon={<Icon icon="material-symbols:add" width={20} />}
404
404
  addView={true}
405
405
  disabled={isMultiPanelView || dashboardItems.length < 1}
406
+ id="h-add-panel"
406
407
  />
407
408
  </div>
408
409
  </div>
@@ -74,7 +74,7 @@ const DiffLegend = (): JSX.Element => {
74
74
  };
75
75
 
76
76
  return (
77
- <div className="mt-1 mb-2 hidden md:block">
77
+ <div className="mt-1 mb-2 hidden md:block" id="h-diff-legend">
78
78
  <div ref={setReferenceElement} className="flex items-center justify-center">
79
79
  <span>Better</span>
80
80
  <DiffLegendBar onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave} />