@parca/profile 0.16.497 → 0.16.499

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 CHANGED
@@ -3,6 +3,14 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ ## [0.16.499](https://github.com/parca-dev/parca/compare/@parca/profile@0.16.498...@parca/profile@0.16.499) (2025-04-24)
7
+
8
+ **Note:** Version bump only for package @parca/profile
9
+
10
+ ## [0.16.498](https://github.com/parca-dev/parca/compare/@parca/profile@0.16.497...@parca/profile@0.16.498) (2025-04-24)
11
+
12
+ **Note:** Version bump only for package @parca/profile
13
+
6
14
  ## [0.16.497](https://github.com/parca-dev/parca/compare/@parca/profile@0.16.496...@parca/profile@0.16.497) (2025-04-24)
7
15
 
8
16
  **Note:** Version bump only for package @parca/profile
@@ -7,7 +7,8 @@ interface Props {
7
7
  sampleUnit: string;
8
8
  delta: boolean;
9
9
  utilizationMetrics?: boolean;
10
+ valuePrefix?: string;
10
11
  }
11
- declare const MetricsTooltip: ({ x, y, highlighted, contextElement, sampleUnit, delta, utilizationMetrics, }: Props) => JSX.Element;
12
+ declare const MetricsTooltip: ({ x, y, highlighted, contextElement, sampleUnit, delta, utilizationMetrics, valuePrefix, }: Props) => JSX.Element;
12
13
  export default MetricsTooltip;
13
14
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/MetricsGraph/MetricsTooltip/index.tsx"],"names":[],"mappings":"AAuBA,OAAO,EAAC,iBAAiB,EAAC,MAAM,KAAK,CAAC;AAEtC,UAAU,KAAK;IACb,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,WAAW,EAAE,iBAAiB,CAAC;IAC/B,cAAc,EAAE,OAAO,GAAG,IAAI,CAAC;IAC/B,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,OAAO,CAAC;IACf,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAC9B;AA8BD,QAAA,MAAM,cAAc,kFAQjB,KAAK,KAAG,GAAG,CAAC,OAoMd,CAAC;AAEF,eAAe,cAAc,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/MetricsGraph/MetricsTooltip/index.tsx"],"names":[],"mappings":"AAuBA,OAAO,EAAC,iBAAiB,EAAC,MAAM,KAAK,CAAC;AAEtC,UAAU,KAAK;IACb,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,WAAW,EAAE,iBAAiB,CAAC;IAC/B,cAAc,EAAE,OAAO,GAAG,IAAI,CAAC;IAC/B,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,OAAO,CAAC;IACf,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AA8BD,QAAA,MAAM,cAAc,+FASjB,KAAK,KAAG,GAAG,CAAC,OAuMd,CAAC;AAEF,eAAe,cAAc,CAAC"}
@@ -42,7 +42,7 @@ function generateGetBoundingClientRect(contextElement, x = 0, y = 0) {
42
42
  bottom: domRect.y + y,
43
43
  });
44
44
  }
