@parca/profile 0.19.113 → 0.19.114

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 (86) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/dist/ProfileExplorer/ProfileExplorerSingle.d.ts.map +1 -1
  3. package/dist/ProfileExplorer/ProfileExplorerSingle.js +9 -3
  4. package/dist/ProfileFlameChart/SamplesStrips/SamplesGraph/index.d.ts +31 -0
  5. package/dist/ProfileFlameChart/SamplesStrips/SamplesGraph/index.d.ts.map +1 -0
  6. package/dist/{MetricsGraphStrips/AreaGraph → ProfileFlameChart/SamplesStrips/SamplesGraph}/index.js +32 -60
  7. package/dist/{MetricsGraphStrips/MetricsGraphStrips.stories.d.ts → ProfileFlameChart/SamplesStrips/SamplesStrips.stories.d.ts} +4 -3
  8. package/dist/ProfileFlameChart/SamplesStrips/SamplesStrips.stories.d.ts.map +1 -0
  9. package/dist/{MetricsGraphStrips/MetricsGraphStrips.stories.js → ProfileFlameChart/SamplesStrips/SamplesStrips.stories.js} +5 -4
  10. package/dist/{MetricsGraphStrips → ProfileFlameChart/SamplesStrips}/index.d.ts +5 -4
  11. package/dist/ProfileFlameChart/SamplesStrips/index.d.ts.map +1 -0
  12. package/dist/ProfileFlameChart/SamplesStrips/index.js +141 -0
  13. package/dist/ProfileFlameChart/index.d.ts +20 -0
  14. package/dist/ProfileFlameChart/index.d.ts.map +1 -0
  15. package/dist/ProfileFlameChart/index.js +155 -0
  16. package/dist/ProfileFlameGraph/index.d.ts.map +1 -1
  17. package/dist/ProfileFlameGraph/index.js +0 -1
  18. package/dist/ProfileMetricsGraph/hooks/useQueryRange.d.ts +2 -1
  19. package/dist/ProfileMetricsGraph/hooks/useQueryRange.d.ts.map +1 -1
  20. package/dist/ProfileMetricsGraph/hooks/useQueryRange.js +11 -21
  21. package/dist/ProfileMetricsGraph/index.d.ts.map +1 -1
  22. package/dist/ProfileMetricsGraph/index.js +13 -3
  23. package/dist/ProfileSelector/index.d.ts.map +1 -1
  24. package/dist/ProfileSelector/index.js +4 -0
  25. package/dist/ProfileView/components/ActionButtons/GroupByDropdown.d.ts +1 -0
  26. package/dist/ProfileView/components/ActionButtons/GroupByDropdown.d.ts.map +1 -1
  27. package/dist/ProfileView/components/ActionButtons/GroupByDropdown.js +2 -2
  28. package/dist/ProfileView/components/DashboardItems/index.d.ts +5 -4
  29. package/dist/ProfileView/components/DashboardItems/index.d.ts.map +1 -1
  30. package/dist/ProfileView/components/DashboardItems/index.js +4 -3
  31. package/dist/ProfileView/components/GroupByLabelsDropdown/index.d.ts +2 -1
  32. package/dist/ProfileView/components/GroupByLabelsDropdown/index.d.ts.map +1 -1
  33. package/dist/ProfileView/components/GroupByLabelsDropdown/index.js +2 -2
  34. package/dist/ProfileView/components/Toolbars/MultiLevelDropdown.js +1 -1
  35. package/dist/ProfileView/components/Toolbars/index.d.ts +2 -0
  36. package/dist/ProfileView/components/Toolbars/index.d.ts.map +1 -1
  37. package/dist/ProfileView/components/Toolbars/index.js +4 -2
  38. package/dist/ProfileView/hooks/useAutoSelectDimension.d.ts +16 -0
  39. package/dist/ProfileView/hooks/useAutoSelectDimension.d.ts.map +1 -0
  40. package/dist/ProfileView/hooks/useAutoSelectDimension.js +75 -0
  41. package/dist/ProfileView/hooks/useVisualizationState.d.ts +2 -0
  42. package/dist/ProfileView/hooks/useVisualizationState.d.ts.map +1 -1
  43. package/dist/ProfileView/hooks/useVisualizationState.js +8 -0
  44. package/dist/ProfileView/index.d.ts +1 -1
  45. package/dist/ProfileView/index.d.ts.map +1 -1
  46. package/dist/ProfileView/index.js +7 -4
  47. package/dist/ProfileView/types/visualization.d.ts +15 -3
  48. package/dist/ProfileView/types/visualization.d.ts.map +1 -1
  49. package/dist/ProfileViewWithData.d.ts +2 -1
  50. package/dist/ProfileViewWithData.d.ts.map +1 -1
  51. package/dist/ProfileViewWithData.js +41 -29
  52. package/dist/index.d.ts +1 -0
  53. package/dist/index.d.ts.map +1 -1
  54. package/dist/index.js +4 -0
  55. package/dist/styles.css +1 -1
  56. package/package.json +8 -7
  57. package/src/ProfileExplorer/ProfileExplorerSingle.tsx +14 -3
  58. package/src/{MetricsGraphStrips/AreaGraph → ProfileFlameChart/SamplesStrips/SamplesGraph}/index.tsx +77 -81
  59. package/src/{MetricsGraphStrips/MetricsGraphStrips.stories.tsx → ProfileFlameChart/SamplesStrips/SamplesStrips.stories.tsx} +7 -6
  60. package/src/ProfileFlameChart/SamplesStrips/index.tsx +301 -0
  61. package/src/ProfileFlameChart/index.tsx +305 -0
  62. package/src/ProfileFlameGraph/index.tsx +0 -1
  63. package/src/ProfileMetricsGraph/hooks/useQueryRange.ts +18 -26
  64. package/src/ProfileMetricsGraph/index.tsx +24 -2
  65. package/src/ProfileSelector/index.tsx +11 -0
  66. package/src/ProfileView/components/ActionButtons/GroupByDropdown.tsx +3 -0
  67. package/src/ProfileView/components/DashboardItems/index.tsx +19 -17
  68. package/src/ProfileView/components/GroupByLabelsDropdown/index.tsx +4 -2
  69. package/src/ProfileView/components/Toolbars/MultiLevelDropdown.tsx +1 -1
  70. package/src/ProfileView/components/Toolbars/index.tsx +18 -1
  71. package/src/ProfileView/hooks/useAutoSelectDimension.ts +90 -0
  72. package/src/ProfileView/hooks/useVisualizationState.ts +17 -0
  73. package/src/ProfileView/index.tsx +16 -2
  74. package/src/ProfileView/types/visualization.ts +17 -3
  75. package/src/ProfileViewWithData.tsx +80 -37
  76. package/src/index.tsx +4 -0
  77. package/dist/MetricsGraphStrips/AreaGraph/Tooltip.d.ts +0 -10
  78. package/dist/MetricsGraphStrips/AreaGraph/Tooltip.d.ts.map +0 -1
  79. package/dist/MetricsGraphStrips/AreaGraph/Tooltip.js +0 -44
  80. package/dist/MetricsGraphStrips/AreaGraph/index.d.ts +0 -21
  81. package/dist/MetricsGraphStrips/AreaGraph/index.d.ts.map +0 -1
  82. package/dist/MetricsGraphStrips/MetricsGraphStrips.stories.d.ts.map +0 -1
  83. package/dist/MetricsGraphStrips/index.d.ts.map +0 -1
  84. package/dist/MetricsGraphStrips/index.js +0 -70
  85. package/src/MetricsGraphStrips/AreaGraph/Tooltip.tsx +0 -83
  86. package/src/MetricsGraphStrips/index.tsx +0 -142
