Skip to content

Commit

Permalink
Add stacked line chart functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
andrewballantyne committed Mar 18, 2023
1 parent b5a3459 commit 36ba734
Show file tree
Hide file tree
Showing 8 changed files with 191 additions and 50 deletions.
20 changes: 15 additions & 5 deletions frontend/src/api/prometheus/serving.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,16 @@ export const useModelServingMetrics = (
timeframe,
);

const inferenceRequestCount = useQueryRangeResourceData(
const inferenceRequestSuccessCount = useQueryRangeResourceData(
type === 'inference',
queries[InferenceMetricType.REQUEST_COUNT],
queries[InferenceMetricType.REQUEST_COUNT_SUCCESS],
end,
timeframe,
);

const inferenceRequestFailedCount = useQueryRangeResourceData(
type === 'inference',
queries[InferenceMetricType.REQUEST_COUNT_FAILED],
end,
timeframe,
);
Expand All @@ -63,7 +70,8 @@ export const useModelServingMetrics = (
runtimeAverageResponseTime,
runtimeCPUUtilization,
runtimeMemoryUtilization,
inferenceRequestCount,
inferenceRequestSuccessCount,
inferenceRequestFailedCount,
]);

const refreshAllMetrics = React.useCallback(() => {
Expand All @@ -77,7 +85,8 @@ export const useModelServingMetrics = (
[RuntimeMetricType.AVG_RESPONSE_TIME]: runtimeAverageResponseTime,
[RuntimeMetricType.CPU_UTILIZATION]: runtimeCPUUtilization,
[RuntimeMetricType.MEMORY_UTILIZATION]: runtimeMemoryUtilization,
[InferenceMetricType.REQUEST_COUNT]: inferenceRequestCount,
[InferenceMetricType.REQUEST_COUNT_SUCCESS]: inferenceRequestSuccessCount,
[InferenceMetricType.REQUEST_COUNT_FAILED]: inferenceRequestFailedCount,
},
refresh: refreshAllMetrics,
}),
Expand All @@ -86,7 +95,8 @@ export const useModelServingMetrics = (
runtimeAverageResponseTime,
runtimeCPUUtilization,
runtimeMemoryUtilization,
inferenceRequestCount,
inferenceRequestSuccessCount,
inferenceRequestFailedCount,
refreshAllMetrics,
],
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
ModelServingMetricsContext,
} from '~/pages/modelServing/screens/metrics/ModelServingMetricsContext';
import { TimeframeTitle } from '~/pages/modelServing/screens/types';
import { per100 } from './utils';

