@parca/profile 0.16.443 → 0.16.445
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/MetricsGraphStrips/AreaGraph/index.d.ts +20 -0
- package/dist/MetricsGraphStrips/AreaGraph/index.d.ts.map +1 -0
- package/dist/MetricsGraphStrips/AreaGraph/index.js +166 -0
- package/dist/MetricsGraphStrips/MetricsGraphStrips.stories.d.ts +17 -0
- package/dist/MetricsGraphStrips/MetricsGraphStrips.stories.d.ts.map +1 -0
- package/dist/MetricsGraphStrips/MetricsGraphStrips.stories.js +48 -0
- package/dist/MetricsGraphStrips/TimelineGuide/index.d.ts +10 -0
- package/dist/MetricsGraphStrips/TimelineGuide/index.d.ts.map +1 -0
- package/dist/MetricsGraphStrips/TimelineGuide/index.js +40 -0
- package/dist/MetricsGraphStrips/index.d.ts +13 -0
- package/dist/MetricsGraphStrips/index.d.ts.map +1 -0
- package/dist/MetricsGraphStrips/index.js +41 -0
- package/dist/ProfileIcicleGraph/index.d.ts.map +1 -1
- package/dist/ProfileIcicleGraph/index.js +3 -11
- package/dist/ProfileView/ColorStackLegend.d.ts.map +1 -0
- package/dist/{ProfileIcicleGraph/IcicleGraphArrow → ProfileView}/ColorStackLegend.js +2 -2
- package/dist/ProfileView/VisualizationPanel.d.ts +4 -1
- package/dist/ProfileView/VisualizationPanel.d.ts.map +1 -1
- package/dist/ProfileView/VisualizationPanel.js +4 -6
- package/dist/ProfileView/index.d.ts.map +1 -1
- package/dist/ProfileView/index.js +33 -10
- package/dist/ProfileViewWithData.d.ts.map +1 -1
- package/dist/ProfileViewWithData.js +1 -3
- package/dist/Table/index.d.ts +2 -29
- package/dist/Table/index.d.ts.map +1 -1
- package/dist/Table/index.js +52 -159
- package/dist/Table/utils/functions.d.ts +49 -0
- package/dist/Table/utils/functions.d.ts.map +1 -0
- package/dist/Table/utils/functions.js +181 -0
- package/dist/components/ActionButtons/GroupByDropdown.js +1 -1
- package/dist/components/ActionButtons/SortByDropdown.d.ts +3 -0
- package/dist/components/ActionButtons/SortByDropdown.d.ts.map +1 -0
- package/dist/components/ActionButtons/SortByDropdown.js +49 -0
- package/dist/components/VisualisationToolbar/MultiLevelDropdown.d.ts.map +1 -1
- package/dist/components/VisualisationToolbar/MultiLevelDropdown.js +3 -27
- package/dist/components/VisualisationToolbar/TableColumnsDropdown.d.ts.map +1 -1
- package/dist/components/VisualisationToolbar/TableColumnsDropdown.js +3 -1
- package/dist/components/VisualisationToolbar/index.d.ts +11 -0
- package/dist/components/VisualisationToolbar/index.d.ts.map +1 -1
- package/dist/components/VisualisationToolbar/index.js +13 -6
- package/dist/styles.css +1 -1
- package/package.json +4 -3
- package/src/MetricsGraphStrips/AreaGraph/index.tsx +321 -0
- package/src/MetricsGraphStrips/MetricsGraphStrips.stories.tsx +57 -0
- package/src/MetricsGraphStrips/TimelineGuide/index.tsx +111 -0
- package/src/MetricsGraphStrips/index.tsx +93 -0
- package/src/ProfileIcicleGraph/index.tsx +2 -18
- package/src/{ProfileIcicleGraph/IcicleGraphArrow → ProfileView}/ColorStackLegend.tsx +2 -2
- package/src/ProfileView/VisualizationPanel.tsx +13 -10
- package/src/ProfileView/index.tsx +59 -9
- package/src/ProfileViewWithData.tsx +1 -3
- package/src/Table/index.tsx +121 -263
- package/src/Table/utils/functions.ts +284 -0
- package/src/components/ActionButtons/GroupByDropdown.tsx +1 -1
- package/src/components/ActionButtons/SortByDropdown.tsx +84 -0
- package/src/components/VisualisationToolbar/MultiLevelDropdown.tsx +7 -30
- package/src/components/VisualisationToolbar/TableColumnsDropdown.tsx +3 -1
- package/src/components/VisualisationToolbar/index.tsx +103 -58
- package/dist/ProfileIcicleGraph/IcicleGraphArrow/ColorStackLegend.d.ts.map +0 -1
- /package/dist/{ProfileIcicleGraph/IcicleGraphArrow → ProfileView}/ColorStackLegend.d.ts +0 -0
|
@@ -0,0 +1,321 @@
|
|
|
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, useState} from 'react';
|
|
15
|
+
|
|
16
|
+
import {Icon} from '@iconify/react';
|
|
17
|
+
import cx from 'classnames';
|
|
18
|
+
import * as d3 from 'd3';
|
|
19
|
+
|
|
20
|
+
export interface DataPoint {
|
|
21
|
+
timestamp: number;
|
|
22
|
+
value: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type NumberDuo = [number, number];
|
|
26
|
+
|
|
27
|
+
interface Props {
|
|
28
|
+
width: number;
|
|
29
|
+
height: number;
|
|
30
|
+
marginLeft?: number;
|
|
31
|
+
marginRight?: number;
|
|
32
|
+
marginTop?: number;
|
|
33
|
+
marginBottom?: number;
|
|
34
|
+
fill?: string;
|
|
35
|
+
data: DataPoint[];
|
|
36
|
+
selectionBounds?: NumberDuo | undefined;
|
|
37
|
+
setSelectionBounds: (newBounds: NumberDuo | undefined) => void;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const DraggingWindow = ({
|
|
41
|
+
dragStart,
|
|
42
|
+
currentX,
|
|
43
|
+
}: {
|
|
44
|
+
dragStart: number | undefined;
|
|
45
|
+
currentX: number | undefined;
|
|
46
|
+
}): JSX.Element | null => {
|
|
47
|
+
const start = useMemo(() => Math.min(dragStart ?? 0, currentX ?? 0), [dragStart, currentX]);
|
|
48
|
+
const width = useMemo(() => Math.abs((dragStart ?? 0) - (currentX ?? 0)), [dragStart, currentX]);
|
|
49
|
+
|
|
50
|
+
if (dragStart === undefined || currentX === undefined) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<div
|
|
56
|
+
style={{height: '100%', width, left: start}}
|
|
57
|
+
className={cx(
|
|
58
|
+
'bg-gray-500 absolute top-0 opacity-50 border-x-2 border-gray-900 dark:border-gray-100'
|
|
59
|
+
)}
|
|
60
|
+
></div>
|
|
61
|
+
);
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const ZoomWindow = ({
|
|
65
|
+
zoomWindow,
|
|
66
|
+
width,
|
|
67
|
+
onZoomWindowChange,
|
|
68
|
+
setIsHoveringDragHandle,
|
|
69
|
+
}: {
|
|
70
|
+
zoomWindow?: NumberDuo;
|
|
71
|
+
width: number;
|
|
72
|
+
onZoomWindowChange: (newWindow: NumberDuo) => void;
|
|
73
|
+
setIsHoveringDragHandle: (arg: boolean) => void;
|
|
74
|
+
}): JSX.Element | null => {
|
|
75
|
+
const windowStartHandleRef = useRef<HTMLDivElement>(null);
|
|
76
|
+
const windowEndHandleRef = useRef<HTMLDivElement>(null);
|
|
77
|
+
const [zoomWindowState, setZoomWindowState] = useState<NumberDuo | undefined>(zoomWindow);
|
|
78
|
+
const [dragginStart, setDraggingStart] = useState(false);
|
|
79
|
+
const [draggingEnd, setDraggingEnd] = useState(false);
|
|
80
|
+
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
if (
|
|
83
|
+
zoomWindow === undefined ||
|
|
84
|
+
zoomWindowState === undefined ||
|
|
85
|
+
zoomWindow[0] !== zoomWindowState[0] ||
|
|
86
|
+
zoomWindow[1] !== zoomWindowState[1]
|
|
87
|
+
) {
|
|
88
|
+
setZoomWindowState(zoomWindow);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
92
|
+
}, [zoomWindow]);
|
|
93
|
+
|
|
94
|
+
if (zoomWindowState === undefined) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
const beforeStart = 0;
|
|
98
|
+
const beforeWidth = zoomWindowState[0];
|
|
99
|
+
const afterStart = zoomWindowState[1];
|
|
100
|
+
const afterWidth = width - zoomWindowState[1];
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<div
|
|
104
|
+
className="absolute w-full h-full"
|
|
105
|
+
onMouseMove={e => {
|
|
106
|
+
if (dragginStart) {
|
|
107
|
+
const [x] = d3.pointer(e);
|
|
108
|
+
if (x >= afterStart - 10) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const newStart = Math.min(x, afterStart);
|
|
112
|
+
const newEnd = Math.max(x, afterStart);
|
|
113
|
+
setZoomWindowState([newStart, newEnd]);
|
|
114
|
+
}
|
|
115
|
+
if (draggingEnd) {
|
|
116
|
+
const [x] = d3.pointer(e);
|
|
117
|
+
if (x <= beforeWidth + 10) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
const newStart = Math.min(x, beforeWidth);
|
|
121
|
+
const newEnd = Math.max(x, beforeWidth);
|
|
122
|
+
setZoomWindowState([newStart, newEnd]);
|
|
123
|
+
}
|
|
124
|
+
}}
|
|
125
|
+
onMouseLeave={() => {
|
|
126
|
+
setDraggingStart(false);
|
|
127
|
+
setDraggingEnd(false);
|
|
128
|
+
}}
|
|
129
|
+
onMouseUp={() => {
|
|
130
|
+
if (dragginStart) {
|
|
131
|
+
setDraggingStart(false);
|
|
132
|
+
}
|
|
133
|
+
if (draggingEnd) {
|
|
134
|
+
setDraggingEnd(false);
|
|
135
|
+
}
|
|
136
|
+
if (zoomWindowState[0] === zoomWindow?.[0] && zoomWindowState[1] === zoomWindow?.[1]) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
onZoomWindowChange(zoomWindowState);
|
|
140
|
+
setZoomWindowState(undefined);
|
|
141
|
+
}}
|
|
142
|
+
>
|
|
143
|
+
<div
|
|
144
|
+
style={{height: '100%', width: beforeWidth, left: beforeStart}}
|
|
145
|
+
className={cx(
|
|
146
|
+
'bg-gray-500/50 absolute top-0 border-r-2 border-gray-900 dark:border-gray-100 z-20'
|
|
147
|
+
)}
|
|
148
|
+
>
|
|
149
|
+
<div
|
|
150
|
+
className="w-3 h-4 absolute top-0 right-[-7px] rounded-b bg-gray-200 cursor-ew-resize flex justify-center"
|
|
151
|
+
onMouseDown={e => {
|
|
152
|
+
setDraggingStart(true);
|
|
153
|
+
e.stopPropagation();
|
|
154
|
+
e.preventDefault();
|
|
155
|
+
}}
|
|
156
|
+
ref={windowStartHandleRef}
|
|
157
|
+
onMouseEnter={() => {
|
|
158
|
+
setIsHoveringDragHandle(true);
|
|
159
|
+
}}
|
|
160
|
+
onMouseLeave={() => {
|
|
161
|
+
setIsHoveringDragHandle(false);
|
|
162
|
+
}}
|
|
163
|
+
>
|
|
164
|
+
<Icon icon="si:drag-handle-line" className="rotate-90" fontSize={16} />
|
|
165
|
+
</div>
|
|
166
|
+
</div>
|
|
167
|
+
|
|
168
|
+
<div
|
|
169
|
+
style={{height: '100%', width: afterWidth, left: afterStart}}
|
|
170
|
+
className={cx(
|
|
171
|
+
'bg-gray-500/50 absolute top-0 border-l-2 border-gray-900 dark:border-gray-100'
|
|
172
|
+
)}
|
|
173
|
+
>
|
|
174
|
+
<div
|
|
175
|
+
className="w-3 h-4 absolute top-0 rounded-b bg-gray-200 cursor-ew-resize flex justify-center left-[-7px]"
|
|
176
|
+
onMouseDown={e => {
|
|
177
|
+
setDraggingEnd(true);
|
|
178
|
+
e.stopPropagation();
|
|
179
|
+
e.preventDefault();
|
|
180
|
+
}}
|
|
181
|
+
ref={windowEndHandleRef}
|
|
182
|
+
onMouseEnter={() => {
|
|
183
|
+
setIsHoveringDragHandle(true);
|
|
184
|
+
}}
|
|
185
|
+
onMouseLeave={() => {
|
|
186
|
+
setIsHoveringDragHandle(false);
|
|
187
|
+
}}
|
|
188
|
+
>
|
|
189
|
+
<Icon icon="si:drag-handle-line" className="rotate-90" fontSize={16} />
|
|
190
|
+
</div>
|
|
191
|
+
</div>
|
|
192
|
+
</div>
|
|
193
|
+
);
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
export const AreaGraph = ({
|
|
197
|
+
data,
|
|
198
|
+
height,
|
|
199
|
+
width,
|
|
200
|
+
marginLeft = 0,
|
|
201
|
+
marginRight = 0,
|
|
202
|
+
marginBottom = 0,
|
|
203
|
+
marginTop = 0,
|
|
204
|
+
fill = 'gray',
|
|
205
|
+
selectionBounds,
|
|
206
|
+
setSelectionBounds,
|
|
207
|
+
}: Props): JSX.Element => {
|
|
208
|
+
const [mousePosition, setMousePosition] = useState<NumberDuo | undefined>(undefined);
|
|
209
|
+
const [dragStart, setDragStart] = useState<number | undefined>(undefined);
|
|
210
|
+
const [isHoveringDragHandle, setIsHoveringDragHandle] = useState(false);
|
|
211
|
+
const isDragging = dragStart !== undefined;
|
|
212
|
+
|
|
213
|
+
// Declare the x (horizontal position) scale.
|
|
214
|
+
const x = d3.scaleUtc(d3.extent(data, d => d.timestamp) as NumberDuo, [
|
|
215
|
+
marginLeft,
|
|
216
|
+
width - marginRight,
|
|
217
|
+
]);
|
|
218
|
+
|
|
219
|
+
// Declare the y (vertical position) scale.
|
|
220
|
+
const y = d3.scaleLinear(
|
|
221
|
+
[0, d3.max(data, d => d.value) as number],
|
|
222
|
+
[height - marginBottom, marginTop]
|
|
223
|
+
);
|
|
224
|
+
const area = d3
|
|
225
|
+
.area<DataPoint>()
|
|
226
|
+
.curve(d3.curveMonotoneX)
|
|
227
|
+
.x(d => x(d.timestamp))
|
|
228
|
+
.y0(y(0))
|
|
229
|
+
.y1(d => y(d.value));
|
|
230
|
+
|
|
231
|
+
const zoomWindow: NumberDuo | undefined = useMemo(() => {
|
|
232
|
+
if (selectionBounds === undefined) {
|
|
233
|
+
return undefined;
|
|
234
|
+
}
|
|
235
|
+
return [x(selectionBounds[0]), x(selectionBounds[1])];
|
|
236
|
+
|
|
237
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
238
|
+
}, [selectionBounds]);
|
|
239
|
+
|
|
240
|
+
const setSelectionBoundsWithScaling = ([startPx, endPx]: NumberDuo): void => {
|
|
241
|
+
setSelectionBounds([x.invert(startPx).getTime(), x.invert(endPx).getTime()]);
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
return (
|
|
245
|
+
<div
|
|
246
|
+
style={{height, width}}
|
|
247
|
+
onMouseMove={e => {
|
|
248
|
+
const [x, y] = d3.pointer(e);
|
|
249
|
+
setMousePosition([x, y]);
|
|
250
|
+
}}
|
|
251
|
+
onMouseLeave={() => {
|
|
252
|
+
setMousePosition(undefined);
|
|
253
|
+
setDragStart(undefined);
|
|
254
|
+
}}
|
|
255
|
+
onMouseDown={e => {
|
|
256
|
+
// only left mouse button
|
|
257
|
+
if (e.button !== 0) {
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// X/Y coordinate array relative to svg
|
|
262
|
+
const rel = d3.pointer(e);
|
|
263
|
+
|
|
264
|
+
const xCoordinate = rel[0];
|
|
265
|
+
const xCoordinateWithoutMargin = xCoordinate - marginLeft;
|
|
266
|
+
if (xCoordinateWithoutMargin >= 0) {
|
|
267
|
+
setDragStart(xCoordinateWithoutMargin);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
e.stopPropagation();
|
|
271
|
+
e.preventDefault();
|
|
272
|
+
}}
|
|
273
|
+
onMouseUp={e => {
|
|
274
|
+
if (dragStart === undefined) {
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const rel = d3.pointer(e);
|
|
279
|
+
const xCoordinate = rel[0];
|
|
280
|
+
const xCoordinateWithoutMargin = xCoordinate - marginLeft;
|
|
281
|
+
if (xCoordinateWithoutMargin >= 0 && dragStart !== xCoordinateWithoutMargin) {
|
|
282
|
+
const start = Math.min(dragStart, xCoordinateWithoutMargin);
|
|
283
|
+
const end = Math.max(dragStart, xCoordinateWithoutMargin);
|
|
284
|
+
setSelectionBoundsWithScaling([start, end]);
|
|
285
|
+
}
|
|
286
|
+
setDragStart(undefined);
|
|
287
|
+
}}
|
|
288
|
+
className="relative"
|
|
289
|
+
>
|
|
290
|
+
{/* onHover guide, only visible when hovering and not dragging and not having an active zoom window */}
|
|
291
|
+
<div
|
|
292
|
+
style={{height, width: 2, left: mousePosition?.[0] ?? -1}}
|
|
293
|
+
className={cx('bg-gray-700/75 dark:bg-gray-200/75 absolute top-0', {
|
|
294
|
+
hidden: mousePosition === undefined || isDragging || isHoveringDragHandle,
|
|
295
|
+
})}
|
|
296
|
+
></div>
|
|
297
|
+
|
|
298
|
+
{/* drag guide, only visible when dragging */}
|
|
299
|
+
<DraggingWindow dragStart={dragStart} currentX={mousePosition?.[0]} />
|
|
300
|
+
|
|
301
|
+
{/* zoom window */}
|
|
302
|
+
<ZoomWindow
|
|
303
|
+
zoomWindow={zoomWindow}
|
|
304
|
+
width={width}
|
|
305
|
+
onZoomWindowChange={setSelectionBoundsWithScaling}
|
|
306
|
+
setIsHoveringDragHandle={setIsHoveringDragHandle}
|
|
307
|
+
/>
|
|
308
|
+
|
|
309
|
+
{/* Inactive indicator */}
|
|
310
|
+
<div
|
|
311
|
+
className={cx('absolute top-0 left-0 w-full h-full bg-gray-900/50 dark:bg-gray-200/50', {
|
|
312
|
+
hidden: isDragging || selectionBounds !== undefined,
|
|
313
|
+
})}
|
|
314
|
+
></div>
|
|
315
|
+
|
|
316
|
+
<svg style={{width: '100%', height: '100%'}}>
|
|
317
|
+
<path fill={fill} d={area(data) as string} className="opacity-80" />
|
|
318
|
+
</svg>
|
|
319
|
+
</div>
|
|
320
|
+
);
|
|
321
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
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
|
+
// eslint-disable-next-line import/named
|
|
15
|
+
import {useArgs} from '@storybook/preview-api';
|
|
16
|
+
// eslint-disable-next-line import/named
|
|
17
|
+
import {Meta} from '@storybook/react';
|
|
18
|
+
|
|
19
|
+
import {DataPoint, NumberDuo} from './AreaGraph';
|
|
20
|
+
import {MetricsGraphStrips} from './index';
|
|
21
|
+
|
|
22
|
+
const mockData: DataPoint[][] = [[], [], []];
|
|
23
|
+
|
|
24
|
+
for (let i = 0; i < 200; i++) {
|
|
25
|
+
for (let j = 0; j < mockData.length; j++) {
|
|
26
|
+
mockData[j].push({
|
|
27
|
+
timestamp: 1731326092000 + i * 100,
|
|
28
|
+
value: Math.floor(Math.random() * 100),
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
const meta: Meta = {
|
|
33
|
+
title: 'components/MetricsGraphStrips',
|
|
34
|
+
component: MetricsGraphStrips,
|
|
35
|
+
};
|
|
36
|
+
export default meta;
|
|
37
|
+
|
|
38
|
+
export const ThreeCPUStrips = {
|
|
39
|
+
args: {
|
|
40
|
+
cpus: Array.from(mockData, (_, i) => `CPU ${i + 1}`),
|
|
41
|
+
data: mockData,
|
|
42
|
+
selectedTimeline: {index: 1, bounds: [mockData[0][25].timestamp, mockData[0][100].timestamp]},
|
|
43
|
+
onSelectedTimeline: (index: number, bounds: NumberDuo): void => {
|
|
44
|
+
console.log('onSelectedTimeline', index, bounds);
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
render: function Component(args: any): JSX.Element {
|
|
48
|
+
const [, setArgs] = useArgs();
|
|
49
|
+
|
|
50
|
+
const onSelectedTimeline = (index: number, bounds: NumberDuo): void => {
|
|
51
|
+
args.onSelectedTimeline(index, bounds);
|
|
52
|
+
setArgs({...args, selectedTimeline: {index, bounds}});
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
return <MetricsGraphStrips {...args} onSelectedTimeline={onSelectedTimeline} />;
|
|
56
|
+
},
|
|
57
|
+
};
|
|
@@ -0,0 +1,111 @@
|
|
|
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 {Fragment, useMemo} from 'react';
|
|
15
|
+
|
|
16
|
+
import * as d3 from 'd3';
|
|
17
|
+
|
|
18
|
+
import {DataPoint, NumberDuo} from '../AreaGraph';
|
|
19
|
+
|
|
20
|
+
interface Props {
|
|
21
|
+
width: number;
|
|
22
|
+
height: number;
|
|
23
|
+
margin: number;
|
|
24
|
+
data: DataPoint[][];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const alignBeforeAxisCorrection = (val: number): number => {
|
|
28
|
+
if (val < 10000) {
|
|
29
|
+
return -24;
|
|
30
|
+
}
|
|
31
|
+
if (val < 100000) {
|
|
32
|
+
return -28;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return 0;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export const TimelineGuide = ({data, width, height, margin}: Props): JSX.Element => {
|
|
39
|
+
const bounds = useMemo(() => {
|
|
40
|
+
const bounds: NumberDuo = [Infinity, -Infinity];
|
|
41
|
+
data.forEach(cpuData => {
|
|
42
|
+
cpuData.forEach(dataPoint => {
|
|
43
|
+
bounds[0] = Math.min(bounds[0], dataPoint.timestamp);
|
|
44
|
+
bounds[1] = Math.max(bounds[1], dataPoint.timestamp);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
return [0, bounds[1] - bounds[0]];
|
|
48
|
+
}, [data]);
|
|
49
|
+
|
|
50
|
+
const xScale = d3.scaleLinear().domain(bounds).range([0, width]);
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<div className="relative h-4">
|
|
54
|
+
<div className="absolute" style={{width, height}}>
|
|
55
|
+
<svg style={{width: '100%', height: '100%'}}>
|
|
56
|
+
<g
|
|
57
|
+
className="x axis"
|
|
58
|
+
fill="none"
|
|
59
|
+
fontSize="10"
|
|
60
|
+
textAnchor="middle"
|
|
61
|
+
transform={`translate(0,${height - margin})`}
|
|
62
|
+
>
|
|
63
|
+
{xScale.ticks().map((d, i) => (
|
|
64
|
+
<Fragment key={`${i.toString()}-${d.toString()}`}>
|
|
65
|
+
<g
|
|
66
|
+
key={`tick-${i}`}
|
|
67
|
+
className="tick"
|
|
68
|
+
/* eslint-disable-next-line @typescript-eslint/restrict-template-expressions */
|
|
69
|
+
transform={`translate(${xScale(d) + alignBeforeAxisCorrection(d)}, ${-height})`}
|
|
70
|
+
>
|
|
71
|
+
{/* <line y2={6} className="stroke-gray-300 dark:stroke-gray-500" /> */}
|
|
72
|
+
<text fill="currentColor" dy=".71em" y={9}>
|
|
73
|
+
{d} ms
|
|
74
|
+
</text>
|
|
75
|
+
</g>
|
|
76
|
+
<g key={`grid-${i}`}>
|
|
77
|
+
<line
|
|
78
|
+
className="stroke-gray-300 dark:stroke-gray-500"
|
|
79
|
+
x1={xScale(d)}
|
|
80
|
+
x2={xScale(d)}
|
|
81
|
+
y1={0}
|
|
82
|
+
y2={-height + margin}
|
|
83
|
+
/>
|
|
84
|
+
</g>
|
|
85
|
+
</Fragment>
|
|
86
|
+
))}
|
|
87
|
+
<line
|
|
88
|
+
className="stroke-gray-300 dark:stroke-gray-500"
|
|
89
|
+
x1={0}
|
|
90
|
+
x2={width}
|
|
91
|
+
y1={-height + 1}
|
|
92
|
+
y2={-height + 1}
|
|
93
|
+
/>
|
|
94
|
+
<line
|
|
95
|
+
className="stroke-gray-300 dark:stroke-gray-500"
|
|
96
|
+
x1={0}
|
|
97
|
+
x2={width}
|
|
98
|
+
y1={-height + 20}
|
|
99
|
+
y2={-height + 20}
|
|
100
|
+
/>
|
|
101
|
+
{/* <g transform={`translate(${(width - 2.5 * margin) / 2}, ${margin / 2})`}>
|
|
102
|
+
<text fill="currentColor" dy=".71em" y={5} className="text-sm">
|
|
103
|
+
Time
|
|
104
|
+
</text>
|
|
105
|
+
</g> */}
|
|
106
|
+
</g>
|
|
107
|
+
</svg>
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
);
|
|
111
|
+
};
|
|
@@ -0,0 +1,93 @@
|
|
|
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 {useState} from 'react';
|
|
15
|
+
|
|
16
|
+
import {Icon} from '@iconify/react';
|
|
17
|
+
import * as d3 from 'd3';
|
|
18
|
+
|
|
19
|
+
import {AreaGraph, DataPoint, NumberDuo} from './AreaGraph';
|
|
20
|
+
import {TimelineGuide} from './TimelineGuide';
|
|
21
|
+
|
|
22
|
+
interface Props {
|
|
23
|
+
cpus: string[];
|
|
24
|
+
data: DataPoint[][];
|
|
25
|
+
selectedTimeline?: {
|
|
26
|
+
index: number;
|
|
27
|
+
bounds: NumberDuo;
|
|
28
|
+
};
|
|
29
|
+
onSelectedTimeline: (index: number, bounds: NumberDuo | undefined) => void;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const getTimelineGuideHeight = (cpus: string[], collapsedIndices: number[]): number => {
|
|
33
|
+
return 56 * (cpus.length - collapsedIndices.length) + 20 * collapsedIndices.length + 24;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const MetricsGraphStrips = ({
|
|
37
|
+
cpus,
|
|
38
|
+
data,
|
|
39
|
+
selectedTimeline,
|
|
40
|
+
onSelectedTimeline,
|
|
41
|
+
}: Props): JSX.Element => {
|
|
42
|
+
const [collapsedIndices, setCollapsedIndices] = useState<number[]>([]);
|
|
43
|
+
|
|
44
|
+
// @ts-expect-error
|
|
45
|
+
const color = d3.scaleOrdinal(d3.schemeObservable10);
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<div className="flex flex-col gap-1 relative">
|
|
49
|
+
<TimelineGuide
|
|
50
|
+
data={data}
|
|
51
|
+
width={1468}
|
|
52
|
+
height={getTimelineGuideHeight(cpus, collapsedIndices)}
|
|
53
|
+
margin={1}
|
|
54
|
+
/>
|
|
55
|
+
{cpus.map((cpu, i) => {
|
|
56
|
+
const isCollapsed = collapsedIndices.includes(i);
|
|
57
|
+
return (
|
|
58
|
+
<div className="relative min-h-5" key={cpu}>
|
|
59
|
+
<div
|
|
60
|
+
className="text-xs absolute top-0 left-0 flex gap-[2px] items-center bg-white/50 px-1 rounded-sm cursor-pointer z-30"
|
|
61
|
+
onClick={() => {
|
|
62
|
+
const newCollapsedIndices = [...collapsedIndices];
|
|
63
|
+
if (collapsedIndices.includes(i)) {
|
|
64
|
+
newCollapsedIndices.splice(newCollapsedIndices.indexOf(i), 1);
|
|
65
|
+
} else {
|
|
66
|
+
newCollapsedIndices.push(i);
|
|
67
|
+
}
|
|
68
|
+
setCollapsedIndices(newCollapsedIndices);
|
|
69
|
+
}}
|
|
70
|
+
>
|
|
71
|
+
<Icon icon={isCollapsed ? 'bxs:right-arrow' : 'bxs:down-arrow'} />
|
|
72
|
+
{cpu}
|
|
73
|
+
</div>
|
|
74
|
+
{!isCollapsed ? (
|
|
75
|
+
<AreaGraph
|
|
76
|
+
data={data[i]}
|
|
77
|
+
height={56}
|
|
78
|
+
width={1468}
|
|
79
|
+
fill={color(i.toString()) as string}
|
|
80
|
+
selectionBounds={
|
|
81
|
+
selectedTimeline?.index === i ? selectedTimeline.bounds : undefined
|
|
82
|
+
}
|
|
83
|
+
setSelectionBounds={bounds => {
|
|
84
|
+
onSelectedTimeline(i, bounds);
|
|
85
|
+
}}
|
|
86
|
+
/>
|
|
87
|
+
) : null}
|
|
88
|
+
</div>
|
|
89
|
+
);
|
|
90
|
+
})}
|
|
91
|
+
</div>
|
|
92
|
+
);
|
|
93
|
+
};
|
|
@@ -13,20 +13,18 @@
|
|
|
13
13
|
|
|
14
14
|
import React, {useEffect, useMemo, useState} from 'react';
|
|
15
15
|
|
|
16
|
-
import {Table, tableFromIPC} from 'apache-arrow';
|
|
17
16
|
import {AnimatePresence, motion} from 'framer-motion';
|
|
18
17
|
|
|
19
18
|
import {Flamegraph, FlamegraphArrow} from '@parca/client';
|
|
20
19
|
import {IcicleGraphSkeleton, useParcaContext, useURLState} from '@parca/components';
|
|
21
20
|
import {ProfileType} from '@parca/parser';
|
|
22
|
-
import {capitalizeOnlyFirstLetter, divide
|
|
21
|
+
import {capitalizeOnlyFirstLetter, divide} from '@parca/utilities';
|
|
23
22
|
|
|
24
23
|
import {useProfileViewContext} from '../ProfileView/ProfileViewContext';
|
|
25
24
|
import DiffLegend from '../components/DiffLegend';
|
|
26
25
|
import {IcicleGraph} from './IcicleGraph';
|
|
27
26
|
import {FIELD_FUNCTION_NAME, IcicleGraphArrow} from './IcicleGraphArrow';
|
|
28
|
-
import
|
|
29
|
-
import useMappingList, {useFilenamesList} from './IcicleGraphArrow/useMappingList';
|
|
27
|
+
import useMappingList from './IcicleGraphArrow/useMappingList';
|
|
30
28
|
|
|
31
29
|
const numberFormatter = new Intl.NumberFormat('en-US');
|
|
32
30
|
|
|
@@ -70,14 +68,8 @@ const ProfileIcicleGraph = function ProfileIcicleGraphNonMemo({
|
|
|
70
68
|
const {onError, authenticationErrorMessage, isDarkMode} = useParcaContext();
|
|
71
69
|
const {compareMode} = useProfileViewContext();
|
|
72
70
|
const [isLoading, setIsLoading] = useState<boolean>(true);
|
|
73
|
-
const isColorStackLegendEnabled = selectQueryParam('color_stack_legend') === 'true';
|
|
74
|
-
|
|
75
|
-
const table: Table<any> | null = useMemo(() => {
|
|
76
|
-
return arrow !== undefined ? tableFromIPC(arrow.record) : null;
|
|
77
|
-
}, [arrow]);
|
|
78
71
|
|
|
79
72
|
const mappingsList = useMappingList(metadataMappingFiles);
|
|
80
|
-
const filenamesList = useFilenamesList(table);
|
|
81
73
|
|
|
82
74
|
const [storeSortBy = FIELD_FUNCTION_NAME] = useURLState('sort_by');
|
|
83
75
|
const [colorBy, setColorBy] = useURLState('color_by');
|
|
@@ -89,7 +81,6 @@ const ProfileIcicleGraph = function ProfileIcicleGraphNonMemo({
|
|
|
89
81
|
const [compareAbsolute = compareAbsoluteDefault] = useURLState('compare_absolute');
|
|
90
82
|
const isCompareAbsolute = compareAbsolute === 'true';
|
|
91
83
|
|
|
92
|
-
const colorByValue = colorBy === undefined || colorBy === '' ? 'binary' : (colorBy as string);
|
|
93
84
|
const mappingsListCount = useMemo(
|
|
94
85
|
() => mappingsList.filter(m => m !== '').length,
|
|
95
86
|
[mappingsList]
|
|
@@ -231,13 +222,6 @@ const ProfileIcicleGraph = function ProfileIcicleGraphNonMemo({
|
|
|
231
222
|
transition={{duration: 0.5}}
|
|
232
223
|
>
|
|
233
224
|
{compareMode ? <DiffLegend /> : null}
|
|
234
|
-
{isColorStackLegendEnabled && (
|
|
235
|
-
<ColorStackLegend
|
|
236
|
-
compareMode={compareMode}
|
|
237
|
-
mappings={colorByValue === 'binary' ? mappingsList : filenamesList}
|
|
238
|
-
loading={isLoading}
|
|
239
|
-
/>
|
|
240
|
-
)}
|
|
241
225
|
<div className="min-h-48" id="h-icicle-graph">
|
|
242
226
|
<>{icicleGraph}</>
|
|
243
227
|
</div>
|
|
@@ -20,8 +20,8 @@ import {useURLState} from '@parca/components';
|
|
|
20
20
|
import {USER_PREFERENCES, useCurrentColorProfile, useUserPreference} from '@parca/hooks';
|
|
21
21
|
import {EVERYTHING_ELSE, selectDarkMode, useAppSelector} from '@parca/store';
|
|
22
22
|
|
|
23
|
-
import {getMappingColors} from '
|
|
24
|
-
import useMappingList from '
|
|
23
|
+
import {getMappingColors} from '../ProfileIcicleGraph/IcicleGraphArrow/';
|
|
24
|
+
import useMappingList from '../ProfileIcicleGraph/IcicleGraphArrow/useMappingList';
|
|
25
25
|
|
|
26
26
|
interface Props {
|
|
27
27
|
mappings?: string[];
|