45
- const MetricsTooltip = ({ x, y, highlighted, contextElement, sampleUnit, delta, utilizationMetrics = false, }) => {
45
+ const MetricsTooltip = ({ x, y, highlighted, contextElement, sampleUnit, delta, utilizationMetrics = false, valuePrefix, }) => {
46
46
  const { timezone } = useParcaContext();
47
47
  const [popperElement, setPopperElement] = useState(null);
48
48
  const { styles, attributes, ...popperProps } = usePopper(virtualElement, popperElement, {
@@ -91,7 +91,7 @@ const MetricsTooltip = ({ x, y, highlighted, contextElement, sampleUnit, delta,
91
91
  }, [x, y, contextElement, update]);
92
92
  const nameLabel = highlighted?.labels.find(e => e.name === '__name__');
93
93
  const highlightedNameLabel = nameLabel !== undefined ? nameLabel : { name: '', value: '' };
94
- return (_jsx("div", { ref: setPopperElement, style: styles.popper, ...attributes.popper, className: "z-50", children: _jsx("div", { className: "flex max-w-lg", children: _jsx("div", { className: "m-auto", children: _jsx("div", { className: "rounded-lg border-gray-300 bg-gray-50 p-3 opacity-90 shadow-lg dark:border-gray-500 dark:bg-gray-900", style: { borderWidth: 1 }, children: _jsx("div", { className: "flex flex-row", children: _jsxs("div", { className: "ml-2 mr-6", children: [_jsx("span", { className: "font-semibold", children: highlightedNameLabel.value }), _jsx("span", { className: "my-2 block text-gray-700 dark:text-gray-300", children: _jsx("table", { className: "table-auto", children: _jsxs("tbody", { children: [delta ? (_jsxs(_Fragment, { children: [_jsxs("tr", { children: [_jsx("td", { className: "w-1/4 pr-3", children: "Per\u00A0Second" }), _jsx("td", { className: "w-3/4", children: valueFormatter(highlighted.valuePerSecond, sampleUnit === 'nanoseconds' ? 'CPU Cores' : sampleUnit, 5) })] }), _jsxs("tr", { children: [_jsx("td", { className: "w-1/4", children: "Total" }), _jsx("td", { className: "w-3/4", children: valueFormatter(highlighted.value, sampleUnit, 2) })] })] })) : (_jsxs("tr", { children: [_jsx("td", { className: "w-1/4", children: "Value" }), _jsx("td", { className: "w-3/4", children: valueFormatter(highlighted.valuePerSecond, sampleUnit, 5) })] })), highlighted.duration > 0 && (_jsxs("tr", { children: [_jsx("td", { className: "w-1/4", children: "Duration" }), _jsx("td", { className: "w-3/4", children: valueFormatter(highlighted.duration, 'nanoseconds', 2) })] })), _jsxs("tr", { children: [_jsx("td", { className: "w-1/4", children: "At" }), _jsx("td", { className: "w-3/4", children: formatDate(highlighted.timestamp, timePattern(timezone), timezone) })] })] }) }) }), _jsx("span", { className: "my-2 block text-gray-500", children: utilizationMetrics ? (_jsxs(_Fragment, { children: [Object.keys(attributesResourceMap).length > 0 && (_jsx("span", { className: "text-sm font-bold text-gray-700 dark:text-white", children: "Resource Attributes" })), _jsx("span", { className: "my-2 block text-gray-500", children: Object.keys(attributesResourceMap).map(name => (_jsx("div", { className: "mr-3 inline-block rounded-lg bg-gray-200 px-2 py-1 text-xs font-bold text-gray-700 dark:bg-gray-700 dark:text-gray-400", children: _jsx(TextWithTooltip, { text: `${name.replace('attributes.', '')}="${attributesResourceMap[name]}"`, maxTextLength: 48, id: `tooltip-${name}-${attributesResourceMap[name]}` }) }, name))) }), Object.keys(attributesMap).length > 0 && (_jsx("span", { className: "text-sm font-bold text-gray-700 dark:text-white", children: "Attributes" })), _jsx("span", { className: "my-2 block text-gray-500", children: Object.keys(attributesMap).map(name => (_jsx("div", { className: "mr-3 inline-block rounded-lg bg-gray-200 px-2 py-1 text-xs font-bold text-gray-700 dark:bg-gray-700 dark:text-gray-400", children: _jsx(TextWithTooltip, { text: `${name.replace('attributes.', '')}="${attributesMap[name]}"`, maxTextLength: 48, id: `tooltip-${name}-${attributesMap[name]}` }) }, name))) })] })) : (_jsx(_Fragment, { children: highlighted.labels
94
+ return (_jsx("div", { ref: setPopperElement, style: styles.popper, ...attributes.popper, className: "z-50", children: _jsx("div", { className: "flex max-w-lg", children: _jsx("div", { className: "m-auto", children: _jsx("div", { className: "rounded-lg border-gray-300 bg-gray-50 p-3 opacity-90 shadow-lg dark:border-gray-500 dark:bg-gray-900", style: { borderWidth: 1 }, children: _jsx("div", { className: "flex flex-row", children: _jsxs("div", { className: "ml-2 mr-6", children: [_jsx("span", { className: "font-semibold", children: highlightedNameLabel.value }), _jsx("span", { className: "my-2 block text-gray-700 dark:text-gray-300", children: _jsx("table", { className: "table-auto", children: _jsxs("tbody", { children: [delta ? (_jsxs(_Fragment, { children: [_jsxs("tr", { children: [_jsx("td", { className: "w-1/4 pr-3", children: "Per\u00A0Second" }), _jsx("td", { className: "w-3/4", children: valueFormatter(highlighted.valuePerSecond, sampleUnit === 'nanoseconds' ? 'CPU Cores' : sampleUnit, 5) })] }), _jsxs("tr", { children: [_jsx("td", { className: "w-1/4", children: "Total" }), _jsx("td", { className: "w-3/4", children: valueFormatter(highlighted.value, sampleUnit, 2) })] })] })) : (_jsxs("tr", { children: [_jsxs("td", { className: "w-1/4", children: [valuePrefix ?? '', "Value"] }), _jsx("td", { className: "w-3/4", children: valueFormatter(highlighted.valuePerSecond, sampleUnit, 5) })] })), highlighted.duration > 0 && (_jsxs("tr", { children: [_jsx("td", { className: "w-1/4", children: "Duration" }), _jsx("td", { className: "w-3/4", children: valueFormatter(highlighted.duration, 'nanoseconds', 2) })] })), _jsxs("tr", { children: [_jsx("td", { className: "w-1/4", children: "At" }), _jsx("td", { className: "w-3/4", children: formatDate(highlighted.timestamp, timePattern(timezone), timezone) })] })] }) }) }), _jsx("span", { className: "my-2 block text-gray-500", children: utilizationMetrics ? (_jsxs(_Fragment, { children: [Object.keys(attributesResourceMap).length > 0 && (_jsx("span", { className: "text-sm font-bold text-gray-700 dark:text-white", children: "Resource Attributes" })), _jsx("span", { className: "my-2 block text-gray-500", children: Object.keys(attributesResourceMap).map(name => (_jsx("div", { className: "mr-3 inline-block rounded-lg bg-gray-200 px-2 py-1 text-xs font-bold text-gray-700 dark:bg-gray-700 dark:text-gray-400", children: _jsx(TextWithTooltip, { text: `${name.replace('attributes.', '')}="${attributesResourceMap[name]}"`, maxTextLength: 48, id: `tooltip-${name}-${attributesResourceMap[name]}` }) }, name))) }), Object.keys(attributesMap).length > 0 && (_jsx("span", { className: "text-sm font-bold text-gray-700 dark:text-white", children: "Attributes" })), _jsx("span", { className: "my-2 block text-gray-500", children: Object.keys(attributesMap).map(name => (_jsx("div", { className: "mr-3 inline-block rounded-lg bg-gray-200 px-2 py-1 text-xs font-bold text-gray-700 dark:bg-gray-700 dark:text-gray-400", children: _jsx(TextWithTooltip, { text: `${name.replace('attributes.', '')}="${attributesMap[name]}"`, maxTextLength: 48, id: `tooltip-${name}-${attributesMap[name]}` }) }, name))) })] })) : (_jsx(_Fragment, { children: highlighted.labels
95
95
  .filter((label) => label.name !== '__name__')
96
96
  .map((label) => (_jsx("div", { className: "mr-3 inline-block rounded-lg bg-gray-200 px-2 py-1 text-xs font-bold text-gray-700 dark:bg-gray-700 dark:text-gray-400", children: _jsx(TextWithTooltip, { text: `${label.name}="${label.value}"`, maxTextLength: 37, id: `tooltip-${label.name}` }) }, label.name))) })) }), _jsxs("div", { className: "flex w-full items-center gap-1 text-xs text-gray-500", children: [_jsx(Icon, { icon: "iconoir:mouse-button-right" }), _jsx("div", { children: "Right click to add labels to query." })] })] }) }) }) }) }) }));
97
97
  };
@@ -0,0 +1,24 @@
1
+ import { DateTimeRange } from '@parca/components';
2
+ import { type UtilizationMetrics as MetricSeries } from '../../ProfileSelector';
3
+ interface CommonProps {
4
+ transmitData: MetricSeries[];
5
+ receiveData: MetricSeries[];
6
+ addLabelMatcher: (labels: {
7
+ key: string;
8
+ value: string;
9
+ } | Array<{
10
+ key: string;
11
+ value: string;
12
+ }>) => void;
13
+ setTimeRange: (range: DateTimeRange) => void;
14
+ name: string;
15
+ humanReadableName: string;
16
+ from: number;
17
+ to: number;
18
+ }
19
+ type Props = CommonProps & {
20
+ utilizationMetricsLoading?: boolean;
21
+ };
22
+ declare const AreaChart: ({ transmitData, receiveData, addLabelMatcher, setTimeRange, utilizationMetricsLoading, name, humanReadableName, from, to, }: Props) => JSX.Element;
23
+ export default AreaChart;
24
+ //# sourceMappingURL=AreaChart.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AreaChart.d.ts","sourceRoot":"","sources":["../../../src/MetricsGraph/UtilizationMetrics/AreaChart.tsx"],"names":[],"mappings":"AAqBA,OAAO,EAAC,aAAa,EAAqD,MAAM,mBAAmB,CAAC;AAIpG,OAAO,EAAC,KAAK,kBAAkB,IAAI,YAAY,EAAC,MAAM,uBAAuB,CAAC;AAiB9E,UAAU,WAAW;IACnB,YAAY,EAAE,YAAY,EAAE,CAAC;IAC7B,WAAW,EAAE,YAAY,EAAE,CAAC;IAC5B,eAAe,EAAE,CACf,MAAM,EAAE;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAC,GAAG,KAAK,CAAC;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAC,CAAC,KACvE,IAAI,CAAC;IACV,YAAY,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CAAC;IAC7C,IAAI,EAAE,MAAM,CAAC;IACb,iBAAiB,EAAE,MAAM,CAAC;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;CACZ;AAQD,KAAK,KAAK,GAAG,WAAW,GAAG;IACzB,yBAAyB,CAAC,EAAE,OAAO,CAAC;CACrC,CAAC;AAqfF,QAAA,MAAM,SAAS,gIAUZ,KAAK,KAAG,GAAG,CAAC,OAiCd,CAAC;AAEF,eAAe,SAAS,CAAC"}
@@ -0,0 +1,279 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ // Copyright 2022 The Parca Authors
3
+ // Licensed under the Apache License, Version 2.0 (the "License");
4
+ // you may not use this file except in compliance with the License.
5
+ // You may obtain a copy of the License at
6
+ //
7
+ // http://www.apache.org/licenses/LICENSE-2.0
8
+ //
9
+ // Unless required by applicable law or agreed to in writing, software
10
+ // distributed under the License is distributed on an "AS IS" BASIS,
11
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ // See the License for the specific language governing permissions and
13
+ // limitations under the License.
14
+ import { Fragment, useCallback, useId, useMemo, useRef, useState } from 'react';
15
+ import * as d3 from 'd3';
16
+ import { pointer } from 'd3-selection';
17
+ import { AnimatePresence, motion } from 'framer-motion';
18
+ import throttle from 'lodash.throttle';
19
+ import { useContextMenu } from 'react-contexify';
20
+ import { DateTimeRange, MetricsGraphSkeleton, useParcaContext, useURLState } from '@parca/components';
21
+ import { formatDate, formatForTimespan, getPrecision, valueFormatter } from '@parca/utilities';
22
+ import MetricsContextMenu from '../MetricsContextMenu';
23
+ import MetricsTooltip from '../MetricsTooltip';
24
+ import { useMetricsGraphDimensions } from '../useMetricsGraphDimensions';
25
+ function transformToSeries(data, isReceive = false) {
26
+ const series = data.reduce(function (agg, s) {
27
+ if (s.labelset !== undefined) {
28
+ const metric = s.labelset.labels.sort((a, b) => a.name.localeCompare(b.name));
29
+ agg.push({
30
+ metric,
31
+ values: s.samples.reduce(function (agg, d) {
32
+ if (d.timestamp !== undefined && d.value !== undefined) {
33
+ // Multiply receive values by -1 to display below zero
34
+ const value = isReceive ? -1 * d.value : d.value;
35
+ agg.push([d.timestamp, value]);
36
+ }
37
+ return agg;
38
+ }, []),
39
+ labelset: metric.map(m => `${m.name}=${m.value}`).join(','),
40
+ isReceive,
41
+ });
42
+ }
43
+ return agg;
44
+ }, []);
45
+ // Sort values by timestamp for each series
46
+ return series.map(series => ({
47
+ ...series,
48
+ values: series.values.sort((a, b) => a[0] - b[0]),
49
+ }));
50
+ }
51
+ const getYAxisUnit = (name) => {
52
+ switch (name) {
53
+ case 'gpu_utilization_percent':
54
+ return 'percent';
55
+ case 'gpu_memory_utilization_percent':
56
+ return 'percent';
57
+ case 'gpu_power_watt':
58
+ return 'watts';
59
+ case 'gpu_pcie_throughput_transmit_bytes':
60
+ return 'bytes_per_second';
61
+ case 'gpu_pcie_throughput_receive_bytes':
62
+ return 'bytes_per_second';
63
+ default:
64
+ return 'percent';
65
+ }
66
+ };
67
+ const RawAreaChart = ({ transmitData, receiveData, addLabelMatcher, setTimeRange, width, height, margin, name, humanReadableName, from, to, }) => {
68
+ const { timezone } = useParcaContext();
69
+ const graph = useRef(null);
70
+ const [dragging, setDragging] = useState(false);
71
+ const [hovering, setHovering] = useState(false);
72
+ const [relPos, setRelPos] = useState(-1);
73
+ const [pos, setPos] = useState([0, 0]);
74
+ const [isContextMenuOpen, setIsContextMenuOpen] = useState(false);
75
+ const idForContextMenu = useId();
76
+ const [selectedSeries, setSelectedSeries] = useURLState('selectedSeries');
77
+ const [_, setSelectedTimeframe] = useURLState('gpu_selected_timeframe');
78
+ const parsedSelectedSeries = useMemo(() => {
79
+ if (selectedSeries === undefined) {
80
+ return [];
81
+ }
82
+ return JSON.parse(decodeURIComponent(selectedSeries));
83
+ }, [selectedSeries]);
84
+ const lineStroke = '1px';
85
+ const lineStrokeHover = '2px';
86
+ const lineStrokeSelected = '3px';
87
+ const graphWidth = width - margin * 1.5 - margin / 2;
88
+ const paddedFrom = from;
89
+ const paddedTo = to;
90
+ const series = useMemo(() => {
91
+ const transmitSeries = transformToSeries(transmitData);
92
+ const receiveSeries = transformToSeries(receiveData, true);
93
+ return [...transmitSeries, ...receiveSeries];
94
+ }, [transmitData, receiveData]);
95
+ const extentsY = series.map(function (s) {
96
+ return d3.extent(s.values, function (d) {
97
+ return d[1];
98
+ });
99
+ });
100
+ const minY = d3.min(extentsY, function (d) {
101
+ return d[0];
102
+ });
103
+ const maxY = d3.max(extentsY, function (d) {
104
+ return d[1];
105
+ });
106
+ // Setup scales with padded time range
107
+ const xScale = d3.scaleUtc().domain([paddedFrom, paddedTo]).range([0, graphWidth]);
108
+ // Find the absolute maximum to ensure symmetric scale
109
+ const absMax = Math.max(Math.abs(minY ?? 0), Math.abs(maxY ?? 0));
110
+ const yScale = d3
111
+ .scaleLinear()
112
+ // Ensure domain is symmetric around 0 and includes all values
113
+ .domain([-absMax, absMax])
114
+ .range([height - margin, 0]);
115
+ const throttledSetPos = throttle(setPos, 20);
116
+ const onMouseMove = (e) => {
117
+ if (isContextMenuOpen) {
118
+ return;
119
+ }
120
+ // X/Y coordinate array relative to svg
121
+ const rel = pointer(e);
122
+ const xCoordinate = rel[0];
123
+ const xCoordinateWithoutMargin = xCoordinate - margin;
124
+ const yCoordinate = rel[1];
125
+ const yCoordinateWithoutMargin = yCoordinate - margin;
126
+ throttledSetPos([xCoordinateWithoutMargin, yCoordinateWithoutMargin]);
127
+ };
128
+ const trackVisibility = (isVisible) => {
129
+ setIsContextMenuOpen(isVisible);
130
+ };
131
+ const MENU_ID = `areachart-context-menu-${idForContextMenu}`;
132
+ const { show } = useContextMenu({
133
+ id: MENU_ID,
134
+ });
135
+ const displayMenu = useCallback((e) => {
136
+ show({
137
+ event: e,
138
+ });
139
+ }, [show]);
140
+ const getSeriesColor = (series) => {
141
+ return series.isReceive === true ? '#EAB308' : '#22C55E'; // Yellow for receive, Green for transmit
142
+ };
143
+ // Create area generator for transmit (above zero)
144
+ const transmitArea = d3
145
+ .area()
146
+ .x(d => xScale(d[0]))
147
+ .y0(yScale(0)) // Start from zero line
148
+ .y1(d => yScale(d[1])); // Top of the area (data point)
149
+ // Create area generator for receive (below zero)
150
+ const receiveArea = d3
151
+ .area()
152
+ .x(d => xScale(d[0]))
153
+ .y0(yScale(0)) // Start from zero line
154
+ .y1(d => yScale(d[1])); // Bottom of the area (negative data point)
155
+ const highlighted = useMemo(() => {
156
+ if (series.length === 0) {
157
+ return null;
158
+ }
159
+ // Return the closest point as the highlighted point
160
+ const closestPointPerSeries = series.map(function (s) {
161
+ const distances = s.values.map(d => {
162
+ const x = xScale(d[0]) + margin / 2;
163
+ const y = yScale(d[1]) - margin / 3;
164
+ return Math.sqrt(Math.pow(pos[0] - x, 2) + Math.pow(pos[1] - y, 2));
165
+ });
166
+ const pointIndex = d3.minIndex(distances);
167
+ const minDistance = distances[pointIndex];
168
+ return {
169
+ pointIndex,
170
+ distance: minDistance,
171
+ };
172
+ });
173
+ const closestSeriesIndex = d3.minIndex(closestPointPerSeries, s => s.distance);
174
+ const pointIndex = closestPointPerSeries[closestSeriesIndex].pointIndex;
175
+ const point = series[closestSeriesIndex].values[pointIndex];
176
+ return {
177
+ seriesIndex: closestSeriesIndex,
178
+ labels: series[closestSeriesIndex].metric,
179
+ timestamp: point[0],
180
+ valuePerSecond: point[1],
181
+ value: point[2],
182
+ duration: point[3],
183
+ x: xScale(point[0]),
184
+ y: yScale(point[1]),
185
+ };
186
+ }, [pos, series, xScale, yScale, margin]);
187
+ const onMouseDown = (e) => {
188
+ // only left mouse button
189
+ if (e.button !== 0) {
190
+ return;
191
+ }
192
+ // X/Y coordinate array relative to svg
193
+ const rel = pointer(e);
194
+ const xCoordinate = rel[0];
195
+ const xCoordinateWithoutMargin = xCoordinate - margin;
196
+ if (xCoordinateWithoutMargin >= 0) {
197
+ setRelPos(xCoordinateWithoutMargin);
198
+ setDragging(true);
199
+ }
200
+ e.stopPropagation();
201
+ e.preventDefault();
202
+ };
203
+ const onMouseUp = (e) => {
204
+ setDragging(false);
205
+ if (relPos === -1) {
206
+ // MouseDown happened outside of this element.
207
+ return;
208
+ }
209
+ // This is a normal click. We tolerate tiny movements to still be a
210
+ // click as they can occur when clicking based on user feedback.
211
+ if (Math.abs(relPos - pos[0]) <= 1) {
212
+ setRelPos(-1);
213
+ return;
214
+ }
215
+ let startPos = relPos;
216
+ let endPos = pos[0];
217
+ if (startPos > endPos) {
218
+ startPos = pos[0];
219
+ endPos = relPos;
220
+ }
221
+ const startCorrection = 10;
222
+ const endCorrection = 30;
223
+ const firstTime = xScale.invert(startPos - startCorrection).valueOf();
224
+ const secondTime = xScale.invert(endPos - endCorrection).valueOf();
225
+ setTimeRange(DateTimeRange.fromAbsoluteDates(firstTime, secondTime));
226
+ setRelPos(-1);
227
+ e.stopPropagation();
228
+ e.preventDefault();
229
+ };
230
+ return (_jsxs(_Fragment, { children: [_jsx(MetricsContextMenu, { onAddLabelMatcher: addLabelMatcher, menuId: MENU_ID, highlighted: highlighted, trackVisibility: trackVisibility, utilizationMetrics: true }), highlighted != null && hovering && !dragging && pos[0] !== 0 && pos[1] !== 0 && (_jsx("div", { onMouseMove: onMouseMove, onMouseEnter: () => setHovering(true), onMouseLeave: () => setHovering(false), children: !isContextMenuOpen && (_jsx(MetricsTooltip, { x: pos[0] + margin, y: pos[1] + margin, highlighted: {
231
+ ...highlighted,
232
+ valuePerSecond: Math.abs(highlighted.valuePerSecond),
233
+ }, contextElement: graph.current, sampleUnit: getYAxisUnit(name), delta: false, utilizationMetrics: true, valuePrefix: highlighted.seriesIndex >= transmitData.length ? 'Receive ' : 'Transmit ' })) })), _jsx("div", { ref: graph, onMouseEnter: () => setHovering(true), onMouseLeave: () => setHovering(false), onContextMenu: displayMenu, children: _jsxs("svg", { width: `${width}px`, height: `${height + margin}px`, onMouseDown: onMouseDown, onMouseUp: onMouseUp, onMouseMove: onMouseMove, children: [_jsx("g", { transform: `translate(${margin}, 0)`, children: dragging && (_jsx("g", { className: "zoom-time-rect", children: _jsx("rect", { className: "bar", x: pos[0] - relPos < 0 ? pos[0] : relPos, y: 0, height: height, width: Math.abs(pos[0] - relPos), fill: 'rgba(0, 0, 0, 0.125)' }) })) }), _jsxs("g", { transform: `translate(${margin * 1.5}, ${margin / 1.5})`, children: [_jsxs("g", { className: "y axis", textAnchor: "end", fontSize: "10", fill: "none", children: [yScale.ticks(6).map((d, i, allTicks) => {
234
+ let decimals = 2;
235
+ const intervalBetweenTicks = allTicks[1] - allTicks[0];
236
+ if (intervalBetweenTicks < 1) {
237
+ const precision = getPrecision(intervalBetweenTicks);
238
+ decimals = precision;
239
+ }
240
+ return (_jsxs(Fragment, { children: [_jsxs("g", { className: "tick", transform: `translate(0, ${yScale(d)})`, children: [_jsx("line", { className: "stroke-gray-300 dark:stroke-gray-500", x2: -6 }), _jsxs("text", { fill: "currentColor", x: -9, dy: '0.32em', children: [d < 0 ? '-' : '', valueFormatter(Math.abs(d), getYAxisUnit(name), decimals)] })] }, `tick-${i}`), _jsx("g", { children: _jsx("line", { className: "stroke-gray-300 dark:stroke-gray-500", x1: xScale(from), x2: xScale(to), y1: yScale(d), y2: yScale(d) }) }, `grid-${i}`)] }, `${i.toString()}-${d.toString()}`));
241
+ }), _jsx("line", { className: "stroke-gray-300 dark:stroke-gray-500", x1: 0, x2: 0, y1: 0, y2: height - margin }), _jsx("line", { className: "stroke-gray-300 dark:stroke-gray-500", x1: xScale(to), x2: xScale(to), y1: 0, y2: height - margin }), _jsx("g", { transform: `translate(${-margin}, ${(height - margin) / 2}) rotate(270)`, children: _jsx("text", { fill: "currentColor", dy: "-0.7em", className: "text-sm capitalize", textAnchor: "middle", children: humanReadableName }) })] }), _jsxs("g", { className: "x axis", fill: "none", fontSize: "10", textAnchor: "middle", transform: `translate(0,${height - margin})`, children: [xScale.ticks(5).map((d, i) => (_jsxs(Fragment, { children: [_jsxs("g", { className: "tick",
242
+ /* eslint-disable-next-line @typescript-eslint/restrict-template-expressions */
243
+ transform: `translate(${xScale(d)}, 0)`, children: [_jsx("line", { y2: 6, className: "stroke-gray-300 dark:stroke-gray-500" }), _jsx("text", { fill: "currentColor", dy: ".71em", y: 9, children: formatDate(d, formatForTimespan(from, to), timezone) })] }, `tick-${i}`), _jsx("g", { children: _jsx("line", { className: "stroke-gray-300 dark:stroke-gray-500", x1: xScale(d), x2: xScale(d), y1: 0, y2: -height + margin }) }, `grid-${i}`)] }, `${i.toString()}-${d.toString()}`))), _jsx("line", { className: "stroke-gray-300 dark:stroke-gray-500", x1: 0, x2: graphWidth, y1: 0, y2: 0 }), _jsx("g", { transform: `translate(${(width - 2.5 * margin) / 2}, ${margin / 2})`, children: _jsx("text", { fill: "currentColor", dy: ".71em", y: 5, className: "text-sm", children: "Time" }) })] }), _jsx("g", { className: "areas", children: series.map((s, i) => {
244
+ let isSelected = false;
245
+ if (parsedSelectedSeries != null && parsedSelectedSeries.length > 0) {
246
+ isSelected = parsedSelectedSeries.every(m => {
247
+ for (let i = 0; i < s.metric.length; i++) {
248
+ if (s.metric[i].name === m.key && s.metric[i].value === m.value) {
249
+ return true;
250
+ }
251
+ }
252
+ return false;
253
+ });
254
+ }
255
+ const seriesColor = getSeriesColor(s);
256
+ const fillOpacity = isSelected ? 0.4 : 0.2;
257
+ const strokeOpacity = isSelected ? 1 : 0.8;
258
+ const areaGenerator = s.isReceive === true ? receiveArea : transmitArea;
259
+ return (_jsx("g", { className: "area cursor-pointer", children: _jsx("path", { d: areaGenerator(s.values) ?? '', fill: seriesColor, fillOpacity: fillOpacity, stroke: seriesColor, strokeWidth: isSelected
260
+ ? lineStrokeSelected
261
+ : hovering && highlighted != null && i === highlighted.seriesIndex
262
+ ? lineStrokeHover
263
+ : lineStroke, strokeOpacity: strokeOpacity, onClick: () => {
264
+ if (highlighted != null) {
265
+ setSelectedSeries(JSON.stringify(highlighted.labels.map(l => ({
266
+ key: l.name,
267
+ value: l.value,
268
+ }))));
269
+ setSelectedTimeframe(undefined);
270
+ }
271
+ } }) }, i));
272
+ }) })] })] }) })] }));
273
+ };
274
+ const AreaChart = ({ transmitData, receiveData, addLabelMatcher, setTimeRange, utilizationMetricsLoading, name, humanReadableName, from, to, }) => {
275
+ const { isDarkMode } = useParcaContext();
276
+ const { width, height, margin, heightStyle } = useMetricsGraphDimensions(false, true);
277
+ return (_jsx(AnimatePresence, { children: _jsx(motion.div, { className: "w-full relative", initial: { display: 'none', opacity: 0 }, animate: { display: 'block', opacity: 1 }, transition: { duration: 0.5 }, children: utilizationMetricsLoading === true ? (_jsx(MetricsGraphSkeleton, { heightStyle: heightStyle, isDarkMode: isDarkMode, isMini: true })) : (_jsx(RawAreaChart, { transmitData: transmitData, receiveData: receiveData, addLabelMatcher: addLabelMatcher, setTimeRange: setTimeRange, width: width, height: height, margin: margin, name: name, humanReadableName: humanReadableName, from: from, to: to })) }, "area-chart-graph-loaded") }));
278
+ };
279
+ export default AreaChart;
@@ -1 +1 @@
1
- {"version":3,"file":"MetricsGraphSection.d.ts","sourceRoot":"","sources":["../../src/ProfileSelector/MetricsGraphSection.tsx"],"names":[],"mappings":"AAeA,OAAO,EAAQ,kBAAkB,EAAC,MAAM,eAAe,CAAC;AACxD,OAAO,EAAC,aAAa,EAAC,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAC,KAAK,EAAC,MAAM,eAAe,CAAC;AAEpC,OAAO,EAAyB,gBAAgB,EAAC,MAAM,IAAI,CAAC;AAG5D,OAAO,EAAC,cAAc,EAAE,KAAK,kBAAkB,IAAI,sBAAsB,EAAC,MAAM,SAAS,CAAC;AAE1F,UAAU,wBAAwB;IAChC,gBAAgB,EAAE,OAAO,CAAC;IAC1B,gCAAgC,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;IAC3D,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,cAAc,CAAC;IAC/B,gBAAgB,EAAE,gBAAgB,GAAG,IAAI,CAAC;IAC1C,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;IACvB,mBAAmB,EAAE,OAAO,CAAC;IAC7B,WAAW,EAAE,kBAAkB,CAAC;IAChC,qBAAqB,EAAE,MAAM,CAAC;IAC9B,qBAAqB,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CAAC;IACtD,WAAW,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,CAAC;IAC7C,aAAa,EAAE,CAAC,MAAM,EAAE,gBAAgB,KAAK,IAAI,CAAC;IAClD,KAAK,EAAE,KAAK,CAAC;IACb,qBAAqB,EAAE,CAAC,eAAe,EAAE,MAAM,KAAK,IAAI,CAAC;IACzD,kBAAkB,EAAE,CAAC,QAAQ,CAAC,EAAE,OAAO,KAAK,IAAI,CAAC;IACjD,kBAAkB,CAAC,EAAE,KAAK,CAAC;QACzB,IAAI,EAAE,MAAM,CAAC;QACb,iBAAiB,EAAE,MAAM,CAAC;QAC1B,IAAI,EAAE,sBAAsB,EAAE,CAAC;KAChC,CAAC,CAAC;IACH,yBAAyB,CAAC,EAAE,OAAO,CAAC;CACrC;AAED,wBAAgB,mBAAmB,CAAC,EAClC,gBAAgB,EAChB,gCAAgC,EAChC,WAAW,EACX,cAAc,EACd,gBAAgB,EAChB,SAAS,EACT,KAAK,EACL,mBAAmB,EACnB,WAAW,EACX,qBAAqB,EACrB,qBAAqB,EACrB,WAAW,EACX,aAAa,EACb,KAAK,EACL,qBAAqB,EACrB,kBAAkB,EAClB,yBAAyB,GAC1B,EAAE,wBAAwB,GAAG,GAAG,CAAC,OAAO,CA2IxC"}
1
+ {"version":3,"file":"MetricsGraphSection.d.ts","sourceRoot":"","sources":["../../src/ProfileSelector/MetricsGraphSection.tsx"],"names":[],"mappings":"AAeA,OAAO,EAAQ,kBAAkB,EAAC,MAAM,eAAe,CAAC;AACxD,OAAO,EAAC,aAAa,EAAC,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAC,KAAK,EAAC,MAAM,eAAe,CAAC;AAEpC,OAAO,EAAyB,gBAAgB,EAAC,MAAM,IAAI,CAAC;AAI5D,OAAO,EAAC,cAAc,EAAE,KAAK,kBAAkB,IAAI,sBAAsB,EAAC,MAAM,SAAS,CAAC;AAE1F,UAAU,wBAAwB;IAChC,gBAAgB,EAAE,OAAO,CAAC;IAC1B,gCAAgC,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;IAC3D,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,cAAc,CAAC;IAC/B,gBAAgB,EAAE,gBAAgB,GAAG,IAAI,CAAC;IAC1C,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;IACvB,mBAAmB,EAAE,OAAO,CAAC;IAC7B,WAAW,EAAE,kBAAkB,CAAC;IAChC,qBAAqB,EAAE,MAAM,CAAC;IAC9B,qBAAqB,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CAAC;IACtD,WAAW,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,CAAC;IAC7C,aAAa,EAAE,CAAC,MAAM,EAAE,gBAAgB,KAAK,IAAI,CAAC;IAClD,KAAK,EAAE,KAAK,CAAC;IACb,qBAAqB,EAAE,CAAC,eAAe,EAAE,MAAM,KAAK,IAAI,CAAC;IACzD,kBAAkB,EAAE,CAAC,QAAQ,CAAC,EAAE,OAAO,KAAK,IAAI,CAAC;IACjD,kBAAkB,CAAC,EAAE,KAAK,CAAC;QACzB,IAAI,EAAE,MAAM,CAAC;QACb,iBAAiB,EAAE,MAAM,CAAC;QAC1B,IAAI,EAAE,sBAAsB,EAAE,CAAC;KAChC,CAAC,CAAC;IACH,yBAAyB,CAAC,EAAE,OAAO,CAAC;CACrC;AAED,wBAAgB,mBAAmB,CAAC,EAClC,gBAAgB,EAChB,gCAAgC,EAChC,WAAW,EACX,cAAc,EACd,gBAAgB,EAChB,SAAS,EACT,KAAK,EACL,mBAAmB,EACnB,WAAW,EACX,qBAAqB,EACrB,qBAAqB,EACrB,WAAW,EACX,aAAa,EACb,KAAK,EACL,qBAAqB,EACrB,kBAAkB,EAClB,yBAAyB,GAC1B,EAAE,wBAAwB,GAAG,GAAG,CAAC,OAAO,CA0LxC"}
@@ -1,4 +1,4 @@
1
- import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  // Copyright 2022 The Parca Authors
3
3
  // Licensed under the Apache License, Version 2.0 (the "License");
4
4
  // you may not use this file except in compliance with the License.
@@ -15,6 +15,7 @@ import cx from 'classnames';
15
15
  import { Query } from '@parca/parser';
16
16
  import { MergedProfileSelection } from '..';
17
17
  import UtilizationMetricsGraph from '../MetricsGraph/UtilizationMetrics';
18
+ import AreaChart from '../MetricsGraph/UtilizationMetrics/AreaChart';
18
19
  import ProfileMetricsGraph, { ProfileMetricsEmptyState } from '../ProfileMetricsGraph';
19
20
  export function MetricsGraphSection({ showMetricsGraph, setDisplayHideMetricsGraphButton, heightStyle, querySelection, profileSelection, comparing, sumBy, defaultSumByLoading, queryClient, queryExpressionString, setTimeRangeSelection, selectQuery, selectProfile, query, setNewQueryExpression, utilizationMetrics, utilizationMetricsLoading, }) {
20
21
  const handleTimeRangeChange = (range) => {
@@ -75,7 +76,21 @@ export function MetricsGraphSection({ showMetricsGraph, setDisplayHideMetricsGra
75
76
  const mergeTo = query.profileType().delta ? mergeFrom + durationInMilliseconds : mergeFrom;
76
77
  selectProfile(new MergedProfileSelection(mergeFrom, mergeTo, query));
77
78
  };
79
+ const UtilizationGraphToShow = ({ utilizationMetrics, }) => {
80
+ const throughputMetrics = utilizationMetrics.filter(metric => metric.name === 'gpu_pcie_throughput_transmit_bytes' ||
81
+ metric.name === 'gpu_pcie_throughput_receive_bytes');
82
+ if (utilizationMetrics === undefined || utilizationMetrics.length === 0) {
83
+ return _jsx(_Fragment, {});
84
+ }
85
+ return (_jsxs("div", { children: [utilizationMetrics.map(({ name, humanReadableName, data }) => {
86
+ if (name !== 'gpu_pcie_throughput_transmit_bytes' &&
87
+ name !== 'gpu_pcie_throughput_receive_bytes') {
88
+ return (_jsx(_Fragment, { children: _jsx(UtilizationMetricsGraph, { data: data, addLabelMatcher: addLabelMatcher, setTimeRange: handleTimeRangeChange, utilizationMetricsLoading: utilizationMetricsLoading, name: name, humanReadableName: humanReadableName, from: querySelection.from, to: querySelection.to }, name) }));
89
+ }
90
+ return null;
91
+ }), throughputMetrics.length > 0 && (_jsx(AreaChart, { transmitData: throughputMetrics[0].data, receiveData: throughputMetrics[1].data, addLabelMatcher: addLabelMatcher, setTimeRange: handleTimeRangeChange, name: throughputMetrics[0].name, humanReadableName: throughputMetrics[0].humanReadableName, from: querySelection.from, to: querySelection.to, utilizationMetricsLoading: utilizationMetricsLoading }))] }));
92
+ };
78
93
  return (_jsxs("div", { className: cx('relative', { 'py-4': !showMetricsGraph }), children: [setDisplayHideMetricsGraphButton != null ? (_jsxs("button", { onClick: () => setDisplayHideMetricsGraphButton(!showMetricsGraph), className: cx('hidden px-3 py-1 text-sm font-medium text-gray-700 dark:text-gray-200 bg-gray-100 rounded-md hover:bg-gray-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:bg-gray-900 z-10', showMetricsGraph && 'absolute right-0 bottom-3 !flex', !showMetricsGraph && 'relative !flex ml-auto'), children: [showMetricsGraph ? 'Hide' : 'Show', " Metrics Graph"] })) : null, showMetricsGraph && (_jsx(_Fragment, { children: _jsx("div", { style: { height: heightStyle }, children: querySelection.expression !== '' &&
79
94
  querySelection.from !== undefined &&
80
- querySelection.to !== undefined ? (_jsx(_Fragment, { children: utilizationMetrics !== undefined ? (utilizationMetrics.map(({ name, humanReadableName, data }) => (_jsx(_Fragment, { children: _jsx(UtilizationMetricsGraph, { data: data, addLabelMatcher: addLabelMatcher, setTimeRange: handleTimeRangeChange, utilizationMetricsLoading: utilizationMetricsLoading, name: name, humanReadableName: humanReadableName, from: querySelection.from, to: querySelection.to }, name) })))) : (_jsx(_Fragment, { children: _jsx(ProfileMetricsGraph, { queryClient: queryClient, queryExpression: querySelection.expression, from: querySelection.from, to: querySelection.to, profile: profileSelection, comparing: comparing, sumBy: querySelection.sumBy ?? sumBy ?? [], sumByLoading: defaultSumByLoading, setTimeRange: handleTimeRangeChange, addLabelMatcher: addLabelMatcher, onPointClick: handlePointClick }) })) })) : (profileSelection === null && (_jsx("div", { className: "p-2", children: _jsx(ProfileMetricsEmptyState, { message: "Please select a profile type and click 'Search' to begin." }) }))) }) }))] }));
95
+ querySelection.to !== undefined ? (_jsx(_Fragment, { children: utilizationMetrics !== undefined ? (_jsx(UtilizationGraphToShow, { utilizationMetrics: utilizationMetrics })) : (_jsx(_Fragment, { children: _jsx(ProfileMetricsGraph, { queryClient: queryClient, queryExpression: querySelection.expression, from: querySelection.from, to: querySelection.to, profile: profileSelection, comparing: comparing, sumBy: querySelection.sumBy ?? sumBy ?? [], sumByLoading: defaultSumByLoading, setTimeRange: handleTimeRangeChange, addLabelMatcher: addLabelMatcher, onPointClick: handlePointClick }) })) })) : (profileSelection === null && (_jsx("div", { className: "p-2", children: _jsx(ProfileMetricsEmptyState, { message: "Please select a profile type and click 'Search' to begin." }) }))) }) }))] }));
81
96
  }
package/package.json CHANGED
@@ -1,18 +1,18 @@
1
1
  {
2
2
  "name": "@parca/profile",
3
- "version": "0.16.497",
3
+ "version": "0.16.499",
4
4
  "description": "Profile viewing libraries",
5
5
  "dependencies": {
6
6
  "@headlessui/react": "^1.7.19",
7
7
  "@iconify/react": "^4.0.0",
8
8
  "@parca/client": "0.16.128",
9
- "@parca/components": "0.16.328",
9
+ "@parca/components": "0.16.329",
10
10
  "@parca/dynamicsize": "0.16.65",
11
- "@parca/hooks": "0.0.83",
11
+ "@parca/hooks": "0.0.84",
12
12
  "@parca/icons": "0.16.71",
13
13
  "@parca/parser": "0.16.78",
14
- "@parca/store": "0.16.168",
15
- "@parca/utilities": "0.0.94",
14
+ "@parca/store": "0.16.169",
15
+ "@parca/utilities": "0.0.95",
16
16
  "@popperjs/core": "^2.11.8",
17
17
  "@protobuf-ts/runtime-rpc": "^2.5.0",
18
18
  "@storybook/preview-api": "^8.4.3",
@@ -77,5 +77,5 @@
77
77
  "access": "public",
78
78
  "registry": "https://registry.npmjs.org/"
79
79
  },
80
- "gitHead": "3bc555cbbc3e53222bbd799218fc01149d8274d3"
80
+ "gitHead": "8973a316f189e3eaa00c8a7ca3d7f5c528d6f1da"
81
81
  }
@@ -31,6 +31,7 @@ interface Props {
31
31
  sampleUnit: string;
32
32
  delta: boolean;
33
33
  utilizationMetrics?: boolean;
34
+ valuePrefix?: string;
34
35
  }
35
36
 
36
37
  const virtualElement: VirtualElement = {
@@ -69,6 +70,7 @@ const MetricsTooltip = ({
69
70
  sampleUnit,
70
71
  delta,
71
72
  utilizationMetrics = false,
73
+ valuePrefix,
72
74
  }: Props): JSX.Element => {
73
75
  const {timezone} = useParcaContext();
74
76
 
@@ -164,7 +166,10 @@ const MetricsTooltip = ({
164
166
  </>
165
167
  ) : (
166
168
  <tr>
167
- <td className="w-1/4">Value</td>
169
+ <td className="w-1/4">
170
+ {valuePrefix ?? ''}
171
+ Value
172
+ </td>
168
173
  <td className="w-3/4">
169
174
  {valueFormatter(highlighted.valuePerSecond, sampleUnit, 5)}
170
175
  </td>
@@ -0,0 +1,610 @@
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, useCallback, useId, useMemo, useRef, useState} from 'react';
15
+
16
+ import * as d3 from 'd3';
17
+ import {pointer} from 'd3-selection';
18
+ import {AnimatePresence, motion} from 'framer-motion';
19
+ import throttle from 'lodash.throttle';
20
+ import {useContextMenu} from 'react-contexify';
21
+
22
+ import {DateTimeRange, MetricsGraphSkeleton, useParcaContext, useURLState} from '@parca/components';
23
+ import {Matcher} from '@parca/parser';
24
+ import {formatDate, formatForTimespan, getPrecision, valueFormatter} from '@parca/utilities';
25
+
26
+ import {type UtilizationMetrics as MetricSeries} from '../../ProfileSelector';
27
+ import MetricsContextMenu from '../MetricsContextMenu';
28
+ import MetricsTooltip from '../MetricsTooltip';
29
+ import {useMetricsGraphDimensions} from '../useMetricsGraphDimensions';
30
+
31
+ interface NetworkLabel {
32
+ name: string;
33
+ value: string;
34
+ }
35
+
36
+ interface NetworkSeries {
37
+ metric: NetworkLabel[];
38
+ values: number[][];
39
+ labelset: string;
40
+ isReceive?: boolean;
41
+ }
42
+
43
+ interface CommonProps {
44
+ transmitData: MetricSeries[];
45
+ receiveData: MetricSeries[];
46
+ addLabelMatcher: (
47
+ labels: {key: string; value: string} | Array<{key: string; value: string}>
48
+ ) => void;
49
+ setTimeRange: (range: DateTimeRange) => void;
50
+ name: string;
51
+ humanReadableName: string;
52
+ from: number;
53
+ to: number;
54
+ }
55
+
56
+ type RawAreaChartProps = CommonProps & {
57
+ width: number;
58
+ height: number;
59
+ margin: number;
60
+ };
61
+
62
+ type Props = CommonProps & {
63
+ utilizationMetricsLoading?: boolean;
64
+ };
65
+
66
+ interface MetricsSample {
67
+ timestamp: number;
68
+ value: number;
69
+ }
70
+
71
+ function transformToSeries(data: MetricSeries[], isReceive = false): NetworkSeries[] {
72
+ const series: NetworkSeries[] = data.reduce<NetworkSeries[]>(function (
73
+ agg: NetworkSeries[],
74
+ s: MetricSeries
75
+ ) {
76
+ if (s.labelset !== undefined) {
77
+ const metric = s.labelset.labels.sort((a, b) => a.name.localeCompare(b.name));
78
+ agg.push({
79
+ metric,
80
+ values: s.samples.reduce<number[][]>(function (agg: number[][], d: MetricsSample) {
81
+ if (d.timestamp !== undefined && d.value !== undefined) {
82
+ // Multiply receive values by -1 to display below zero
83
+ const value = isReceive ? -1 * d.value : d.value;
84
+ agg.push([d.timestamp, value]);
85
+ }
86
+ return agg;
87
+ }, []),
88
+ labelset: metric.map(m => `${m.name}=${m.value}`).join(','),
89
+ isReceive,
90
+ });
91
+ }
92
+ return agg;
93
+ },
94
+ []);
95
+
96
+ // Sort values by timestamp for each series
97
+ return series.map(series => ({
98
+ ...series,
99
+ values: series.values.sort((a, b) => a[0] - b[0]),
100
+ }));
101
+ }
102
+
103
+ const getYAxisUnit = (name: string): string => {
104
+ switch (name) {
105
+ case 'gpu_utilization_percent':
106
+ return 'percent';
107
+ case 'gpu_memory_utilization_percent':
108
+ return 'percent';
109
+ case 'gpu_power_watt':
110
+ return 'watts';
111
+ case 'gpu_pcie_throughput_transmit_bytes':
112
+ return 'bytes_per_second';
113
+ case 'gpu_pcie_throughput_receive_bytes':
114
+ return 'bytes_per_second';
115
+ default:
116
+ return 'percent';
117
+ }
118
+ };
119
+
120
+ const RawAreaChart = ({
121
+ transmitData,
122
+ receiveData,
123
+ addLabelMatcher,
124
+ setTimeRange,
125
+ width,
126
+ height,
127
+ margin,
128
+ name,
129
+ humanReadableName,
130
+ from,
131
+ to,
132
+ }: RawAreaChartProps): JSX.Element => {
133
+ const {timezone} = useParcaContext();
134
+ const graph = useRef(null);
135
+ const [dragging, setDragging] = useState(false);
136
+ const [hovering, setHovering] = useState(false);
137
+ const [relPos, setRelPos] = useState(-1);
138
+ const [pos, setPos] = useState([0, 0]);
139
+ const [isContextMenuOpen, setIsContextMenuOpen] = useState<boolean>(false);
140
+ const idForContextMenu = useId();
141
+ const [selectedSeries, setSelectedSeries] = useURLState<string>('selectedSeries');
142
+ const [_, setSelectedTimeframe] = useURLState('gpu_selected_timeframe');
143
+
144
+ const parsedSelectedSeries: Matcher[] = useMemo(() => {
145
+ if (selectedSeries === undefined) {
146
+ return [];
147
+ }
148
+
149
+ return JSON.parse(decodeURIComponent(selectedSeries));
150
+ }, [selectedSeries]);
151
+
152
+ const lineStroke = '1px';
153
+ const lineStrokeHover = '2px';
154
+ const lineStrokeSelected = '3px';
155
+
156
+ const graphWidth = width - margin * 1.5 - margin / 2;
157
+
158
+ const paddedFrom = from;
159
+ const paddedTo = to;
160
+
161
+ const series = useMemo(() => {
162
+ const transmitSeries = transformToSeries(transmitData);
163
+ const receiveSeries = transformToSeries(receiveData, true);
164
+ return [...transmitSeries, ...receiveSeries];
165
+ }, [transmitData, receiveData]);
166
+
167
+ const extentsY = series.map(function (s) {
168
+ return d3.extent(s.values, function (d) {
169
+ return d[1];
170
+ });
171
+ });
172
+
173
+ const minY = d3.min(extentsY, function (d) {
174
+ return d[0];
175
+ });
176
+
177
+ const maxY = d3.max(extentsY, function (d) {
178
+ return d[1];
179
+ });
180
+
181
+ // Setup scales with padded time range
182
+ const xScale = d3.scaleUtc().domain([paddedFrom, paddedTo]).range([0, graphWidth]);
183
+
184
+ // Find the absolute maximum to ensure symmetric scale
185
+ const absMax = Math.max(Math.abs(minY ?? 0), Math.abs(maxY ?? 0));
186
+
187
+ const yScale = d3
188
+ .scaleLinear()
189
+ // Ensure domain is symmetric around 0 and includes all values
190
+ .domain([-absMax, absMax])
191
+ .range([height - margin, 0]);
192
+
193
+ const throttledSetPos = throttle(setPos, 20);
194
+
195
+ const onMouseMove = (e: React.MouseEvent<SVGSVGElement | HTMLDivElement, MouseEvent>): void => {
196
+ if (isContextMenuOpen) {
197
+ return;
198
+ }
199
+
200
+ // X/Y coordinate array relative to svg
201
+ const rel = pointer(e);
202
+
203
+ const xCoordinate = rel[0];
204
+ const xCoordinateWithoutMargin = xCoordinate - margin;
205
+ const yCoordinate = rel[1];
206
+ const yCoordinateWithoutMargin = yCoordinate - margin;
207
+
208
+ throttledSetPos([xCoordinateWithoutMargin, yCoordinateWithoutMargin]);
209
+ };
210
+
211
+ const trackVisibility = (isVisible: boolean): void => {
212
+ setIsContextMenuOpen(isVisible);
213
+ };
214
+
215
+ const MENU_ID = `areachart-context-menu-${idForContextMenu}`;
216
+
217
+ const {show} = useContextMenu({
218
+ id: MENU_ID,
219
+ });
220
+
221
+ const displayMenu = useCallback(
222
+ (e: React.MouseEvent): void => {
223
+ show({
224
+ event: e,
225
+ });
226
+ },
227
+ [show]
228
+ );
229
+
230
+ const getSeriesColor = (series: NetworkSeries): string => {
231
+ return series.isReceive === true ? '#EAB308' : '#22C55E'; // Yellow for receive, Green for transmit
232
+ };
233
+
234
+ // Create area generator for transmit (above zero)
235
+ const transmitArea = d3
236
+ .area<number[]>()
237
+ .x(d => xScale(d[0]))
238
+ .y0(yScale(0)) // Start from zero line
239
+ .y1(d => yScale(d[1])); // Top of the area (data point)
240
+
241
+ // Create area generator for receive (below zero)
242
+ const receiveArea = d3
243
+ .area<number[]>()
244
+ .x(d => xScale(d[0]))
245
+ .y0(yScale(0)) // Start from zero line
246
+ .y1(d => yScale(d[1])); // Bottom of the area (negative data point)
247
+
248
+ const highlighted = useMemo(() => {
249
+ if (series.length === 0) {
250
+ return null;
251
+ }
252
+
253
+ // Return the closest point as the highlighted point
254
+ const closestPointPerSeries = series.map(function (s) {
255
+ const distances = s.values.map(d => {
256
+ const x = xScale(d[0]) + margin / 2;
257
+ const y = yScale(d[1]) - margin / 3;
258
+
259
+ return Math.sqrt(Math.pow(pos[0] - x, 2) + Math.pow(pos[1] - y, 2));
260
+ });
261
+
262
+ const pointIndex = d3.minIndex(distances);
263
+ const minDistance = distances[pointIndex];
264
+
265
+ return {
266
+ pointIndex,
267
+ distance: minDistance,
268
+ };
269
+ });
270
+
271
+ const closestSeriesIndex = d3.minIndex(closestPointPerSeries, s => s.distance);
272
+ const pointIndex = closestPointPerSeries[closestSeriesIndex].pointIndex;
273
+ const point = series[closestSeriesIndex].values[pointIndex];
274
+ return {
275
+ seriesIndex: closestSeriesIndex,
276
+ labels: series[closestSeriesIndex].metric,
277
+ timestamp: point[0],
278
+ valuePerSecond: point[1],
279
+ value: point[2],
280
+ duration: point[3],
281
+ x: xScale(point[0]),
282
+ y: yScale(point[1]),
283
+ };
284
+ }, [pos, series, xScale, yScale, margin]);
285
+
286
+ const onMouseDown = (e: React.MouseEvent<SVGSVGElement | HTMLDivElement, MouseEvent>): void => {
287
+ // only left mouse button
288
+ if (e.button !== 0) {
289
+ return;
290
+ }
291
+
292
+ // X/Y coordinate array relative to svg
293
+ const rel = pointer(e);
294
+
295
+ const xCoordinate = rel[0];
296
+ const xCoordinateWithoutMargin = xCoordinate - margin;
297
+ if (xCoordinateWithoutMargin >= 0) {
298
+ setRelPos(xCoordinateWithoutMargin);
299
+ setDragging(true);
300
+ }
301
+
302
+ e.stopPropagation();
303
+ e.preventDefault();
304
+ };
305
+
306
+ const onMouseUp = (e: React.MouseEvent<SVGSVGElement | HTMLDivElement, MouseEvent>): void => {
307
+ setDragging(false);
308
+
309
+ if (relPos === -1) {
310
+ // MouseDown happened outside of this element.
311
+ return;
312
+ }
313
+
314
+ // This is a normal click. We tolerate tiny movements to still be a
315
+ // click as they can occur when clicking based on user feedback.
316
+ if (Math.abs(relPos - pos[0]) <= 1) {
317
+ setRelPos(-1);
318
+ return;
319
+ }
320
+
321
+ let startPos = relPos;
322
+ let endPos = pos[0];
323
+
324
+ if (startPos > endPos) {
325
+ startPos = pos[0];
326
+ endPos = relPos;
327
+ }
328
+
329
+ const startCorrection = 10;
330
+ const endCorrection = 30;
331
+
332
+ const firstTime = xScale.invert(startPos - startCorrection).valueOf();
333
+ const secondTime = xScale.invert(endPos - endCorrection).valueOf();
334
+
335
+ setTimeRange(DateTimeRange.fromAbsoluteDates(firstTime, secondTime));
336
+
337
+ setRelPos(-1);
338
+
339
+ e.stopPropagation();
340
+ e.preventDefault();
341
+ };
342
+
343
+ return (
344
+ <>
345
+ <MetricsContextMenu
346
+ onAddLabelMatcher={addLabelMatcher}
347
+ menuId={MENU_ID}
348
+ highlighted={highlighted}
349
+ trackVisibility={trackVisibility}
350
+ utilizationMetrics={true}
351
+ />
352
+
353
+ {highlighted != null && hovering && !dragging && pos[0] !== 0 && pos[1] !== 0 && (
354
+ <div
355
+ onMouseMove={onMouseMove}
356
+ onMouseEnter={() => setHovering(true)}
357
+ onMouseLeave={() => setHovering(false)}
358
+ >
359
+ {!isContextMenuOpen && (
360
+ <MetricsTooltip
361
+ x={pos[0] + margin}
362
+ y={pos[1] + margin}
363
+ highlighted={{
364
+ ...highlighted,
365
+ valuePerSecond: Math.abs(highlighted.valuePerSecond),
366
+ }}
367
+ contextElement={graph.current}
368
+ sampleUnit={getYAxisUnit(name)}
369
+ delta={false}
370
+ utilizationMetrics={true}
371
+ valuePrefix={
372
+ highlighted.seriesIndex >= transmitData.length ? 'Receive ' : 'Transmit '
373
+ }
374
+ />
375
+ )}
376
+ </div>
377
+ )}
378
+ <div
379
+ ref={graph}
380
+ onMouseEnter={() => setHovering(true)}
381
+ onMouseLeave={() => setHovering(false)}
382
+ onContextMenu={displayMenu}
383
+ >
384
+ <svg
385
+ width={`${width}px`}
386
+ height={`${height + margin}px`}
387
+ onMouseDown={onMouseDown}
388
+ onMouseUp={onMouseUp}
389
+ onMouseMove={onMouseMove}
390
+ >
391
+ <g transform={`translate(${margin}, 0)`}>
392
+ {dragging && (
393
+ <g className="zoom-time-rect">
394
+ <rect
395
+ className="bar"
396
+ x={pos[0] - relPos < 0 ? pos[0] : relPos}
397
+ y={0}
398
+ height={height}
399
+ width={Math.abs(pos[0] - relPos)}
400
+ fill={'rgba(0, 0, 0, 0.125)'}
401
+ />
402
+ </g>
403
+ )}
404
+ </g>
405
+ <g transform={`translate(${margin * 1.5}, ${margin / 1.5})`}>
406
+ <g className="y axis" textAnchor="end" fontSize="10" fill="none">
407
+ {yScale.ticks(6).map((d, i, allTicks) => {
408
+ let decimals = 2;
409
+ const intervalBetweenTicks = allTicks[1] - allTicks[0];
410
+
411
+ if (intervalBetweenTicks < 1) {
412
+ const precision = getPrecision(intervalBetweenTicks);
413
+ decimals = precision;
414
+ }
415
+
416
+ return (
417
+ <Fragment key={`${i.toString()}-${d.toString()}`}>
418
+ <g key={`tick-${i}`} className="tick" transform={`translate(0, ${yScale(d)})`}>
419
+ <line className="stroke-gray-300 dark:stroke-gray-500" x2={-6} />
420
+ <text fill="currentColor" x={-9} dy={'0.32em'}>
421
+ {d < 0 ? '-' : ''}
422
+ {valueFormatter(Math.abs(d), getYAxisUnit(name), decimals)}
423
+ </text>
424
+ </g>
425
+ <g key={`grid-${i}`}>
426
+ <line
427
+ className="stroke-gray-300 dark:stroke-gray-500"
428
+ x1={xScale(from)}
429
+ x2={xScale(to)}
430
+ y1={yScale(d)}
431
+ y2={yScale(d)}
432
+ />
433
+ </g>
434
+ </Fragment>
435
+ );
436
+ })}
437
+ <line
438
+ className="stroke-gray-300 dark:stroke-gray-500"
439
+ x1={0}
440
+ x2={0}
441
+ y1={0}
442
+ y2={height - margin}
443
+ />
444
+ <line
445
+ className="stroke-gray-300 dark:stroke-gray-500"
446
+ x1={xScale(to)}
447
+ x2={xScale(to)}
448
+ y1={0}
449
+ y2={height - margin}
450
+ />
451
+ <g transform={`translate(${-margin}, ${(height - margin) / 2}) rotate(270)`}>
452
+ <text
453
+ fill="currentColor"
454
+ dy="-0.7em"
455
+ className="text-sm capitalize"
456
+ textAnchor="middle"
457
+ >
458
+ {humanReadableName}
459
+ </text>
460
+ </g>
461
+ </g>
462
+ <g
463
+ className="x axis"
464
+ fill="none"
465
+ fontSize="10"
466
+ textAnchor="middle"
467
+ transform={`translate(0,${height - margin})`}
468
+ >
469
+ {xScale.ticks(5).map((d, i) => (
470
+ <Fragment key={`${i.toString()}-${d.toString()}`}>
471
+ <g
472
+ key={`tick-${i}`}
473
+ className="tick"
474
+ /* eslint-disable-next-line @typescript-eslint/restrict-template-expressions */
475
+ transform={`translate(${xScale(d)}, 0)`}
476
+ >
477
+ <line y2={6} className="stroke-gray-300 dark:stroke-gray-500" />
478
+ <text fill="currentColor" dy=".71em" y={9}>
479
+ {formatDate(d, formatForTimespan(from, to), timezone)}
480
+ </text>
481
+ </g>
482
+ <g key={`grid-${i}`}>
483
+ <line
484
+ className="stroke-gray-300 dark:stroke-gray-500"
485
+ x1={xScale(d)}
486
+ x2={xScale(d)}
487
+ y1={0}
488
+ y2={-height + margin}
489
+ />
490
+ </g>
491
+ </Fragment>
492
+ ))}
493
+ <line
494
+ className="stroke-gray-300 dark:stroke-gray-500"
495
+ x1={0}
496
+ x2={graphWidth}
497
+ y1={0}
498
+ y2={0}
499
+ />
500
+ <g transform={`translate(${(width - 2.5 * margin) / 2}, ${margin / 2})`}>
501
+ <text fill="currentColor" dy=".71em" y={5} className="text-sm">
502
+ Time
503
+ </text>
504
+ </g>
505
+ </g>
506
+ <g className="areas">
507
+ {series.map((s, i) => {
508
+ let isSelected = false;
509
+ if (parsedSelectedSeries != null && parsedSelectedSeries.length > 0) {
510
+ isSelected = parsedSelectedSeries.every(m => {
511
+ for (let i = 0; i < s.metric.length; i++) {
512
+ if (s.metric[i].name === m.key && s.metric[i].value === m.value) {
513
+ return true;
514
+ }
515
+ }
516
+ return false;
517
+ });
518
+ }
519
+
520
+ const seriesColor = getSeriesColor(s);
521
+ const fillOpacity = isSelected ? 0.4 : 0.2;
522
+ const strokeOpacity = isSelected ? 1 : 0.8;
523
+ const areaGenerator = s.isReceive === true ? receiveArea : transmitArea;
524
+
525
+ return (
526
+ <g key={i} className="area cursor-pointer">
527
+ <path
528
+ d={areaGenerator(s.values) ?? ''}
529
+ fill={seriesColor}
530
+ fillOpacity={fillOpacity}
531
+ stroke={seriesColor}
532
+ strokeWidth={
533
+ isSelected
534
+ ? lineStrokeSelected
535
+ : hovering && highlighted != null && i === highlighted.seriesIndex
536
+ ? lineStrokeHover
537
+ : lineStroke
538
+ }
539
+ strokeOpacity={strokeOpacity}
540
+ onClick={() => {
541
+ if (highlighted != null) {
542
+ setSelectedSeries(
543
+ JSON.stringify(
544
+ highlighted.labels.map(l => ({
545
+ key: l.name,
546
+ value: l.value,
547
+ }))
548
+ )
549
+ );
550
+ setSelectedTimeframe(undefined);
551
+ }
552
+ }}
553
+ />
554
+ </g>
555
+ );
556
+ })}
557
+ </g>
558
+ </g>
559
+ </svg>
560
+ </div>
561
+ </>
562
+ );
563
+ };
564
+
565
+ const AreaChart = ({
566
+ transmitData,
567
+ receiveData,
568
+ addLabelMatcher,
569
+ setTimeRange,
570
+ utilizationMetricsLoading,
571
+ name,
572
+ humanReadableName,
573
+ from,
574
+ to,
575
+ }: Props): JSX.Element => {
576
+ const {isDarkMode} = useParcaContext();
577
+ const {width, height, margin, heightStyle} = useMetricsGraphDimensions(false, true);
578
+
579
+ return (
580
+ <AnimatePresence>
581
+ <motion.div
582
+ className="w-full relative"
583
+ key="area-chart-graph-loaded"
584
+ initial={{display: 'none', opacity: 0}}
585
+ animate={{display: 'block', opacity: 1}}
586
+ transition={{duration: 0.5}}
587
+ >
588
+ {utilizationMetricsLoading === true ? (
589
+ <MetricsGraphSkeleton heightStyle={heightStyle} isDarkMode={isDarkMode} isMini={true} />
590
+ ) : (
591
+ <RawAreaChart
592
+ transmitData={transmitData}
593
+ receiveData={receiveData}
594
+ addLabelMatcher={addLabelMatcher}
595
+ setTimeRange={setTimeRange}
596
+ width={width}
597
+ height={height}
598
+ margin={margin}
599
+ name={name}
600
+ humanReadableName={humanReadableName}
601
+ from={from}
602
+ to={to}
603
+ />
604
+ )}
605
+ </motion.div>
606
+ </AnimatePresence>
607
+ );
608
+ };
609
+
610
+ export default AreaChart;
@@ -19,6 +19,7 @@ import {Query} from '@parca/parser';
19
19
 
20
20
  import {MergedProfileSelection, ProfileSelection} from '..';
21
21
  import UtilizationMetricsGraph from '../MetricsGraph/UtilizationMetrics';
22
+ import AreaChart from '../MetricsGraph/UtilizationMetrics/AreaChart';
22
23
  import ProfileMetricsGraph, {ProfileMetricsEmptyState} from '../ProfileMetricsGraph';
23
24
  import {QuerySelection, type UtilizationMetrics as UtilizationMetricsType} from './index';
24
25
 
@@ -137,6 +138,67 @@ export function MetricsGraphSection({
137
138
  selectProfile(new MergedProfileSelection(mergeFrom, mergeTo, query));
138
139
  };
139
140
 
141
+ const UtilizationGraphToShow = ({
142
+ utilizationMetrics,
143
+ }: {
144
+ utilizationMetrics: Array<{
145
+ name: string;
146
+ humanReadableName: string;
147
+ data: UtilizationMetricsType[];
148
+ }>;
149
+ }): JSX.Element => {
150
+ const throughputMetrics = utilizationMetrics.filter(
151
+ metric =>
152
+ metric.name === 'gpu_pcie_throughput_transmit_bytes' ||
153
+ metric.name === 'gpu_pcie_throughput_receive_bytes'
154
+ );
155
+
156
+ if (utilizationMetrics === undefined || utilizationMetrics.length === 0) {
157
+ return <></>;
158
+ }
159
+
160
+ return (
161
+ <div>
162
+ {utilizationMetrics.map(({name, humanReadableName, data}) => {
163
+ if (
164
+ name !== 'gpu_pcie_throughput_transmit_bytes' &&
165
+ name !== 'gpu_pcie_throughput_receive_bytes'
166
+ ) {
167
+ return (
168
+ <>
169
+ <UtilizationMetricsGraph
170
+ key={name}
171
+ data={data}
172
+ addLabelMatcher={addLabelMatcher}
173
+ setTimeRange={handleTimeRangeChange}
174
+ utilizationMetricsLoading={utilizationMetricsLoading}
175
+ name={name}
176
+ humanReadableName={humanReadableName}
177
+ from={querySelection.from}
178
+ to={querySelection.to}
179
+ />
180
+ </>
181
+ );
182
+ }
183
+ return null;
184
+ })}
185
+ {throughputMetrics.length > 0 && (
186
+ <AreaChart
187
+ transmitData={throughputMetrics[0].data}
188
+ receiveData={throughputMetrics[1].data}
189
+ addLabelMatcher={addLabelMatcher}
190
+ setTimeRange={handleTimeRangeChange}
191
+ name={throughputMetrics[0].name}
192
+ humanReadableName={throughputMetrics[0].humanReadableName}
193
+ from={querySelection.from}
194
+ to={querySelection.to}
195
+ utilizationMetricsLoading={utilizationMetricsLoading}
196
+ />
197
+ )}
198
+ </div>
199
+ );
200
+ };
201
+
140
202
  return (
141
203
  <div className={cx('relative', {'py-4': !showMetricsGraph})}>
142
204
  {setDisplayHideMetricsGraphButton != null ? (
@@ -159,21 +221,7 @@ export function MetricsGraphSection({
159
221
  querySelection.to !== undefined ? (
160
222
  <>
161
223
  {utilizationMetrics !== undefined ? (
162
- utilizationMetrics.map(({name, humanReadableName, data}) => (
163
- <>
164
- <UtilizationMetricsGraph
165
- key={name}
166
- data={data}
167
- addLabelMatcher={addLabelMatcher}
168
- setTimeRange={handleTimeRangeChange}
169
- utilizationMetricsLoading={utilizationMetricsLoading}
170
- name={name}
171
- humanReadableName={humanReadableName}
172
- from={querySelection.from}
173
- to={querySelection.to}
174
- />
175
- </>
176
- ))
224
+ <UtilizationGraphToShow utilizationMetrics={utilizationMetrics} />
177
225
  ) : (
178
226
  <>
179
227
  <ProfileMetricsGraph