@@ -16,9 +16,9 @@ import {useArgs} from '@storybook/preview-api';
16
16
  // eslint-disable-next-line import/named
17
17
  import {Meta} from '@storybook/react';
18
18
 
19
- import {NumberDuo} from '../utils';
20
- import {DataPoint} from './AreaGraph';
21
- import {MetricsGraphStrips} from './index';
19
+ import {NumberDuo} from '../../utils';
20
+ import {DataPoint} from './SamplesGraph';
21
+ import {SamplesStrip} from './index';
22
22
 
23
23
  function seededRandom(seed: number): () => number {
24
24
  return () => {
@@ -39,8 +39,8 @@ for (let i = 0; i < 200; i++) {
39
39
  }
40
40
  }
41
41
  const meta: Meta = {
42
- title: 'components/MetricsGraphStrips',
43
- component: MetricsGraphStrips,
42
+ title: 'components/SamplesStrip',
43
+ component: SamplesStrip,
44
44
  };
45
45
  export default meta;
46
46
 
@@ -53,6 +53,7 @@ export const ThreeCPUStrips = {
53
53
  console.log('onSelectedTimeframe', index, bounds);
54
54
  },
55
55
  bounds: [mockData[0][0].timestamp, mockData[0][mockData[0].length - 1].timestamp],
56
+ stepMs: 100,
56
57
  },