const InferenceGraphs: React.FC = () => {
const { data, currentTimeframe } = React.useContext(ModelServingMetricsContext);
Expand All @@ -17,11 +18,23 @@ const InferenceGraphs: React.FC = () => {
<Stack hasGutter>
<StackItem>
<MetricsChart
// TODO: This needs to be an improved inference metric
// TODO: This needs to be two values -- stacked line
metrics={data[InferenceMetricType.REQUEST_COUNT]}
color="blue"
// TODO: Make sure this is handled per day and is dividing by 100
metrics={[
{
name: 'Success http requests (x100)',
metric: data[InferenceMetricType.REQUEST_COUNT_SUCCESS],
translatePoint: per100,
},
{
name: 'Failed http requests (x100)',
metric: data[InferenceMetricType.REQUEST_COUNT_FAILED],
translatePoint: (point) => {
// TODO: remove when real values are used
const newPoint = per100(point);
const y = Math.floor(newPoint.y / (Math.floor(Math.random() * 2) + 2));
return { ...newPoint, y };
},
},
]}
title={`Http requests per ${inHours ? 'hour' : 'day'} (x100)`}
/>
</StackItem>
Expand Down
79 changes: 56 additions & 23 deletions frontend/src/pages/modelServing/screens/metrics/MetricsChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,41 +13,61 @@ import {
ChartArea,
ChartAxis,
ChartGroup,
ChartThemeColor,
ChartThreshold,
ChartVoronoiContainer,
getResizeObserver,
} from '@patternfly/react-charts';
import { CubesIcon } from '@patternfly/react-icons';
import { ContextResourceData, PrometheusQueryRangeResultValue } from '~/types';
import { TimeframeTimeRange } from '~/pages/modelServing/screens/const';
import { ModelServingMetricsContext } from './ModelServingMetricsContext';
import { convertTimestamp, formatToShow, getThresholdData } from './utils';
import { MetricChartLine, ProcessedMetrics } from './types';
import {
convertTimestamp,
formatToShow,
getThresholdData,
createGraphMetricLine,
useStableMetrics,
} from './utils';

type MetricsChartProps = {
title: string;
color: string;
metrics: ContextResourceData<PrometheusQueryRangeResultValue>;
color?: string;
metrics: MetricChartLine;
threshold?: number;
};

const MetricsChart: React.FC<MetricsChartProps> = ({ title, color, metrics, threshold }) => {
const MetricsChart: React.FC<MetricsChartProps> = ({
title,
color,
metrics: unstableMetrics,
threshold,
}) => {
const bodyRef = React.useRef<HTMLDivElement>(null);
const [chartWidth, setChartWidth] = React.useState(0);
const { currentTimeframe, lastUpdateTime } = React.useContext(ModelServingMetricsContext);
const metrics = useStableMetrics(unstableMetrics, title);

const processedData = React.useMemo(
const { data: graphLines, maxYValue } = React.useMemo(
() =>
metrics.data?.map((data) => ({
x: data[0] * 1000,
y: parseInt(data[1]),
name: title,
})) || [],
[metrics, title],
);
metrics.reduce<ProcessedMetrics>(
(acc, metric) => {
const lineValues = createGraphMetricLine(metric);
const newMaxValue = Math.max(...lineValues.map((v) => v.y));

const maxValue = Math.max(...processedData.map((e) => e.y));
return {
data: [...acc.data, lineValues],
maxYValue: Math.max(acc.maxYValue, newMaxValue),
};
},
{ data: [], maxYValue: 0 },
),
[metrics],
);

const hasData = processedData.length > 0;
const error = metrics.find((line) => line.metric.error)?.metric.error;
const isAllLoaded = metrics.every((line) => line.metric.loaded);
const hasSomeData = graphLines.some((line) => line.length > 0);

React.useEffect(() => {
const ref = bodyRef.current;
Expand All @@ -62,12 +82,22 @@ const MetricsChart: React.FC<MetricsChartProps> = ({ title, color, metrics, thre
return () => observer();
}, []);

let legendProps: Partial<React.ComponentProps<typeof Chart>> = {};
if (metrics.length > 1 && metrics.every(({ name }) => !!name)) {
// We don't need a label if there is only one line & we need a name for every item (or it won't align)
legendProps = {
legendData: metrics.map(({ name }) => ({ name })),
legendOrientation: 'horizontal',
legendPosition: 'bottom-left',
};
}

return (
<Card>
<CardTitle>{title}</CardTitle>
<CardBody style={{ height: hasData ? 400 : 200, padding: 0 }}>
<CardBody style={{ height: hasSomeData ? 400 : 200, padding: 0 }}>
<div ref={bodyRef}>
{hasData ? (
{hasSomeData ? (
<Chart
ariaTitle={title}
containerComponent={
Expand All @@ -76,11 +106,12 @@ const MetricsChart: React.FC<MetricsChartProps> = ({ title, color, metrics, thre
constrainToVisibleArea
/>
}
domain={{ y: maxValue === 0 ? [0, 1] : [0, maxValue + 1] }}
domain={{ y: maxYValue === 0 ? [0, 1] : [0, maxYValue + 1] }}
height={400}
width={chartWidth}
padding={{ left: 70, right: 50, bottom: 70, top: 50 }}
themeColor={color}
themeColor={color ?? ChartThemeColor.multi}
{...legendProps}
>
<ChartAxis
tickFormat={(x) => convertTimestamp(x, formatToShow(currentTimeframe))}
Expand All @@ -92,17 +123,19 @@ const MetricsChart: React.FC<MetricsChartProps> = ({ title, color, metrics, thre
/>
<ChartAxis dependentAxis tickCount={10} fixLabelOverlap />
<ChartGroup>
<ChartArea data={processedData} />
{graphLines.map((line, i) => (
<ChartArea key={i} data={line} />
))}
</ChartGroup>
{threshold && <ChartThreshold data={getThresholdData(processedData, threshold)} />}
{threshold && <ChartThreshold data={getThresholdData(graphLines, threshold)} />}
</Chart>
) : (
<EmptyState>
{metrics.loaded ? (
{isAllLoaded ? (
<>
<EmptyStateIcon icon={CubesIcon} />
<Title headingLevel="h4" size="lg">
{metrics.error ? metrics.error.message : 'No available data'}
{error ? error.message : 'No available data'}
</Title>
</>
) : (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ export enum RuntimeMetricType {
}

export enum InferenceMetricType {
REQUEST_COUNT = 'inference_request-count',
REQUEST_COUNT_SUCCESS = 'inference_request-count-successes',
REQUEST_COUNT_FAILED = 'inference_request-count-fails',
}

type ModelServingMetricsContext = {
Expand All @@ -33,7 +34,8 @@ export const ModelServingMetricsContext = React.createContext<ModelServingMetric
[RuntimeMetricType.AVG_RESPONSE_TIME]: DEFAULT_CONTEXT_DATA,
[RuntimeMetricType.CPU_UTILIZATION]: DEFAULT_CONTEXT_DATA,
[RuntimeMetricType.MEMORY_UTILIZATION]: DEFAULT_CONTEXT_DATA,
[InferenceMetricType.REQUEST_COUNT]: DEFAULT_CONTEXT_DATA,
[InferenceMetricType.REQUEST_COUNT_FAILED]: DEFAULT_CONTEXT_DATA,
[InferenceMetricType.REQUEST_COUNT_SUCCESS]: DEFAULT_CONTEXT_DATA,
},
currentTimeframe: TimeframeTitle.ONE_HOUR,
setCurrentTimeframe: () => undefined,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
RuntimeMetricType,
} from '~/pages/modelServing/screens/metrics/ModelServingMetricsContext';
import { TimeframeTitle } from '~/pages/modelServing/screens/types';
import { per100 } from '~/pages/modelServing/screens/metrics/utils';

const RuntimeGraphs: React.FC = () => {
const { data, currentTimeframe } = React.useContext(ModelServingMetricsContext);
Expand All @@ -17,30 +18,29 @@ const RuntimeGraphs: React.FC = () => {
<Stack hasGutter>
<StackItem>
<MetricsChart
metrics={data[RuntimeMetricType.REQUEST_COUNT]}
metrics={{ metric: data[RuntimeMetricType.REQUEST_COUNT], translatePoint: per100 }}
color="blue"
// TODO: Make sure this is handled per day and is dividing by 100
title={`Http requests per ${inHours ? 'hour' : 'day'} (x100)`}
/>
</StackItem>
<StackItem>
<MetricsChart
metrics={data[RuntimeMetricType.AVG_RESPONSE_TIME]}
metrics={{ metric: data[RuntimeMetricType.AVG_RESPONSE_TIME] }}
color="green"
title="Average response time (ms)"
/>
</StackItem>
<StackItem>
<MetricsChart
metrics={data[RuntimeMetricType.CPU_UTILIZATION]}
metrics={{ metric: data[RuntimeMetricType.CPU_UTILIZATION] }}
color="purple"
title="CPU utilization %"
/>
</StackItem>
<StackItem>
<MetricsChart
metrics={data[RuntimeMetricType.MEMORY_UTILIZATION]}
color="purple"
metrics={{ metric: data[RuntimeMetricType.MEMORY_UTILIZATION] }}
color="orange"
title="Memory utilization %"
/>
</StackItem>
Expand Down
28 changes: 28 additions & 0 deletions frontend/src/pages/modelServing/screens/metrics/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { ContextResourceData, PrometheusQueryRangeResultValue } from '~/types';

export type TranslatePoint = (line: GraphMetricPoint) => GraphMetricPoint;

type MetricChartLineBase = {
metric: ContextResourceData<PrometheusQueryRangeResultValue>;
translatePoint?: TranslatePoint;
};
export type NamedMetricChartLine = MetricChartLineBase & {
name: string;
};
export type UnnamedMetricChartLine = MetricChartLineBase & {
/** Assumes chart title */
name?: string;
};
export type MetricChartLine = UnnamedMetricChartLine | NamedMetricChartLine[];

export type GraphMetricPoint = {
x: number;
y: number;
name: string;
};
export type GraphMetricLine = GraphMetricPoint[];

export type ProcessedMetrics = {
data: GraphMetricLine[];
maxYValue: number;
};
Loading

0 comments on commit 36ba734

Please sign in to comment.