@parca/profile 0.19.113 → 0.19.115
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/dist/ProfileExplorer/ProfileExplorerSingle.d.ts.map +1 -1
- package/dist/ProfileExplorer/ProfileExplorerSingle.js +9 -3
- package/dist/ProfileFlameChart/SamplesStrips/SamplesGraph/index.d.ts +31 -0
- package/dist/ProfileFlameChart/SamplesStrips/SamplesGraph/index.d.ts.map +1 -0
- package/dist/{MetricsGraphStrips/AreaGraph → ProfileFlameChart/SamplesStrips/SamplesGraph}/index.js +32 -60
- package/dist/{MetricsGraphStrips/MetricsGraphStrips.stories.d.ts → ProfileFlameChart/SamplesStrips/SamplesStrips.stories.d.ts} +4 -3
- package/dist/ProfileFlameChart/SamplesStrips/SamplesStrips.stories.d.ts.map +1 -0
- package/dist/{MetricsGraphStrips/MetricsGraphStrips.stories.js → ProfileFlameChart/SamplesStrips/SamplesStrips.stories.js} +5 -4
- package/dist/{MetricsGraphStrips → ProfileFlameChart/SamplesStrips}/index.d.ts +5 -4
- package/dist/ProfileFlameChart/SamplesStrips/index.d.ts.map +1 -0
- package/dist/ProfileFlameChart/SamplesStrips/index.js +145 -0
- package/dist/ProfileFlameChart/index.d.ts +20 -0
- package/dist/ProfileFlameChart/index.d.ts.map +1 -0
- package/dist/ProfileFlameChart/index.js +155 -0
- package/dist/ProfileFlameGraph/index.d.ts.map +1 -1
- package/dist/ProfileFlameGraph/index.js +0 -1
- package/dist/ProfileMetricsGraph/hooks/useQueryRange.d.ts +2 -1
- package/dist/ProfileMetricsGraph/hooks/useQueryRange.d.ts.map +1 -1
- package/dist/ProfileMetricsGraph/hooks/useQueryRange.js +11 -21
- package/dist/ProfileMetricsGraph/index.d.ts.map +1 -1
- package/dist/ProfileMetricsGraph/index.js +13 -3
- package/dist/ProfileSelector/index.d.ts.map +1 -1
- package/dist/ProfileSelector/index.js +4 -0
- package/dist/ProfileView/components/ActionButtons/GroupByDropdown.d.ts +1 -0
- package/dist/ProfileView/components/ActionButtons/GroupByDropdown.d.ts.map +1 -1
- package/dist/ProfileView/components/ActionButtons/GroupByDropdown.js +2 -2
- package/dist/ProfileView/components/DashboardItems/index.d.ts +5 -4
- package/dist/ProfileView/components/DashboardItems/index.d.ts.map +1 -1
- package/dist/ProfileView/components/DashboardItems/index.js +4 -3
- package/dist/ProfileView/components/GroupByLabelsDropdown/index.d.ts +2 -1
- package/dist/ProfileView/components/GroupByLabelsDropdown/index.d.ts.map +1 -1
- package/dist/ProfileView/components/GroupByLabelsDropdown/index.js +2 -2
- package/dist/ProfileView/components/Toolbars/MultiLevelDropdown.js +1 -1
- package/dist/ProfileView/components/Toolbars/index.d.ts +2 -0
- package/dist/ProfileView/components/Toolbars/index.d.ts.map +1 -1
- package/dist/ProfileView/components/Toolbars/index.js +4 -2
- package/dist/ProfileView/hooks/useAutoSelectDimension.d.ts +16 -0
- package/dist/ProfileView/hooks/useAutoSelectDimension.d.ts.map +1 -0
- package/dist/ProfileView/hooks/useAutoSelectDimension.js +75 -0
- package/dist/ProfileView/hooks/useVisualizationState.d.ts +2 -0
- package/dist/ProfileView/hooks/useVisualizationState.d.ts.map +1 -1
- package/dist/ProfileView/hooks/useVisualizationState.js +8 -0
- package/dist/ProfileView/index.d.ts +1 -1
- package/dist/ProfileView/index.d.ts.map +1 -1
- package/dist/ProfileView/index.js +7 -4
- package/dist/ProfileView/types/visualization.d.ts +15 -3
- package/dist/ProfileView/types/visualization.d.ts.map +1 -1
- package/dist/ProfileViewWithData.d.ts +2 -1
- package/dist/ProfileViewWithData.d.ts.map +1 -1
- package/dist/ProfileViewWithData.js +41 -29
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/styles.css +1 -1
- package/package.json +8 -7
- package/src/ProfileExplorer/ProfileExplorerSingle.tsx +14 -3
- package/src/{MetricsGraphStrips/AreaGraph → ProfileFlameChart/SamplesStrips/SamplesGraph}/index.tsx +77 -81
- package/src/{MetricsGraphStrips/MetricsGraphStrips.stories.tsx → ProfileFlameChart/SamplesStrips/SamplesStrips.stories.tsx} +7 -6
- package/src/ProfileFlameChart/SamplesStrips/index.tsx +317 -0
- package/src/ProfileFlameChart/index.tsx +305 -0
- package/src/ProfileFlameGraph/index.tsx +0 -1
- package/src/ProfileMetricsGraph/hooks/useQueryRange.ts +18 -26
- package/src/ProfileMetricsGraph/index.tsx +24 -2
- package/src/ProfileSelector/index.tsx +11 -0
- package/src/ProfileView/components/ActionButtons/GroupByDropdown.tsx +3 -0
- package/src/ProfileView/components/DashboardItems/index.tsx +19 -17
- package/src/ProfileView/components/GroupByLabelsDropdown/index.tsx +4 -2
- package/src/ProfileView/components/Toolbars/MultiLevelDropdown.tsx +1 -1
- package/src/ProfileView/components/Toolbars/index.tsx +18 -1
- package/src/ProfileView/hooks/useAutoSelectDimension.ts +90 -0
- package/src/ProfileView/hooks/useVisualizationState.ts +17 -0
- package/src/ProfileView/index.tsx +16 -2
- package/src/ProfileView/types/visualization.ts +17 -3
- package/src/ProfileViewWithData.tsx +80 -37
- package/src/index.tsx +4 -0
- package/dist/MetricsGraphStrips/AreaGraph/Tooltip.d.ts +0 -10
- package/dist/MetricsGraphStrips/AreaGraph/Tooltip.d.ts.map +0 -1
- package/dist/MetricsGraphStrips/AreaGraph/Tooltip.js +0 -44
- package/dist/MetricsGraphStrips/AreaGraph/index.d.ts +0 -21
- package/dist/MetricsGraphStrips/AreaGraph/index.d.ts.map +0 -1
- package/dist/MetricsGraphStrips/MetricsGraphStrips.stories.d.ts.map +0 -1
- package/dist/MetricsGraphStrips/index.d.ts.map +0 -1
- package/dist/MetricsGraphStrips/index.js +0 -70
- package/src/MetricsGraphStrips/AreaGraph/Tooltip.tsx +0 -83
- package/src/MetricsGraphStrips/index.tsx +0 -142
package/src/{MetricsGraphStrips/AreaGraph → ProfileFlameChart/SamplesStrips/SamplesGraph}/index.tsx
RENAMED
|
@@ -17,12 +17,18 @@ import {Icon} from '@iconify/react';
|
|
|
17
17
|
import cx from 'classnames';
|
|
18
18
|
import * as d3 from 'd3';
|
|
19
19
|
|
|
20
|
-
import {NumberDuo} from '
|
|
21
|
-
import {Tooltip} from './Tooltip';
|
|
20
|
+
import {NumberDuo} from '../../../utils';
|
|
22
21
|
|
|
23
22
|
export interface DataPoint {
|
|
24
23
|
timestamp: number;
|
|
25
24
|
value: number;
|
|
25
|
+
sampleCount?: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface DragState {
|
|
29
|
+
stripIndex: number;
|
|
30
|
+
startX: number;
|
|
31
|
+
currentX: number;
|
|
26
32
|
}
|
|
27
33
|
|
|
28
34
|
interface Props {
|
|
@@ -36,7 +42,11 @@ interface Props {
|
|
|
36
42
|
data: DataPoint[];
|
|
37
43
|
selectionBounds?: NumberDuo | undefined;
|
|
38
44
|
setSelectionBounds: (newBounds: NumberDuo | undefined) => void;
|
|
39
|
-
|
|
45
|
+
stepMs: number;
|
|
46
|
+
onDragStart?: (startX: number) => void;
|
|
47
|
+
dragState?: DragState;
|
|
48
|
+
isAnyDragActive?: boolean;
|
|
49
|
+
timeBounds?: NumberDuo;
|
|
40
50
|
}
|
|
41
51
|
|
|
42
52
|
const DraggingWindow = ({
|
|
@@ -189,7 +199,7 @@ const ZoomWindow = ({
|
|
|
189
199
|
);
|
|
190
200
|
};
|
|
191
201
|
|
|
192
|
-
export const
|
|
202
|
+
export const SamplesGraph = ({
|
|
193
203
|
data,
|
|
194
204
|
height,
|
|
195
205
|
width,
|
|
@@ -200,29 +210,30 @@ export const AreaGraph = ({
|
|
|
200
210
|
fill = 'gray',
|
|
201
211
|
selectionBounds,
|
|
202
212
|
setSelectionBounds,
|
|
203
|
-
|
|
213
|
+
stepMs,
|
|
214
|
+
onDragStart,
|
|
215
|
+
dragState,
|
|
216
|
+
isAnyDragActive = false,
|
|
217
|
+
timeBounds,
|
|
204
218
|
}: Props): JSX.Element => {
|
|
205
219
|
const [mousePosition, setMousePosition] = useState<NumberDuo | undefined>(undefined);
|
|
206
|
-
const [dragStart, setDragStart] = useState<number | undefined>(undefined);
|
|
207
220
|
const [isHoveringDragHandle, setIsHoveringDragHandle] = useState(false);
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
//
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
.
|
|
222
|
-
.
|
|
223
|
-
.
|
|
224
|
-
.y0(y(0))
|
|
225
|
-
.y1(d => y(d.value));
|
|
221
|
+
|
|
222
|
+
// use the bounds from props if provided, else compute from data
|
|
223
|
+
const xDomain = timeBounds ?? (d3.extent(data, d => d.timestamp) as NumberDuo);
|
|
224
|
+
const x = d3.scaleUtc(xDomain, [marginLeft, width - marginRight]);
|
|
225
|
+
|
|
226
|
+
// Calculate sample count range for opacity scaling
|
|
227
|
+
const sampleCounts = data.map(d => Number(d.sampleCount ?? 1));
|
|
228
|
+
const maxSampleCount = Math.max(...sampleCounts);
|
|
229
|
+
const minSampleCount = Math.min(...sampleCounts);
|
|
230
|
+
|
|
231
|
+
// Create opacity scale: more samples = higher opacity
|
|
232
|
+
const opacityScale = d3
|
|
233
|
+
.scaleLinear()
|
|
234
|
+
.domain([minSampleCount, maxSampleCount])
|
|
235
|
+
.range([0.5, 1.0])
|
|
236
|
+
.clamp(true);
|
|
226
237
|
|
|
227
238
|
const zoomWindow: NumberDuo | undefined = useMemo(() => {
|
|
228
239
|
if (selectionBounds === undefined) {
|
|
@@ -240,7 +251,11 @@ export const AreaGraph = ({
|
|
|
240
251
|
return (
|
|
241
252
|
<div
|
|
242
253
|
style={{height, width}}
|
|
254
|
+
className="relative"
|
|
243
255
|
onMouseMove={e => {
|
|
256
|
+
// Only track hover position when no drag is active anywhere
|
|
257
|
+
if (isAnyDragActive) return;
|
|
258
|
+
|
|
244
259
|
const [xPos, yPos] = d3.pointer(e);
|
|
245
260
|
|
|
246
261
|
if (
|
|
@@ -250,30 +265,13 @@ export const AreaGraph = ({
|
|
|
250
265
|
yPos <= height - marginBottom
|
|
251
266
|
) {
|
|
252
267
|
setMousePosition([xPos, yPos]);
|
|
253
|
-
|
|
254
|
-
// Find the closest data point
|
|
255
|
-
if (!isHoveringDragHandle && !isDragging) {
|
|
256
|
-
const xDate = x.invert(xPos);
|
|
257
|
-
const bisect = d3.bisector((d: DataPoint) => d.timestamp).left;
|
|
258
|
-
const index = bisect(data, xDate.getTime());
|
|
259
|
-
const dataPoint = data[index];
|
|
260
|
-
if (dataPoint !== undefined) {
|
|
261
|
-
setHoverData(dataPoint);
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
268
|
} else {
|
|
265
269
|
setMousePosition(undefined);
|
|
266
|
-
setHoverData(null);
|
|
267
270
|
}
|
|
268
271
|
}}
|
|
269
|
-
onMouseEnter={() => {
|
|
270
|
-
setIsMouseOverGraph(true);
|
|
271
|
-
}}
|
|
272
272
|
onMouseLeave={() => {
|
|
273
|
-
|
|
273
|
+
// Only clear hover position, drag is managed by parent
|
|
274
274
|
setMousePosition(undefined);
|
|
275
|
-
setDragStart(undefined);
|
|
276
|
-
setHoverData(null);
|
|
277
275
|
}}
|
|
278
276
|
onMouseDown={e => {
|
|
279
277
|
// only left mouse button
|
|
@@ -281,45 +279,28 @@ export const AreaGraph = ({
|
|
|
281
279
|
return;
|
|
282
280
|
}
|
|
283
281
|
|
|
284
|
-
// X/Y coordinate array relative to
|
|
282
|
+
// X/Y coordinate array relative to element
|
|
285
283
|
const rel = d3.pointer(e);
|
|
286
|
-
|
|
287
284
|
const xCoordinate = rel[0];
|
|
288
|
-
|
|
289
|
-
if (
|
|
290
|
-
|
|
285
|
+
|
|
286
|
+
if (xCoordinate >= 0 && onDragStart !== undefined) {
|
|
287
|
+
onDragStart(xCoordinate);
|
|
291
288
|
}
|
|
292
289
|
|
|
293
290
|
e.stopPropagation();
|
|
294
291
|
e.preventDefault();
|
|
295
292
|
}}
|
|
296
|
-
onMouseUp={e => {
|
|
297
|
-
if (dragStart === undefined) {
|
|
298
|
-
return;
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
const rel = d3.pointer(e);
|
|
302
|
-
const xCoordinate = rel[0];
|
|
303
|
-
const xCoordinateWithoutMargin = xCoordinate - marginLeft;
|
|
304
|
-
if (xCoordinateWithoutMargin >= 0 && dragStart !== xCoordinateWithoutMargin) {
|
|
305
|
-
const start = Math.min(dragStart, xCoordinateWithoutMargin);
|
|
306
|
-
const end = Math.max(dragStart, xCoordinateWithoutMargin);
|
|
307
|
-
setSelectionBoundsWithScaling([start, end]);
|
|
308
|
-
}
|
|
309
|
-
setDragStart(undefined);
|
|
310
|
-
}}
|
|
311
|
-
className="relative"
|
|
312
293
|
>
|
|
313
294
|
{/* onHover guide, only visible when hovering and not dragging and not having an active zoom window */}
|
|
314
295
|
<div
|
|
315
296
|
style={{height, width: 2, left: mousePosition?.[0] ?? -1}}
|
|
316
297
|
className={cx('bg-gray-700/75 dark:bg-gray-200/75 absolute top-0', {
|
|
317
|
-
hidden: mousePosition === undefined ||
|
|
298
|
+
hidden: mousePosition === undefined || isAnyDragActive || isHoveringDragHandle,
|
|
318
299
|
})}
|
|
319
300
|
></div>
|
|
320
301
|
|
|
321
302
|
{/* drag guide, only visible when dragging */}
|
|
322
|
-
<DraggingWindow dragStart={
|
|
303
|
+
<DraggingWindow dragStart={dragState?.startX} currentX={dragState?.currentX} />
|
|
323
304
|
|
|
324
305
|
{/* zoom window */}
|
|
325
306
|
<ZoomWindow
|
|
@@ -329,23 +310,38 @@ export const AreaGraph = ({
|
|
|
329
310
|
setIsHoveringDragHandle={setIsHoveringDragHandle}
|
|
330
311
|
/>
|
|
331
312
|
|
|
332
|
-
{/* Update Tooltip conditional render */}
|
|
333
|
-
{mousePosition !== undefined &&
|
|
334
|
-
hoverData !== null &&
|
|
335
|
-
!isDragging &&
|
|
336
|
-
!isHoveringDragHandle &&
|
|
337
|
-
isMouseOverGraph && (
|
|
338
|
-
<Tooltip
|
|
339
|
-
x={mousePosition[0]}
|
|
340
|
-
y={mousePosition[1]}
|
|
341
|
-
timestamp={hoverData.timestamp}
|
|
342
|
-
value={hoverData.value}
|
|
343
|
-
containerWidth={width}
|
|
344
|
-
/>
|
|
345
|
-
)}
|
|
346
|
-
|
|
347
313
|
<svg style={{width: '100%', height: '100%'}}>
|
|
348
|
-
|
|
314
|
+
{/* Background for the full strip area */}
|
|
315
|
+
<rect
|
|
316
|
+
x={marginLeft}
|
|
317
|
+
y={0}
|
|
318
|
+
width={width - marginLeft - marginRight}
|
|
319
|
+
height={height}
|
|
320
|
+
fill={fill}
|
|
321
|
+
fillOpacity={0.1}
|
|
322
|
+
/>
|
|
323
|
+
<g>
|
|
324
|
+
{data.map((d, i) => {
|
|
325
|
+
const xPosition = x(d.timestamp);
|
|
326
|
+
// Use stepMs for bucket width
|
|
327
|
+
const rectWidth = x(d.timestamp + stepMs) - xPosition;
|
|
328
|
+
|
|
329
|
+
// Calculate opacity based on sample count
|
|
330
|
+
const opacity = opacityScale(Number(d.sampleCount ?? 1));
|
|
331
|
+
|
|
332
|
+
return (
|
|
333
|
+
<rect
|
|
334
|
+
key={i}
|
|
335
|
+
x={xPosition}
|
|
336
|
+
y={0}
|
|
337
|
+
width={rectWidth}
|
|
338
|
+
height={height}
|
|
339
|
+
fill={fill}
|
|
340
|
+
fillOpacity={opacity}
|
|
341
|
+
/>
|
|
342
|
+
);
|
|
343
|
+
})}
|
|
344
|
+
</g>
|
|
349
345
|
</svg>
|
|
350
346
|
</div>
|
|
351
347
|
);
|
|
@@ -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 '
|
|
20
|
-
import {DataPoint} from './
|
|
21
|
-
import {
|
|
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/
|
|
43
|
-
component:
|
|
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 <
|
|
66
|
+
return <SamplesStrip {...args} onSelectedTimeframe={onSelectedTimeframe} />;
|
|
66
67
|
},
|
|
67
68
|
};
|
|
@@ -0,0 +1,317 @@
|
|
|
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
|
+
import {Button} from '@parca/components';
|
|
24
|
+
|
|
25
|
+
import {TimelineGuide} from '../../TimelineGuide';
|
|
26
|
+
import {NumberDuo} from '../../utils';
|
|
27
|
+
import {DataPoint, SamplesGraph} from './SamplesGraph';
|
|
28
|
+
|
|
29
|
+
export type {DataPoint} from './SamplesGraph';
|
|
30
|
+
|
|
31
|
+
interface DragState {
|
|
32
|
+
stripIndex: number;
|
|
33
|
+
startX: number;
|
|
34
|
+
currentX: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface Props {
|
|
38
|
+
cpus: LabelSet[];
|
|
39
|
+
data: DataPoint[][];
|
|
40
|
+
selectedTimeframe?: {
|
|
41
|
+
labels: LabelSet;
|
|
42
|
+
bounds: NumberDuo;
|
|
43
|
+
};
|
|
44
|
+
onSelectedTimeframe: (labels: LabelSet, bounds: NumberDuo | undefined) => void;
|
|
45
|
+
width?: number;
|
|
46
|
+
bounds: NumberDuo;
|
|
47
|
+
stepMs: number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const labelSetToString = (labelSet?: LabelSet): string => {
|
|
51
|
+
if (labelSet === undefined) {
|
|
52
|
+
return '{}';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
let str = '{';
|
|
56
|
+
|
|
57
|
+
let isFirst = true;
|
|
58
|
+
for (const label of labelSet.labels) {
|
|
59
|
+
if (!isFirst) {
|
|
60
|
+
str += ', ';
|
|
61
|
+
} else {
|
|
62
|
+
isFirst = false;
|
|
63
|
+
}
|
|
64
|
+
str += `${label.name}: ${label.value}`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
str += '}';
|
|
68
|
+
|
|
69
|
+
return str;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const STRIP_HEIGHT = 24;
|
|
73
|
+
const MAX_VISIBLE_STRIPS = 20;
|
|
74
|
+
|
|
75
|
+
const getTimelineGuideHeight = (cpusCount: number, collapsedCount: number): number => {
|
|
76
|
+
return (STRIP_HEIGHT + 4) * (cpusCount - collapsedCount) + 20 * collapsedCount + 24 - 6;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const stickyPx = 0;
|
|
80
|
+
|
|
81
|
+
const SamplesGraphContainer = ({
|
|
82
|
+
isSelected,
|
|
83
|
+
isCollapsed,
|
|
84
|
+
cpu,
|
|
85
|
+
width,
|
|
86
|
+
onToggleCollapse,
|
|
87
|
+
data,
|
|
88
|
+
selectionBounds,
|
|
89
|
+
setSelectionBounds,
|
|
90
|
+
color,
|
|
91
|
+
stepMs,
|
|
92
|
+
onDragStart,
|
|
93
|
+
dragState,
|
|
94
|
+
stripIndex,
|
|
95
|
+
isAnyDragActive,
|
|
96
|
+
timeBounds,
|
|
97
|
+
}: {
|
|
98
|
+
isSelected: boolean;
|
|
99
|
+
isCollapsed: boolean;
|
|
100
|
+
cpu: LabelSet;
|
|
101
|
+
width: number | undefined;
|
|
102
|
+
onToggleCollapse: () => void;
|
|
103
|
+
data: DataPoint[];
|
|
104
|
+
selectionBounds: NumberDuo | undefined;
|
|
105
|
+
setSelectionBounds: (bounds: NumberDuo | undefined) => void;
|
|
106
|
+
color: (label: string) => string;
|
|
107
|
+
stepMs: number;
|
|
108
|
+
onDragStart: (stripIndex: number, startX: number) => void;
|
|
109
|
+
dragState: DragState | undefined;
|
|
110
|
+
stripIndex: number;
|
|
111
|
+
isAnyDragActive: boolean;
|
|
112
|
+
timeBounds: NumberDuo;
|
|
113
|
+
}): JSX.Element => {
|
|
114
|
+
const labelStr = labelSetToString(cpu);
|
|
115
|
+
|
|
116
|
+
const {isIntersecting, ref} = useIntersectionObserver({
|
|
117
|
+
rootMargin: `${stickyPx}px 0px 0px 0px`,
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const isSticky = useMemo(() => {
|
|
121
|
+
return isSelected && isIntersecting;
|
|
122
|
+
}, [isSelected, isIntersecting]);
|
|
123
|
+
|
|
124
|
+
return (
|
|
125
|
+
<div
|
|
126
|
+
className={cx('min-h-5', {
|
|
127
|
+
relative: !isSelected,
|
|
128
|
+
'sticky z-30 bg-white dark:bg-black bg-opacity-75': isSelected,
|
|
129
|
+
'!bg-opacity-100': isSticky,
|
|
130
|
+
})}
|
|
131
|
+
style={{width: width ?? 1468, top: isSelected ? stickyPx : undefined}}
|
|
132
|
+
key={labelStr}
|
|
133
|
+
ref={ref}
|
|
134
|
+
>
|
|
135
|
+
<div
|
|
136
|
+
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"
|
|
137
|
+
style={{
|
|
138
|
+
zIndex: 15,
|
|
139
|
+
}}
|
|
140
|
+
onClick={onToggleCollapse}
|
|
141
|
+
>
|
|
142
|
+
<Icon icon={isCollapsed ? 'bxs:right-arrow' : 'bxs:down-arrow'} />
|
|
143
|
+
{labelStr}
|
|
144
|
+
</div>
|
|
145
|
+
{!isCollapsed ? (
|
|
146
|
+
<SamplesGraph
|
|
147
|
+
data={data}
|
|
148
|
+
height={STRIP_HEIGHT}
|
|
149
|
+
width={width ?? 1468}
|
|
150
|
+
fill={color(labelStr)}
|
|
151
|
+
selectionBounds={selectionBounds}
|
|
152
|
+
setSelectionBounds={setSelectionBounds}
|
|
153
|
+
stepMs={stepMs}
|
|
154
|
+
onDragStart={(startX: number) => onDragStart(stripIndex, startX)}
|
|
155
|
+
dragState={dragState?.stripIndex === stripIndex ? dragState : undefined}
|
|
156
|
+
isAnyDragActive={isAnyDragActive}
|
|
157
|
+
timeBounds={timeBounds}
|
|
158
|
+
/>
|
|
159
|
+
) : null}
|
|
160
|
+
</div>
|
|
161
|
+
);
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
export const SamplesStrip = ({
|
|
165
|
+
cpus,
|
|
166
|
+
data,
|
|
167
|
+
selectedTimeframe,
|
|
168
|
+
onSelectedTimeframe,
|
|
169
|
+
width,
|
|
170
|
+
bounds,
|
|
171
|
+
stepMs,
|
|
172
|
+
}: Props): JSX.Element => {
|
|
173
|
+
const [collapsedLabels, setCollapsedLabels] = useState<Set<string>>(new Set());
|
|
174
|
+
const [showAll, setShowAll] = useState(false);
|
|
175
|
+
const [dragState, setDragState] = useState<DragState | undefined>(undefined);
|
|
176
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
177
|
+
|
|
178
|
+
const isDragging = dragState !== undefined;
|
|
179
|
+
|
|
180
|
+
// Sort cpus and data by label string for consistent ordering across reloads
|
|
181
|
+
const sortedItems = useMemo(() => {
|
|
182
|
+
const items = cpus.map((cpu, i) => ({
|
|
183
|
+
cpu,
|
|
184
|
+
data: data[i],
|
|
185
|
+
label: labelSetToString(cpu),
|
|
186
|
+
}));
|
|
187
|
+
return items.sort((a, b) => a.label.localeCompare(b.label));
|
|
188
|
+
}, [cpus, data]);
|
|
189
|
+
|
|
190
|
+
const hasMore = useMemo(() => sortedItems.length > MAX_VISIBLE_STRIPS, [sortedItems]);
|
|
191
|
+
const visibleItems = useMemo(
|
|
192
|
+
() => (showAll || !hasMore ? sortedItems : sortedItems.slice(0, MAX_VISIBLE_STRIPS)),
|
|
193
|
+
[sortedItems, showAll, hasMore]
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
// Deterministic color: hash the label string so the same label always gets the same color
|
|
197
|
+
// regardless of render order.
|
|
198
|
+
const color = useMemo(() => {
|
|
199
|
+
const palette = d3.schemeObservable10;
|
|
200
|
+
const hashStr = (s: string): number => {
|
|
201
|
+
let h = 0;
|
|
202
|
+
for (let i = 0; i < s.length; i++) {
|
|
203
|
+
h = (Math.imul(31, h) + s.charCodeAt(i)) | 0;
|
|
204
|
+
}
|
|
205
|
+
return Math.abs(h);
|
|
206
|
+
};
|
|
207
|
+
return (label: string): string => palette[hashStr(label) % palette.length];
|
|
208
|
+
}, []);
|
|
209
|
+
|
|
210
|
+
const handleDragStart = (stripIndex: number, startX: number): void => {
|
|
211
|
+
setDragState({stripIndex, startX, currentX: startX});
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
const handleMouseMove = (e: React.MouseEvent): void => {
|
|
215
|
+
if (dragState === undefined || containerRef.current === null) return;
|
|
216
|
+
|
|
217
|
+
const rect = containerRef.current.getBoundingClientRect();
|
|
218
|
+
const x = e.clientX - rect.left;
|
|
219
|
+
// Clamp to container bounds
|
|
220
|
+
const clampedX = Math.max(0, Math.min(x, width ?? rect.width));
|
|
221
|
+
setDragState({...dragState, currentX: clampedX});
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
const handleMouseUp = (e: React.MouseEvent): void => {
|
|
225
|
+
if (dragState === undefined || containerRef.current === null) return;
|
|
226
|
+
|
|
227
|
+
const rect = containerRef.current.getBoundingClientRect();
|
|
228
|
+
const x = e.clientX - rect.left;
|
|
229
|
+
const clampedX = Math.max(0, Math.min(x, width ?? rect.width));
|
|
230
|
+
|
|
231
|
+
const {stripIndex, startX} = dragState;
|
|
232
|
+
if (startX !== clampedX) {
|
|
233
|
+
const start = Math.min(startX, clampedX);
|
|
234
|
+
const end = Math.max(startX, clampedX);
|
|
235
|
+
// Convert pixel positions to timestamps
|
|
236
|
+
const innerWidth = width ?? rect.width;
|
|
237
|
+
const startTs = bounds[0] + (start / innerWidth) * (bounds[1] - bounds[0]);
|
|
238
|
+
const endTs = bounds[0] + (end / innerWidth) * (bounds[1] - bounds[0]);
|
|
239
|
+
onSelectedTimeframe(visibleItems[stripIndex].cpu, [startTs, endTs]);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
setDragState(undefined);
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
const handleMouseLeave = (): void => {
|
|
246
|
+
setDragState(undefined);
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
if (data.length === 0) {
|
|
250
|
+
return (
|
|
251
|
+
<span className="flex justify-center my-10">
|
|
252
|
+
There is no data matching your filter criteria, please try changing the filter.
|
|
253
|
+
</span>
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return (
|
|
258
|
+
<div
|
|
259
|
+
ref={containerRef}
|
|
260
|
+
className={cx('flex flex-col gap-1 relative my-0', {'cursor-ew-resize': isDragging})}
|
|
261
|
+
style={{width: width ?? '100%'}}
|
|
262
|
+
onMouseMove={handleMouseMove}
|
|
263
|
+
onMouseUp={handleMouseUp}
|
|
264
|
+
onMouseLeave={handleMouseLeave}
|
|
265
|
+
>
|
|
266
|
+
<TimelineGuide
|
|
267
|
+
bounds={[BigInt(0), BigInt(bounds[1] - bounds[0])]}
|
|
268
|
+
width={width ?? 1468}
|
|
269
|
+
height={getTimelineGuideHeight(
|
|
270
|
+
visibleItems.length,
|
|
271
|
+
[...collapsedLabels].filter(l => visibleItems.some(item => item.label === l)).length
|
|
272
|
+
)}
|
|
273
|
+
margin={1}
|
|
274
|
+
/>
|
|
275
|
+
{visibleItems.map((item, i) => {
|
|
276
|
+
const isCollapsed = collapsedLabels.has(item.label);
|
|
277
|
+
const isSelected = isEqual(item.cpu, selectedTimeframe?.labels);
|
|
278
|
+
|
|
279
|
+
return (
|
|
280
|
+
<SamplesGraphContainer
|
|
281
|
+
isSelected={isSelected}
|
|
282
|
+
isCollapsed={isCollapsed}
|
|
283
|
+
cpu={item.cpu}
|
|
284
|
+
width={width}
|
|
285
|
+
data={item.data}
|
|
286
|
+
onToggleCollapse={() => {
|
|
287
|
+
const newCollapsedLabels = new Set(collapsedLabels);
|
|
288
|
+
if (collapsedLabels.has(item.label)) {
|
|
289
|
+
newCollapsedLabels.delete(item.label);
|
|
290
|
+
} else {
|
|
291
|
+
newCollapsedLabels.add(item.label);
|
|
292
|
+
}
|
|
293
|
+
setCollapsedLabels(newCollapsedLabels);
|
|
294
|
+
}}
|
|
295
|
+
selectionBounds={isSelected ? selectedTimeframe?.bounds : undefined}
|
|
296
|
+
setSelectionBounds={newBounds => {
|
|
297
|
+
onSelectedTimeframe(item.cpu, newBounds);
|
|
298
|
+
}}
|
|
299
|
+
color={color}
|
|
300
|
+
stepMs={stepMs}
|
|
301
|
+
onDragStart={handleDragStart}
|
|
302
|
+
dragState={dragState}
|
|
303
|
+
stripIndex={i}
|
|
304
|
+
isAnyDragActive={isDragging}
|
|
305
|
+
timeBounds={bounds}
|
|
306
|
+
key={item.label}
|
|
307
|
+
/>
|
|
308
|
+
);
|
|
309
|
+
})}
|
|
310
|
+
{hasMore && !showAll && (
|
|
311
|
+
<Button variant="secondary" onClick={() => setShowAll(true)} className="w-fit mx-auto mt-2">
|
|
312
|
+
Show all {sortedItems.length} rows
|
|
313
|
+
</Button>
|
|
314
|
+
)}
|
|
315
|
+
</div>
|
|
316
|
+
);
|
|
317
|
+
};
|