57
58
  render: function Component(args: any): JSX.Element {
58
59
  const [, setArgs] = useArgs();
@@ -62,6 +63,6 @@ export const ThreeCPUStrips = {
62
63
  setArgs({...args, selectedTimeframe: {index, bounds}});
63
64
  };
64
65
 
65
- return <MetricsGraphStrips {...args} onSelectedTimeframe={onSelectedTimeframe} />;
66
+ return <SamplesStrip {...args} onSelectedTimeframe={onSelectedTimeframe} />;
66
67
  },
67
68
  };
@@ -0,0 +1,301 @@
1
+ // Copyright 2022 The Parca Authors
2
+ // Licensed under the Apache License, Version 2.0 (the "License");
3
+ // you may not use this file except in compliance with the License.
4
+ // You may obtain a copy of the License at
5
+ //
6
+ // http://www.apache.org/licenses/LICENSE-2.0
7
+ //
8
+ // Unless required by applicable law or agreed to in writing, software
9
+ // distributed under the License is distributed on an "AS IS" BASIS,
10
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11
+ // See the License for the specific language governing permissions and
12
+ // limitations under the License.
13
+
14
+ import {useMemo, useRef, useState} from 'react';
15
+
16
+ import {Icon} from '@iconify/react';
17
+ import cx from 'classnames';
18
+ import * as d3 from 'd3';
19
+ import isEqual from 'fast-deep-equal';
20
+ import {useIntersectionObserver} from 'usehooks-ts';
21
+
22
+ import {LabelSet} from '@parca/client';
23
+
24
+ import {TimelineGuide} from '../../TimelineGuide';
25
+ import {NumberDuo} from '../../utils';
26
+ import {DataPoint, SamplesGraph} from './SamplesGraph';
27
+
28
+ export type {DataPoint} from './SamplesGraph';
29
+
30
+ interface DragState {
31
+ stripIndex: number;
32
+ startX: number;
33
+ currentX: number;
34
+ }
35
+
36
+ interface Props {
37
+ cpus: LabelSet[];
38
+ data: DataPoint[][];
39
+ selectedTimeframe?: {
40
+ labels: LabelSet;
41
+ bounds: NumberDuo;
42
+ };
43
+ onSelectedTimeframe: (labels: LabelSet, bounds: NumberDuo | undefined) => void;
44
+ width?: number;
45
+ bounds: NumberDuo;
46
+ stepMs: number;
47
+ }
48
+
49
+ export const labelSetToString = (labelSet?: LabelSet): string => {
50
+ if (labelSet === undefined) {
51
+ return '{}';
52
+ }
53
+
54
+ let str = '{';
55
+
56
+ let isFirst = true;
57
+ for (const label of labelSet.labels) {
58
+ if (!isFirst) {
59
+ str += ', ';
60
+ } else {
61
+ isFirst = false;
62
+ }
63
+ str += `${label.name}: ${label.value}`;
64
+ }
65
+
66
+ str += '}';
67
+
68
+ return str;
69
+ };
70
+
71
+ const STRIP_HEIGHT = 24;
72
+
73
+ const getTimelineGuideHeight = (cpusCount: number, collapsedCount: number): number => {
74
+ return (STRIP_HEIGHT + 4) * (cpusCount - collapsedCount) + 20 * collapsedCount + 24 - 6;
75
+ };
76
+
77
+ const stickyPx = 0;
78
+
79
+ const SamplesGraphContainer = ({
80
+ isSelected,
81
+ isCollapsed,
82
+ cpu,
83
+ width,
84
+ onToggleCollapse,
85
+ data,
86
+ selectionBounds,
87
+ setSelectionBounds,
88
+ color,
89
+ stepMs,
90
+ onDragStart,
91
+ dragState,
92
+ stripIndex,
93
+ isAnyDragActive,
94
+ timeBounds,
95
+ }: {
96
+ isSelected: boolean;
97
+ isCollapsed: boolean;
98
+ cpu: LabelSet;
99
+ width: number | undefined;
100
+ onToggleCollapse: () => void;
101
+ data: DataPoint[];
102
+ selectionBounds: NumberDuo | undefined;
103
+ setSelectionBounds: (bounds: NumberDuo | undefined) => void;
104
+ color: (label: string) => string;
105
+ stepMs: number;
106
+ onDragStart: (stripIndex: number, startX: number) => void;
107
+ dragState: DragState | undefined;
108
+ stripIndex: number;
109
+ isAnyDragActive: boolean;
110
+ timeBounds: NumberDuo;
111
+ }): JSX.Element => {
112
+ const labelStr = labelSetToString(cpu);
113
+
114
+ const {isIntersecting, ref} = useIntersectionObserver({
115
+ rootMargin: `${stickyPx}px 0px 0px 0px`,
116
+ });
117
+
118
+ const isSticky = useMemo(() => {
119
+ return isSelected && isIntersecting;
120
+ }, [isSelected, isIntersecting]);
121
+
122
+ return (
123
+ <div
124
+ className={cx('min-h-5', {
125
+ relative: !isSelected,
126
+ 'sticky z-30 bg-white dark:bg-black bg-opacity-75': isSelected,
127
+ '!bg-opacity-100': isSticky,
128
+ })}
129
+ style={{width: width ?? 1468, top: isSelected ? stickyPx : undefined}}
130
+ key={labelStr}
131
+ ref={ref}
132
+ >
133
+ <div
134
+ className="text-xs absolute top-0 left-0 flex gap-[2px] items-center bg-white/50 dark:bg-black/50 px-1 rounded-sm cursor-pointer"
135
+ style={{
136
+ zIndex: 15,
137
+ }}
138
+ onClick={onToggleCollapse}
139
+ >
140
+ <Icon icon={isCollapsed ? 'bxs:right-arrow' : 'bxs:down-arrow'} />
141
+ {labelStr}
142
+ </div>
143
+ {!isCollapsed ? (
144
+ <SamplesGraph
145
+ data={data}
146
+ height={STRIP_HEIGHT}
147
+ width={width ?? 1468}
148
+ fill={color(labelStr)}
149
+ selectionBounds={selectionBounds}
150
+ setSelectionBounds={setSelectionBounds}
151
+ stepMs={stepMs}
152
+ onDragStart={(startX: number) => onDragStart(stripIndex, startX)}
153
+ dragState={dragState?.stripIndex === stripIndex ? dragState : undefined}
154
+ isAnyDragActive={isAnyDragActive}
155
+ timeBounds={timeBounds}
156
+ />
157
+ ) : null}
158
+ </div>
159
+ );
160
+ };
161
+
162
+ export const SamplesStrip = ({
163
+ cpus,
164
+ data,
165
+ selectedTimeframe,
166
+ onSelectedTimeframe,
167
+ width,
168
+ bounds,
169
+ stepMs,
170
+ }: Props): JSX.Element => {
171
+ const [collapsedLabels, setCollapsedLabels] = useState<Set<string>>(new Set());
172
+ const [dragState, setDragState] = useState<DragState | undefined>(undefined);
173
+ const containerRef = useRef<HTMLDivElement>(null);
174
+
175
+ const isDragging = dragState !== undefined;
176
+
177
+ // Sort cpus and data by label string for consistent ordering across reloads
178
+ const sortedItems = useMemo(() => {
179
+ const items = cpus.map((cpu, i) => ({
180
+ cpu,
181
+ data: data[i],
182
+ label: labelSetToString(cpu),
183
+ }));
184
+ return items.sort((a, b) => a.label.localeCompare(b.label));
185
+ }, [cpus, data]);
186
+
187
+ // Deterministic color: hash the label string so the same label always gets the same color
188
+ // regardless of render order.
189
+ const color = useMemo(() => {
190
+ const palette = d3.schemeObservable10;
191
+ const hashStr = (s: string): number => {
192
+ let h = 0;
193
+ for (let i = 0; i < s.length; i++) {
194
+ h = (Math.imul(31, h) + s.charCodeAt(i)) | 0;
195
+ }
196
+ return Math.abs(h);
197
+ };
198
+ return (label: string): string => palette[hashStr(label) % palette.length];
199
+ }, []);
200
+
201
+ const handleDragStart = (stripIndex: number, startX: number): void => {
202
+ setDragState({stripIndex, startX, currentX: startX});
203
+ };
204
+
205
+ const handleMouseMove = (e: React.MouseEvent): void => {
206
+ if (dragState === undefined || containerRef.current === null) return;
207
+
208
+ const rect = containerRef.current.getBoundingClientRect();
209
+ const x = e.clientX - rect.left;
210
+ // Clamp to container bounds
211
+ const clampedX = Math.max(0, Math.min(x, width ?? rect.width));
212
+ setDragState({...dragState, currentX: clampedX});
213
+ };
214
+
215
+ const handleMouseUp = (e: React.MouseEvent): void => {
216
+ if (dragState === undefined || containerRef.current === null) return;
217
+
218
+ const rect = containerRef.current.getBoundingClientRect();
219
+ const x = e.clientX - rect.left;
220
+ const clampedX = Math.max(0, Math.min(x, width ?? rect.width));
221
+
222
+ const {stripIndex, startX} = dragState;
223
+ if (startX !== clampedX) {
224
+ const start = Math.min(startX, clampedX);
225
+ const end = Math.max(startX, clampedX);
226
+ // Convert pixel positions to timestamps
227
+ const innerWidth = width ?? rect.width;
228
+ const startTs = bounds[0] + (start / innerWidth) * (bounds[1] - bounds[0]);
229
+ const endTs = bounds[0] + (end / innerWidth) * (bounds[1] - bounds[0]);
230
+ // Use sortedItems to get the correct cpu for the strip index
231
+ onSelectedTimeframe(sortedItems[stripIndex].cpu, [startTs, endTs]);
232
+ }
233
+
234
+ setDragState(undefined);
235
+ };
236
+
237
+ const handleMouseLeave = (): void => {
238
+ setDragState(undefined);
239
+ };
240
+
241
+ if (data.length === 0) {
242
+ return (
243
+ <span className="flex justify-center my-10">
244
+ There is no data matching your filter criteria, please try changing the filter.
245
+ </span>
246
+ );
247
+ }
248
+
249
+ return (
250
+ <div
251
+ ref={containerRef}
252
+ className={cx('flex flex-col gap-1 relative my-0', {'cursor-ew-resize': isDragging})}
253
+ style={{width: width ?? '100%'}}
254
+ onMouseMove={handleMouseMove}
255
+ onMouseUp={handleMouseUp}
256
+ onMouseLeave={handleMouseLeave}
257
+ >
258
+ <TimelineGuide
259
+ bounds={[BigInt(0), BigInt(bounds[1] - bounds[0])]}
260
+ width={width ?? 1468}
261
+ height={getTimelineGuideHeight(sortedItems.length, collapsedLabels.size)}
262
+ margin={1}
263
+ />
264
+ {sortedItems.map((item, i) => {
265
+ const isCollapsed = collapsedLabels.has(item.label);
266
+ const isSelected = isEqual(item.cpu, selectedTimeframe?.labels);
267
+
268
+ return (
269
+ <SamplesGraphContainer
270
+ isSelected={isSelected}
271
+ isCollapsed={isCollapsed}
272
+ cpu={item.cpu}
273
+ width={width}
274
+ data={item.data}
275
+ onToggleCollapse={() => {
276
+ const newCollapsedLabels = new Set(collapsedLabels);
277
+ if (collapsedLabels.has(item.label)) {
278
+ newCollapsedLabels.delete(item.label);
279
+ } else {
280
+ newCollapsedLabels.add(item.label);
281
+ }
282
+ setCollapsedLabels(newCollapsedLabels);
283
+ }}
284
+ selectionBounds={isSelected ? selectedTimeframe?.bounds : undefined}
285
+ setSelectionBounds={newBounds => {
286
+ onSelectedTimeframe(item.cpu, newBounds);
287
+ }}
288
+ color={color}
289
+ stepMs={stepMs}
290
+ onDragStart={handleDragStart}
291
+ dragState={dragState}
292
+ stripIndex={i}
293
+ isAnyDragActive={isDragging}
294
+ timeBounds={bounds}
295
+ key={item.label}
296
+ />
297
+ );
298
+ })}
299
+ </div>
300
+ );
301
+ };
@@ -0,0 +1,305 @@
1
+ // Copyright 2022 The Parca Authors
2
+ // Licensed under the Apache License, Version 2.0 (the "License");
3
+ // you may not use this file except in compliance with the License.
4
+ // You may obtain a copy of the License at
5
+ //
6
+ // http://www.apache.org/licenses/LICENSE-2.0
7
+ //
8
+ // Unless required by applicable law or agreed to in writing, software
9
+ // distributed under the License is distributed on an "AS IS" BASIS,
10
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11
+ // See the License for the specific language governing permissions and
12
+ // limitations under the License.
13
+
14
+ import {useEffect, useMemo, useRef} from 'react';
15
+
16
+ import {LabelSet, QueryRequest_ReportType, QueryServiceClient} from '@parca/client';
17
+ import {
18
+ Button,
19
+ useParcaContext,
20
+ useURLState,
21
+ useURLStateCustom,
22
+ type OptionsCustom,
23
+ } from '@parca/components';
24
+ import {Matcher, MatcherTypes, ProfileType, Query} from '@parca/parser';
25
+
26
+ import ProfileFlameGraph, {validateFlameChartQuery} from '../ProfileFlameGraph';
27
+ import {boundsFromProfileSource} from '../ProfileFlameGraph/FlameGraphArrow/utils';
28
+ import {MergedProfileSource, ProfileSource} from '../ProfileSource';
29
+ import type {SamplesData} from '../ProfileView/types/visualization';
30
+ import {useQuery} from '../useQuery';
31
+ import {NumberDuo} from '../utils';
32
+ import {SamplesStrip} from './SamplesStrips';
33
+
34
+ interface SelectedTimeframe {
35
+ labels: LabelSet;
36
+ bounds: NumberDuo;
37
+ }
38
+
39
+ const TimeframeStateSerializer: OptionsCustom<SelectedTimeframe | undefined> = {
40
+ parse: (value: string | string[] | undefined) => {
41
+ if (value == null || value === '' || value === 'undefined' || Array.isArray(value)) {
42
+ return undefined;
43
+ }
44
+ try {
45
+ const [labelPart, boundsPart] = value.split('|');
46
+ if (labelPart != null && boundsPart != null) {
47
+ const labels = labelPart.split(',').map(labelStr => {
48
+ const [name, ...rest] = labelStr.split(':');
49
+ return {name, value: rest.join(':')};
50
+ });
51
+ const [startMs, endMs] = boundsPart.split(',').map(Number);
52
+ if (labels.length > 0 && !isNaN(startMs) && !isNaN(endMs)) {
53
+ return {
54
+ labels: {labels},
55
+ bounds: [startMs, endMs] as NumberDuo,
56
+ };
57
+ }
58
+ }
59
+ } catch {
60
+ // Ignore parsing errors
61
+ }
62
+ return undefined;
63
+ },
64
+ stringify: (value: SelectedTimeframe | undefined) => {
65
+ if (value == null) {
66
+ return '';
67
+ }
68
+ const labelsStr = value.labels.labels.map(l => `${l.name}:${l.value}`).join(',');
69
+ return `${labelsStr}|${value.bounds[0]},${value.bounds[1]}`;
70
+ },
71
+ };
72
+
73
+ interface ProfileFlameChartProps {
74
+ samplesData?: SamplesData;
75
+ queryClient: QueryServiceClient;
76
+ profileSource: ProfileSource;
77
+ width: number;
78
+ total: bigint;
79
+ filtered: bigint;
80
+ profileType?: ProfileType;
81
+ isHalfScreen: boolean;
82
+ metadataMappingFiles?: string[];
83
+ metadataLoading?: boolean;
84
+ onSwitchToOneMinute?: () => void;
85
+ }
86
+
87
+ // Helper to create a filtered profile source with narrowed time bounds
88
+ // and dimension label matchers from the selected strip.
89
+ const createFilteredProfileSource = (
90
+ profileSource: ProfileSource,
91
+ selectedTimeframe: {labels: LabelSet; bounds: NumberDuo}
92
+ ): ProfileSource | null => {
93
+ if (!(profileSource instanceof MergedProfileSource)) {
94
+ return null;
95
+ }
96
+
97
+ // The bounds are in milliseconds, convert to nanoseconds for the profile source
98
+ // Round to integers since BigInt requires integer values
99
+ const mergeFrom = BigInt(Math.round(selectedTimeframe.bounds[0])) * 1_000_000n;
100
+ const mergeTo = BigInt(Math.round(selectedTimeframe.bounds[1])) * 1_000_000n;
101
+
102
+ // Add dimension labels as additional matchers to the query
103
+ const dimensionMatchers = selectedTimeframe.labels.labels.map(
104
+ l => new Matcher(l.name, MatcherTypes.MatchEqual, l.value)
105
+ );
106
+
107
+ const query = new Query(
108
+ profileSource.query.profType,
109
+ [...profileSource.query.matchers, ...dimensionMatchers],
110
+ ''
111
+ );
112
+
113
+ return new MergedProfileSource(mergeFrom, mergeTo, query);
114
+ };
115
+
116
+ export const ProfileFlameChart = ({
117
+ samplesData,
118
+ queryClient,
119
+ profileSource,
120
+ width,
121
+ total,
122
+ filtered,
123
+ profileType,
124
+ isHalfScreen,
125
+ metadataMappingFiles,
126
+ metadataLoading,
127
+ onSwitchToOneMinute,
128
+ }: ProfileFlameChartProps): JSX.Element => {
129
+ const {loader} = useParcaContext();
130
+
131
+ const [selectedTimeframe, setSelectedTimeframe] = useURLStateCustom<
132
+ SelectedTimeframe | undefined
133
+ >('flamechart_timeframe', TimeframeStateSerializer);
134
+
135
+ // Read flamechart dimension from URL state to detect changes
136
+ const [flamechartDimension] = useURLState<string[]>('flamechart_dimension', {
137
+ alwaysReturnArray: true,
138
+ });
139
+
140
+ // Reset selection when the parent time range (profileSource) changes
141
+ const timeBoundsKey = boundsFromProfileSource(profileSource).join(',');
142
+ const prevTimeBoundsKey = useRef(timeBoundsKey);
143
+ useEffect(() => {
144
+ if (prevTimeBoundsKey.current !== timeBoundsKey) {
145
+ prevTimeBoundsKey.current = timeBoundsKey;
146
+ setSelectedTimeframe(undefined);
147
+ }
148
+ }, [timeBoundsKey, setSelectedTimeframe]);
149
+
150
+ // Reset selection when the dimension changes
151
+ const dimensionKey = (flamechartDimension ?? []).join(',');
152
+ const prevDimensionKey = useRef(dimensionKey);
153
+ useEffect(() => {
154
+ if (prevDimensionKey.current !== dimensionKey) {
155
+ prevDimensionKey.current = dimensionKey;
156
+ setSelectedTimeframe(undefined);
157
+ }
158
+ }, [dimensionKey, setSelectedTimeframe]);
159
+
160
+ // Handle timeframe selection from strips
161
+ const handleSelectedTimeframe = (labels: LabelSet, bounds: NumberDuo | undefined): void => {
162
+ if (bounds === undefined) {
163
+ setSelectedTimeframe(undefined);
164
+ } else {
165
+ setSelectedTimeframe({labels, bounds});
166
+ }
167
+ };
168
+
169
+ // Create filtered profile source when selection exists
170
+ const filteredProfileSource = useMemo(() => {
171
+ if (selectedTimeframe == null) return null;
172
+ return createFilteredProfileSource(profileSource, selectedTimeframe);
173
+ }, [profileSource, selectedTimeframe]);
174
+
175
+ // Query flamechart data only when a strip selection exists
176
+ const {
177
+ isLoading: flamechartLoading,
178
+ response: flamechartResponse,
179
+ error: flamechartError,
180
+ } = useQuery(
181
+ queryClient,
182
+ filteredProfileSource ?? profileSource,
183
+ QueryRequest_ReportType.FLAMECHART,
184
+ {
185
+ skip: selectedTimeframe == null || filteredProfileSource == null,
186
+ }
187
+ );
188
+
189
+ const flamechartArrow =
190
+ flamechartResponse?.report.oneofKind === 'flamegraphArrow'
191
+ ? flamechartResponse.report.flamegraphArrow
192
+ : undefined;
193
+ const flamechartTotal = flamechartResponse != null ? BigInt(flamechartResponse.total) : total;
194
+ const flamechartFiltered =
195
+ flamechartResponse != null ? BigInt(flamechartResponse.filtered) : filtered;
196
+
197
+ // Get time bounds from profile source for the strips
198
+ const timeBounds = boundsFromProfileSource(profileSource);
199
+
200
+ // Transform samples data for SamplesStrip component
201
+ const stripsData = useMemo(() => {
202
+ if (samplesData?.series == null) return {cpus: [], data: [], stepMs: 0};
203
+
204
+ const cpus = samplesData.series.map(s => s.labelset);
205
+ const data = samplesData.series.map(s => s.data);
206
+
207
+ const stepMs = samplesData.stepMs ?? 0;
208
+
209
+ return {cpus, data, stepMs};
210
+ }, [samplesData?.series, samplesData?.stepMs]);
211
+
212
+ const {isValid, isNonDelta, isDurationTooLong} = validateFlameChartQuery(
213
+ profileSource as MergedProfileSource
214
+ );
215
+
216
+ if (!isValid) {
217
+ if (isDurationTooLong) {
218
+ return (
219
+ <div className="flex flex-col justify-center items-center p-10 text-center gap-4 text-sm">
220
+ <span>
221
+ Flame chart is unavailable for queries longer than one minute. Try reducing the time
222
+ range to one minute or selecting a point in the metrics graph.
223
+ </span>
224
+ {onSwitchToOneMinute != null && (
225
+ <Button variant="primary" onClick={onSwitchToOneMinute}>
226
+ Switch to last 1 minute
227
+ </Button>
228
+ )}
229
+ </div>
230
+ );
231
+ }
232
+
233
+ const message = isNonDelta
234
+ ? 'To use the Flame chart, please switch to a Delta profile.'
235
+ : 'Flame chart is unavailable for this query.';
236
+ return (
237
+ <div className="flex flex-col justify-center p-10 text-center gap-6 text-sm">{message}</div>
238
+ );
239
+ }
240
+
241
+ const hasDimension = (flamechartDimension ?? []).length > 0;
242
+
243
+ // Show loader while metadata labels are loading (needed for dimension auto-selection)
244
+ if (metadataLoading === true) {
245
+ return <>{loader}</>;
246
+ }
247
+
248
+ if (!hasDimension) {
249
+ return (
250
+ <div className="flex justify-center items-center py-10 text-gray-500 dark:text-gray-400 text-sm">
251
+ Select a label in the &quot;Samples group by&quot; dropdown above to view the samples
252
+ strips.
253
+ </div>
254
+ );
255
+ }
256
+
257
+ if (samplesData?.loading === true) {
258
+ return <>{loader}</>;
259
+ }
260
+
261
+ return (
262
+ <div>
263
+ {/* Samples Strips - rendered above flamechart */}
264
+ {stripsData.cpus.length > 0 && stripsData.data.length > 0 && (
265
+ <div className="mb-2">
266
+ <SamplesStrip
267
+ cpus={stripsData.cpus}
268
+ data={stripsData.data}
269
+ selectedTimeframe={selectedTimeframe}
270
+ onSelectedTimeframe={handleSelectedTimeframe}
271
+ width={width}
272
+ bounds={[Number(timeBounds[0] / 1_000_000n), Number(timeBounds[1] / 1_000_000n)]}
273
+ stepMs={stripsData.stepMs}
274
+ />
275
+ </div>
276
+ )}
277
+
278
+ {/* Flamegraph visualization - only shown when a time range is selected in the strips */}
279
+ {selectedTimeframe != null && filteredProfileSource != null ? (
280
+ <ProfileFlameGraph
281
+ arrow={flamechartArrow}
282
+ loading={flamechartLoading}
283
+ error={flamechartError}
284
+ profileSource={filteredProfileSource}
285
+ width={width}
286
+ total={flamechartTotal}
287
+ filtered={flamechartFiltered}
288
+ profileType={profileType}
289
+ isHalfScreen={isHalfScreen}
290
+ metadataMappingFiles={metadataMappingFiles}
291
+ metadataLoading={metadataLoading}
292
+ isFlameChart={true}
293
+ curPathArrow={[]}
294
+ setNewCurPathArrow={() => {}}
295
+ />
296
+ ) : (
297
+ <div className="flex justify-center items-center py-10 text-gray-500 dark:text-gray-400 text-sm">
298
+ Select a time range in the samples strips above to view the flamechart.
299
+ </div>
300
+ )}
301
+ </div>
302
+ );
303
+ };
304
+
305
+ export default ProfileFlameChart;
@@ -77,7 +77,6 @@ export const validateFlameChartQuery = (
77
77
  ): {isValid: boolean; isNonDelta: boolean; isDurationTooLong: boolean} => {
78
78
  const isNonDelta = !profileSource.ProfileType().delta;
79
79
  const duration = profileSource.mergeTo - profileSource.mergeFrom;
80
- console.log('duration of flame chart query: ', duration, 'ns');
81
80
  const isDurationTooLong = duration > 60_000_000_000n; // 60 seconds in nanoseconds
82
81
  return {isValid: !isNonDelta && !isDurationTooLong, isNonDelta, isDurationTooLong};
83
82
  };