@parca/profile 0.16.0 → 0.16.22
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 +40 -0
- package/dist/Callgraph/Edge/index.d.ts +22 -0
- package/dist/Callgraph/Edge/index.js +30 -0
- package/dist/Callgraph/Node/index.d.ts +19 -0
- package/dist/Callgraph/Node/index.js +37 -0
- package/dist/Callgraph/index.d.ts +8 -0
- package/dist/Callgraph/index.js +137 -0
- package/dist/Callgraph/mockData/index.d.ts +148 -0
- package/dist/Callgraph/mockData/index.js +577 -0
- package/dist/Callgraph/utils.d.ts +19 -0
- package/dist/Callgraph/utils.js +82 -0
- package/dist/GraphTooltip/index.d.ts +19 -0
- package/dist/GraphTooltip/index.js +119 -0
- package/dist/IcicleGraph.d.ts +35 -0
- package/dist/IcicleGraph.js +139 -0
- package/dist/MatchersInput/index.d.ts +23 -0
- package/dist/MatchersInput/index.js +479 -0
- package/dist/MetricsCircle/index.d.ts +7 -0
- package/dist/MetricsCircle/index.js +18 -0
- package/dist/MetricsGraph/index.d.ts +35 -0
- package/dist/MetricsGraph/index.js +349 -0
- package/dist/MetricsSeries/index.d.ts +11 -0
- package/dist/MetricsSeries/index.js +21 -0
- package/dist/ProfileExplorer/ProfileExplorerCompare.d.ts +19 -0
- package/dist/ProfileExplorer/ProfileExplorerCompare.js +38 -0
- package/dist/ProfileExplorer/ProfileExplorerSingle.d.ts +15 -0
- package/dist/ProfileExplorer/ProfileExplorerSingle.js +19 -0
- package/dist/ProfileExplorer/index.d.ts +9 -0
- package/dist/ProfileExplorer/index.js +203 -0
- package/dist/ProfileIcicleGraph.d.ts +10 -0
- package/dist/ProfileIcicleGraph.js +28 -0
- package/dist/ProfileMetricsGraph/index.d.ts +22 -0
- package/dist/ProfileMetricsGraph/index.js +127 -0
- package/dist/ProfileSVG.module.css +3 -0
- package/dist/ProfileSelector/CompareButton.d.ts +5 -0
- package/dist/ProfileSelector/CompareButton.js +41 -0
- package/dist/ProfileSelector/MergeButton.d.ts +5 -0
- package/dist/ProfileSelector/MergeButton.js +41 -0
- package/dist/ProfileSelector/index.d.ts +29 -0
- package/dist/ProfileSelector/index.js +133 -0
- package/dist/ProfileSource.d.ts +88 -0
- package/dist/ProfileSource.js +239 -0
- package/dist/ProfileTypeSelector/index.d.ts +20 -0
- package/dist/ProfileTypeSelector/index.js +138 -0
- package/dist/ProfileView.d.ts +39 -0
- package/dist/ProfileView.js +111 -0
- package/dist/ProfileView.styles.css +3 -0
- package/dist/ProfileViewWithData.d.ts +11 -0
- package/dist/ProfileViewWithData.js +116 -0
- package/dist/TopTable.d.ts +9 -0
- package/dist/TopTable.js +140 -0
- package/dist/TopTable.styles.css +7 -0
- package/dist/components/DiffLegend.d.ts +2 -0
- package/dist/components/DiffLegend.js +62 -0
- package/dist/components/ProfileShareButton/ResultBox.d.ts +6 -0
- package/dist/components/ProfileShareButton/ResultBox.js +46 -0
- package/dist/components/ProfileShareButton/index.d.ts +7 -0
- package/dist/components/ProfileShareButton/index.js +119 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +64 -0
- package/dist/styles.css +1 -0
- package/dist/useDelayedLoader.d.ts +5 -0
- package/dist/useDelayedLoader.js +33 -0
- package/dist/useQuery.d.ts +13 -0
- package/dist/useQuery.js +41 -0
- package/dist/utils.d.ts +4 -0
- package/dist/utils.js +83 -0
- package/package.json +12 -8
- package/src/Callgraph/Edge/index.tsx +59 -0
- package/src/Callgraph/Node/index.tsx +66 -0
- package/src/Callgraph/index.tsx +169 -0
- package/src/Callgraph/mockData/index.ts +605 -0
- package/src/Callgraph/utils.ts +116 -0
- package/src/GraphTooltip/index.tsx +245 -0
- package/src/IcicleGraph.tsx +3 -3
- package/src/MatchersInput/index.tsx +698 -0
- package/src/MetricsCircle/index.tsx +28 -0
- package/src/MetricsGraph/index.tsx +614 -0
- package/src/MetricsSeries/index.tsx +38 -0
- package/src/ProfileExplorer/ProfileExplorerCompare.tsx +109 -0
- package/src/ProfileExplorer/ProfileExplorerSingle.tsx +72 -0
- package/src/ProfileExplorer/index.tsx +377 -0
- package/src/ProfileMetricsGraph/index.tsx +143 -0
- package/src/ProfileSelector/CompareButton.tsx +72 -0
- package/src/ProfileSelector/MergeButton.tsx +72 -0
- package/src/ProfileSelector/index.tsx +270 -0
- package/src/ProfileTypeSelector/index.tsx +180 -0
- package/src/ProfileView.tsx +2 -7
- package/src/index.tsx +11 -0
- package/src/useQuery.tsx +1 -0
- package/tailwind.config.js +8 -0
- package/tsconfig.json +7 -3
- package/typings.d.ts +14 -0
|
@@ -0,0 +1,28 @@
|
|
|
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
|
+
interface MetricsCircleProps {
|
|
15
|
+
cx: number;
|
|
16
|
+
cy: number;
|
|
17
|
+
radius?: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const defaultRadius = 3;
|
|
21
|
+
|
|
22
|
+
const MetricsCircle = ({cx, cy, radius}: MetricsCircleProps): JSX.Element => (
|
|
23
|
+
<g className="circle">
|
|
24
|
+
<circle cx={cx} cy={cy} r={radius !== undefined ? radius : defaultRadius}></circle>
|
|
25
|
+
</g>
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
export default MetricsCircle;
|
|
@@ -0,0 +1,614 @@
|
|
|
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 React, {useEffect, useRef, useState} from 'react';
|
|
15
|
+
import * as d3 from 'd3';
|
|
16
|
+
import MetricsSeries from '../MetricsSeries';
|
|
17
|
+
import MetricsCircle from '../MetricsCircle';
|
|
18
|
+
import {pointer} from 'd3-selection';
|
|
19
|
+
import {formatForTimespan} from '@parca/functions/time';
|
|
20
|
+
import {SingleProfileSelection, timeFormat} from '..';
|
|
21
|
+
import {cutToMaxStringLength} from '@parca/functions/string';
|
|
22
|
+
import throttle from 'lodash.throttle';
|
|
23
|
+
import {MetricsSeries as MetricsSeriesPb, MetricsSample, Label} from '@parca/client';
|
|
24
|
+
import {usePopper} from 'react-popper';
|
|
25
|
+
import type {VirtualElement} from '@popperjs/core';
|
|
26
|
+
import {valueFormatter, formatDate} from '@parca/functions';
|
|
27
|
+
import {DateTimeRange} from '@parca/components';
|
|
28
|
+
import {useContainerDimensions} from '@parca/dynamicsize';
|
|
29
|
+
|
|
30
|
+
interface RawMetricsGraphProps {
|
|
31
|
+
data: MetricsSeriesPb[];
|
|
32
|
+
from: number;
|
|
33
|
+
to: number;
|
|
34
|
+
profile: SingleProfileSelection | null;
|
|
35
|
+
onSampleClick: (timestamp: number, value: number, labels: Label[]) => void;
|
|
36
|
+
onLabelClick: (labelName: string, labelValue: string) => void;
|
|
37
|
+
setTimeRange: (range: DateTimeRange) => void;
|
|
38
|
+
sampleUnit: string;
|
|
39
|
+
width?: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface HighlightedSeries {
|
|
43
|
+
seriesIndex: number;
|
|
44
|
+
labels: Label[];
|
|
45
|
+
timestamp: number;
|
|
46
|
+
value: number;
|
|
47
|
+
x: number;
|
|
48
|
+
y: number;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface Series {
|
|
52
|
+
metric: Label[];
|
|
53
|
+
values: number[][];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const MetricsGraph = ({
|
|
57
|
+
data,
|
|
58
|
+
from,
|
|
59
|
+
to,
|
|
60
|
+
profile,
|
|
61
|
+
onSampleClick,
|
|
62
|
+
onLabelClick,
|
|
63
|
+
setTimeRange,
|
|
64
|
+
sampleUnit,
|
|
65
|
+
}: RawMetricsGraphProps): JSX.Element => {
|
|
66
|
+
const {ref, dimensions} = useContainerDimensions();
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<div ref={ref}>
|
|
70
|
+
<RawMetricsGraph
|
|
71
|
+
data={data}
|
|
72
|
+
from={from}
|
|
73
|
+
to={to}
|
|
74
|
+
profile={profile}
|
|
75
|
+
onSampleClick={onSampleClick}
|
|
76
|
+
onLabelClick={onLabelClick}
|
|
77
|
+
setTimeRange={setTimeRange}
|
|
78
|
+
sampleUnit={sampleUnit}
|
|
79
|
+
width={dimensions?.width}
|
|
80
|
+
/>
|
|
81
|
+
</div>
|
|
82
|
+
);
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export default MetricsGraph;
|
|
86
|
+
|
|
87
|
+
export const parseValue = (value: string): number | null => {
|
|
88
|
+
const val = parseFloat(value);
|
|
89
|
+
// "+Inf", "-Inf", "+Inf" will be parsed into NaN by parseFloat(). They
|
|
90
|
+
// can't be graphed, so show them as gaps (null).
|
|
91
|
+
return isNaN(val) ? null : val;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const lineStroke = '1px';
|
|
95
|
+
const lineStrokeHover = '2px';
|
|
96
|
+
|
|
97
|
+
interface MetricsTooltipProps {
|
|
98
|
+
x: number;
|
|
99
|
+
y: number;
|
|
100
|
+
highlighted: HighlightedSeries;
|
|
101
|
+
onLabelClick: (labelName: string, labelValue: string) => void;
|
|
102
|
+
contextElement: Element | null;
|
|
103
|
+
sampleUnit: string;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function generateGetBoundingClientRect(contextElement: Element, x = 0, y = 0): () => DOMRect {
|
|
107
|
+
const domRect = contextElement.getBoundingClientRect();
|
|
108
|
+
return () =>
|
|
109
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
110
|
+
({
|
|
111
|
+
width: 0,
|
|
112
|
+
height: 0,
|
|
113
|
+
top: domRect.y + y,
|
|
114
|
+
left: domRect.x + x,
|
|
115
|
+
right: domRect.x + x,
|
|
116
|
+
bottom: domRect.y + y,
|
|
117
|
+
} as DOMRect);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const virtualElement: VirtualElement = {
|
|
121
|
+
getBoundingClientRect: () => {
|
|
122
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
123
|
+
return {
|
|
124
|
+
width: 0,
|
|
125
|
+
height: 0,
|
|
126
|
+
top: 0,
|
|
127
|
+
left: 0,
|
|
128
|
+
right: 0,
|
|
129
|
+
bottom: 0,
|
|
130
|
+
} as DOMRect;
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
export const MetricsTooltip = ({
|
|
135
|
+
x,
|
|
136
|
+
y,
|
|
137
|
+
highlighted,
|
|
138
|
+
onLabelClick,
|
|
139
|
+
contextElement,
|
|
140
|
+
sampleUnit,
|
|
141
|
+
}: MetricsTooltipProps): JSX.Element => {
|
|
142
|
+
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
|
143
|
+
|
|
144
|
+
const {styles, attributes, ...popperProps} = usePopper(virtualElement, popperElement, {
|
|
145
|
+
placement: 'auto-start',
|
|
146
|
+
strategy: 'absolute',
|
|
147
|
+
modifiers: [
|
|
148
|
+
{
|
|
149
|
+
name: 'preventOverflow',
|
|
150
|
+
options: {
|
|
151
|
+
tether: false,
|
|
152
|
+
altAxis: true,
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
name: 'offset',
|
|
157
|
+
options: {
|
|
158
|
+
offset: [30, 30],
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
],
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
const update = popperProps.update;
|
|
165
|
+
|
|
166
|
+
useEffect(() => {
|
|
167
|
+
if (contextElement != null) {
|
|
168
|
+
virtualElement.getBoundingClientRect = generateGetBoundingClientRect(contextElement, x, y);
|
|
169
|
+
void update?.();
|
|
170
|
+
}
|
|
171
|
+
}, [x, y, contextElement, update]);
|
|
172
|
+
|
|
173
|
+
const nameLabel: Label | undefined = highlighted?.labels.find(e => e.name === '__name__');
|
|
174
|
+
const highlightedNameLabel: Label = nameLabel !== undefined ? nameLabel : {name: '', value: ''};
|
|
175
|
+
|
|
176
|
+
return (
|
|
177
|
+
<div ref={setPopperElement} style={styles.popper} {...attributes.popper} className="z-10">
|
|
178
|
+
<div className="flex max-w-md">
|
|
179
|
+
<div className="m-auto">
|
|
180
|
+
<div
|
|
181
|
+
className="border-gray-300 dark:border-gray-500 bg-gray-50 dark:bg-gray-900 rounded-lg p-3 shadow-lg opacity-90"
|
|
182
|
+
style={{borderWidth: 1}}
|
|
183
|
+
>
|
|
184
|
+
<div className="flex flex-row">
|
|
185
|
+
<div className="ml-2 mr-6">
|
|
186
|
+
<span className="font-semibold">{highlightedNameLabel.value}</span>
|
|
187
|
+
<span className="block text-gray-700 dark:text-gray-300 my-2">
|
|
188
|
+
<table className="table-auto">
|
|
189
|
+
<tbody>
|
|
190
|
+
<tr>
|
|
191
|
+
<td className="w-1/4">Value</td>
|
|
192
|
+
<td className="w-3/4">
|
|
193
|
+
{valueFormatter(highlighted.value, sampleUnit, 1)}
|
|
194
|
+
</td>
|
|
195
|
+
</tr>
|
|
196
|
+
<tr>
|
|
197
|
+
<td className="w-1/4">At</td>
|
|
198
|
+
<td className="w-3/4">{formatDate(highlighted.timestamp, timeFormat)}</td>
|
|
199
|
+
</tr>
|
|
200
|
+
</tbody>
|
|
201
|
+
</table>
|
|
202
|
+
</span>
|
|
203
|
+
<span className="block text-gray-500 my-2">
|
|
204
|
+
{highlighted.labels
|
|
205
|
+
.filter((label: Label) => label.name !== '__name__')
|
|
206
|
+
.map(function (label: Label) {
|
|
207
|
+
return (
|
|
208
|
+
<button
|
|
209
|
+
key={label.name}
|
|
210
|
+
type="button"
|
|
211
|
+
className="inline-block rounded-lg text-gray-700 bg-gray-200 dark:bg-gray-700 dark:text-gray-400 px-2 py-1 text-xs font-bold mr-3"
|
|
212
|
+
onClick={() => onLabelClick(label.name, label.value)}
|
|
213
|
+
>
|
|
214
|
+
{cutToMaxStringLength(`${label.name}="${label.value}"`, 37)}
|
|
215
|
+
</button>
|
|
216
|
+
);
|
|
217
|
+
})}
|
|
218
|
+
</span>
|
|
219
|
+
<span className="block text-gray-500 text-xs">
|
|
220
|
+
Hold shift and click label to add to query.
|
|
221
|
+
</span>
|
|
222
|
+
</div>
|
|
223
|
+
</div>
|
|
224
|
+
</div>
|
|
225
|
+
</div>
|
|
226
|
+
</div>
|
|
227
|
+
</div>
|
|
228
|
+
);
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
export const RawMetricsGraph = ({
|
|
232
|
+
data,
|
|
233
|
+
from,
|
|
234
|
+
to,
|
|
235
|
+
profile,
|
|
236
|
+
onSampleClick,
|
|
237
|
+
onLabelClick,
|
|
238
|
+
setTimeRange,
|
|
239
|
+
width,
|
|
240
|
+
sampleUnit,
|
|
241
|
+
}: RawMetricsGraphProps): JSX.Element => {
|
|
242
|
+
const graph = useRef(null);
|
|
243
|
+
const [dragging, setDragging] = useState(false);
|
|
244
|
+
const [hovering, setHovering] = useState(false);
|
|
245
|
+
const [relPos, setRelPos] = useState(-1);
|
|
246
|
+
const [pos, setPos] = useState([0, 0]);
|
|
247
|
+
const [freezeTooltip, setFreezeTooltip] = useState(false);
|
|
248
|
+
const metricPointRef = useRef(null);
|
|
249
|
+
|
|
250
|
+
useEffect(() => {
|
|
251
|
+
const handleShiftDown = (event: {keyCode: number}): void => {
|
|
252
|
+
if (event.keyCode === 16) {
|
|
253
|
+
setFreezeTooltip(true);
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
window.addEventListener('keydown', handleShiftDown);
|
|
257
|
+
|
|
258
|
+
return () => {
|
|
259
|
+
window.removeEventListener('keydown', handleShiftDown);
|
|
260
|
+
};
|
|
261
|
+
}, []);
|
|
262
|
+
|
|
263
|
+
useEffect(() => {
|
|
264
|
+
const handleShiftUp = (event: {keyCode: number}): void => {
|
|
265
|
+
if (event.keyCode === 16) {
|
|
266
|
+
setFreezeTooltip(false);
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
window.addEventListener('keyup', handleShiftUp);
|
|
270
|
+
|
|
271
|
+
return () => {
|
|
272
|
+
window.removeEventListener('keyup', handleShiftUp);
|
|
273
|
+
};
|
|
274
|
+
}, []);
|
|
275
|
+
|
|
276
|
+
const time: number = parseFloat(profile?.HistoryParams().time);
|
|
277
|
+
|
|
278
|
+
if (width === undefined || width == null) {
|
|
279
|
+
width = 0;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const height = Math.min(width / 2.5, 400);
|
|
283
|
+
const margin = 50;
|
|
284
|
+
const marginRight = 20;
|
|
285
|
+
|
|
286
|
+
const series: Series[] = data.reduce<Series[]>(function (agg: Series[], s: MetricsSeriesPb) {
|
|
287
|
+
if (s.labelset !== undefined) {
|
|
288
|
+
agg.push({
|
|
289
|
+
metric: s.labelset.labels,
|
|
290
|
+
values: s.samples.reduce<number[][]>(function (agg: number[][], d: MetricsSample) {
|
|
291
|
+
if (d.timestamp !== undefined && d.value !== undefined) {
|
|
292
|
+
const t = (+d.timestamp.seconds * 1e9 + d.timestamp.nanos) / 1e6; // https://github.com/microsoft/TypeScript/issues/5710#issuecomment-157886246
|
|
293
|
+
agg.push([t, parseFloat(d.value)]);
|
|
294
|
+
}
|
|
295
|
+
return agg;
|
|
296
|
+
}, []),
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
return agg;
|
|
300
|
+
}, []);
|
|
301
|
+
|
|
302
|
+
const extentsY = series.map(function (s) {
|
|
303
|
+
return d3.extent(s.values, function (d) {
|
|
304
|
+
return d[1];
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
const minY = d3.min(extentsY, function (d) {
|
|
309
|
+
return d[0];
|
|
310
|
+
});
|
|
311
|
+
const maxY = d3.max(extentsY, function (d) {
|
|
312
|
+
return d[1];
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
/* Scale */
|
|
316
|
+
const xScale = d3
|
|
317
|
+
.scaleUtc()
|
|
318
|
+
.domain([from, to])
|
|
319
|
+
.range([0, width - margin - marginRight]);
|
|
320
|
+
|
|
321
|
+
const yScale = d3
|
|
322
|
+
.scaleLinear()
|
|
323
|
+
// tslint:disable-next-line
|
|
324
|
+
.domain([minY, maxY] as Iterable<d3.NumberValue>)
|
|
325
|
+
.range([height - margin, 0]);
|
|
326
|
+
|
|
327
|
+
const color = d3.scaleOrdinal(d3.schemeCategory10);
|
|
328
|
+
|
|
329
|
+
const l = d3.line(
|
|
330
|
+
d => xScale(d[0]),
|
|
331
|
+
d => yScale(d[1])
|
|
332
|
+
);
|
|
333
|
+
|
|
334
|
+
const getClosest = (): HighlightedSeries | null => {
|
|
335
|
+
const closestPointPerSeries = series.map(function (s) {
|
|
336
|
+
const distances = s.values.map(d => {
|
|
337
|
+
const x = xScale(d[0]);
|
|
338
|
+
const y = yScale(d[1]);
|
|
339
|
+
|
|
340
|
+
return Math.sqrt(Math.pow(pos[0] - x, 2) + Math.pow(pos[1] - y, 2));
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
const pointIndex = d3.minIndex(distances);
|
|
344
|
+
const minDistance = distances[pointIndex];
|
|
345
|
+
return {
|
|
346
|
+
pointIndex,
|
|
347
|
+
distance: minDistance,
|
|
348
|
+
};
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
const closestSeriesIndex = d3.minIndex(closestPointPerSeries, s => s.distance);
|
|
352
|
+
const pointIndex = closestPointPerSeries[closestSeriesIndex].pointIndex;
|
|
353
|
+
const point = series[closestSeriesIndex].values[pointIndex];
|
|
354
|
+
|
|
355
|
+
return {
|
|
356
|
+
seriesIndex: closestSeriesIndex,
|
|
357
|
+
labels: series[closestSeriesIndex].metric,
|
|
358
|
+
timestamp: point[0],
|
|
359
|
+
value: point[1],
|
|
360
|
+
x: xScale(point[0]),
|
|
361
|
+
y: yScale(point[1]),
|
|
362
|
+
};
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
const highlighted = getClosest();
|
|
366
|
+
|
|
367
|
+
const onMouseDown = (e: React.MouseEvent<SVGSVGElement | HTMLDivElement, MouseEvent>): void => {
|
|
368
|
+
// only left mouse button
|
|
369
|
+
if (e.button !== 0) {
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// X/Y coordinate array relative to svg
|
|
374
|
+
const rel = pointer(e);
|
|
375
|
+
|
|
376
|
+
const xCoordinate = rel[0];
|
|
377
|
+
const xCoordinateWithoutMargin = xCoordinate - margin;
|
|
378
|
+
if (xCoordinateWithoutMargin >= 0) {
|
|
379
|
+
setRelPos(xCoordinateWithoutMargin);
|
|
380
|
+
setDragging(true);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
e.stopPropagation();
|
|
384
|
+
e.preventDefault();
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
const openClosestProfile = (): void => {
|
|
388
|
+
if (highlighted != null) {
|
|
389
|
+
onSampleClick(Math.round(highlighted.timestamp), highlighted.value, highlighted.labels);
|
|
390
|
+
}
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
const onMouseUp = (e: React.MouseEvent<SVGSVGElement | HTMLDivElement, MouseEvent>): void => {
|
|
394
|
+
setDragging(false);
|
|
395
|
+
|
|
396
|
+
if (relPos === -1) {
|
|
397
|
+
// MouseDown happened outside of this element.
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// This is a normal click. We tolerate tiny movements to still be a
|
|
402
|
+
// click as they can occur when clicking based on user feedback.
|
|
403
|
+
if (Math.abs(relPos - pos[0]) <= 1) {
|
|
404
|
+
openClosestProfile();
|
|
405
|
+
setRelPos(-1);
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const firstTime = xScale.invert(relPos).valueOf();
|
|
410
|
+
const secondTime = xScale.invert(pos[0]).valueOf();
|
|
411
|
+
|
|
412
|
+
if (firstTime > secondTime) {
|
|
413
|
+
setTimeRange(DateTimeRange.fromAbsoluteDates(secondTime, firstTime));
|
|
414
|
+
} else {
|
|
415
|
+
setTimeRange(DateTimeRange.fromAbsoluteDates(firstTime, secondTime));
|
|
416
|
+
}
|
|
417
|
+
setRelPos(-1);
|
|
418
|
+
|
|
419
|
+
e.stopPropagation();
|
|
420
|
+
e.preventDefault();
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
const throttledSetPos = throttle(setPos, 20);
|
|
424
|
+
|
|
425
|
+
const onMouseMove = (e: React.MouseEvent<SVGSVGElement | HTMLDivElement, MouseEvent>): void => {
|
|
426
|
+
// X/Y coordinate array relative to svg
|
|
427
|
+
const rel = pointer(e);
|
|
428
|
+
|
|
429
|
+
const xCoordinate = rel[0];
|
|
430
|
+
const xCoordinateWithoutMargin = xCoordinate - margin;
|
|
431
|
+
const yCoordinate = rel[1];
|
|
432
|
+
const yCoordinateWithoutMargin = yCoordinate - margin;
|
|
433
|
+
|
|
434
|
+
if (!freezeTooltip) {
|
|
435
|
+
throttledSetPos([xCoordinateWithoutMargin, yCoordinateWithoutMargin]);
|
|
436
|
+
}
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
const findSelectedProfile = (): HighlightedSeries | null => {
|
|
440
|
+
if (profile == null) {
|
|
441
|
+
return null;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
let s: Series | null = null;
|
|
445
|
+
let seriesIndex: number = -1;
|
|
446
|
+
|
|
447
|
+
outer: for (let i = 0; i < series.length; i++) {
|
|
448
|
+
const keys = profile.labels.map(e => e.name);
|
|
449
|
+
for (let j = 0; j < keys.length; j++) {
|
|
450
|
+
const labelName = keys[j];
|
|
451
|
+
const label = series[i].metric.find(e => e.name === labelName);
|
|
452
|
+
if (label === undefined) {
|
|
453
|
+
continue outer; // label doesn't exist to begin with
|
|
454
|
+
}
|
|
455
|
+
if (profile.labels[j].value !== label.value) {
|
|
456
|
+
continue outer; // label values don't match
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
seriesIndex = i;
|
|
460
|
+
s = series[i];
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
if (s == null) {
|
|
464
|
+
return null;
|
|
465
|
+
}
|
|
466
|
+
// Find the sample that matches the timestamp
|
|
467
|
+
const sample = s.values.find(v => {
|
|
468
|
+
return Math.round(v[0]) === time;
|
|
469
|
+
});
|
|
470
|
+
if (sample === undefined) {
|
|
471
|
+
return null;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
return {
|
|
475
|
+
labels: [],
|
|
476
|
+
seriesIndex,
|
|
477
|
+
timestamp: sample[0],
|
|
478
|
+
value: sample[1],
|
|
479
|
+
x: xScale(sample[0]),
|
|
480
|
+
y: yScale(sample[1]),
|
|
481
|
+
};
|
|
482
|
+
};
|
|
483
|
+
|
|
484
|
+
const selected = findSelectedProfile();
|
|
485
|
+
|
|
486
|
+
return (
|
|
487
|
+
<>
|
|
488
|
+
{highlighted != null && hovering && !dragging && pos[0] !== 0 && pos[1] !== 0 && (
|
|
489
|
+
<div
|
|
490
|
+
onMouseMove={onMouseMove}
|
|
491
|
+
onMouseEnter={() => setHovering(true)}
|
|
492
|
+
onMouseLeave={() => setHovering(false)}
|
|
493
|
+
>
|
|
494
|
+
<MetricsTooltip
|
|
495
|
+
x={pos[0] + margin}
|
|
496
|
+
y={pos[1] + margin}
|
|
497
|
+
highlighted={highlighted}
|
|
498
|
+
onLabelClick={onLabelClick}
|
|
499
|
+
contextElement={graph.current}
|
|
500
|
+
sampleUnit={sampleUnit}
|
|
501
|
+
/>
|
|
502
|
+
</div>
|
|
503
|
+
)}
|
|
504
|
+
<div
|
|
505
|
+
ref={graph}
|
|
506
|
+
onMouseEnter={function () {
|
|
507
|
+
setHovering(true);
|
|
508
|
+
setFreezeTooltip(false);
|
|
509
|
+
}}
|
|
510
|
+
onMouseLeave={() => setHovering(false)}
|
|
511
|
+
>
|
|
512
|
+
<svg
|
|
513
|
+
width={`${width}px`}
|
|
514
|
+
height={`${height + margin}px`}
|
|
515
|
+
onMouseDown={onMouseDown}
|
|
516
|
+
onMouseUp={onMouseUp}
|
|
517
|
+
onMouseMove={onMouseMove}
|
|
518
|
+
>
|
|
519
|
+
<g transform={`translate(${margin}, 0)`}>
|
|
520
|
+
{dragging && (
|
|
521
|
+
<g className="zoom-time-rect">
|
|
522
|
+
<rect
|
|
523
|
+
className="bar"
|
|
524
|
+
x={pos[0] - relPos < 0 ? pos[0] : relPos}
|
|
525
|
+
y={0}
|
|
526
|
+
height={height}
|
|
527
|
+
width={Math.abs(pos[0] - relPos)}
|
|
528
|
+
fill={'rgba(0, 0, 0, 0.125)'}
|
|
529
|
+
/>
|
|
530
|
+
</g>
|
|
531
|
+
)}
|
|
532
|
+
</g>
|
|
533
|
+
<g transform={`translate(${margin}, ${margin})`}>
|
|
534
|
+
<g className="lines fill-transparent">
|
|
535
|
+
{series.map((s, i) => (
|
|
536
|
+
<g key={i} className="line">
|
|
537
|
+
<MetricsSeries
|
|
538
|
+
data={s}
|
|
539
|
+
line={l}
|
|
540
|
+
color={color(i.toString())}
|
|
541
|
+
strokeWidth={
|
|
542
|
+
hovering && highlighted != null && i === highlighted.seriesIndex
|
|
543
|
+
? lineStrokeHover
|
|
544
|
+
: lineStroke
|
|
545
|
+
}
|
|
546
|
+
xScale={xScale}
|
|
547
|
+
yScale={yScale}
|
|
548
|
+
/>
|
|
549
|
+
</g>
|
|
550
|
+
))}
|
|
551
|
+
</g>
|
|
552
|
+
{hovering && highlighted != null && (
|
|
553
|
+
<g
|
|
554
|
+
className="circle-group"
|
|
555
|
+
ref={metricPointRef}
|
|
556
|
+
style={{fill: color(highlighted.seriesIndex.toString())}}
|
|
557
|
+
>
|
|
558
|
+
<MetricsCircle cx={highlighted.x} cy={highlighted.y} />
|
|
559
|
+
</g>
|
|
560
|
+
)}
|
|
561
|
+
{selected != null && (
|
|
562
|
+
<g
|
|
563
|
+
className="circle-group"
|
|
564
|
+
style={
|
|
565
|
+
selected?.seriesIndex != null
|
|
566
|
+
? {fill: color(selected.seriesIndex.toString())}
|
|
567
|
+
: {}
|
|
568
|
+
}
|
|
569
|
+
>
|
|
570
|
+
<MetricsCircle cx={selected.x} cy={selected.y} radius={5} />
|
|
571
|
+
</g>
|
|
572
|
+
)}
|
|
573
|
+
<g
|
|
574
|
+
className="x axis"
|
|
575
|
+
fill="none"
|
|
576
|
+
fontSize="10"
|
|
577
|
+
textAnchor="middle"
|
|
578
|
+
transform={`translate(0,${height - margin})`}
|
|
579
|
+
>
|
|
580
|
+
{xScale.ticks(5).map((d, i) => (
|
|
581
|
+
<g
|
|
582
|
+
key={i}
|
|
583
|
+
className="tick"
|
|
584
|
+
/* eslint-disable-next-line @typescript-eslint/restrict-template-expressions */
|
|
585
|
+
transform={`translate(${xScale(d)}, 0)`}
|
|
586
|
+
>
|
|
587
|
+
<line y2={6} stroke="currentColor" />
|
|
588
|
+
<text fill="currentColor" dy=".71em" y={9}>
|
|
589
|
+
{formatDate(d, formatForTimespan(from, to))}
|
|
590
|
+
</text>
|
|
591
|
+
</g>
|
|
592
|
+
))}
|
|
593
|
+
</g>
|
|
594
|
+
<g className="y axis" textAnchor="end" fontSize="10" fill="none">
|
|
595
|
+
{yScale.ticks(3).map((d, i) => (
|
|
596
|
+
<g
|
|
597
|
+
key={i}
|
|
598
|
+
className="tick"
|
|
599
|
+
/* eslint-disable-next-line @typescript-eslint/restrict-template-expressions */
|
|
600
|
+
transform={`translate(0, ${yScale(d)})`}
|
|
601
|
+
>
|
|
602
|
+
<line stroke="currentColor" x2={-6} />
|
|
603
|
+
<text fill="currentColor" x={-9} dy={'0.32em'}>
|
|
604
|
+
{valueFormatter(d, sampleUnit, 1)}
|
|
605
|
+
</text>
|
|
606
|
+
</g>
|
|
607
|
+
))}
|
|
608
|
+
</g>
|
|
609
|
+
</g>
|
|
610
|
+
</svg>
|
|
611
|
+
</div>
|
|
612
|
+
</>
|
|
613
|
+
);
|
|
614
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
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 * as d3 from 'd3';
|
|
15
|
+
|
|
16
|
+
interface MetricsSeriesProps {
|
|
17
|
+
data: any;
|
|
18
|
+
line: d3.Line<[number, number]>;
|
|
19
|
+
color: string;
|
|
20
|
+
strokeWidth: string;
|
|
21
|
+
xScale: (input: number) => number;
|
|
22
|
+
yScale: (input: number) => number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const MetricsSeries = ({data, line, color, strokeWidth}: MetricsSeriesProps): JSX.Element => (
|
|
26
|
+
<g className="line-group">
|
|
27
|
+
<path
|
|
28
|
+
className="line"
|
|
29
|
+
d={line(data.values) ?? undefined}
|
|
30
|
+
style={{
|
|
31
|
+
stroke: color,
|
|
32
|
+
strokeWidth,
|
|
33
|
+
}}
|
|
34
|
+
/>
|
|
35
|
+
</g>
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
export default MetricsSeries;
|