diff --git a/application/ui/src/features/inference/aside/data-collection.component.tsx b/application/ui/src/features/inference/aside/data-collection.component.tsx
index aaac9bc2c3..daded02af3 100644
--- a/application/ui/src/features/inference/aside/data-collection.component.tsx
+++ b/application/ui/src/features/inference/aside/data-collection.component.tsx
@@ -6,10 +6,13 @@ import { useEffect, useState } from 'react';
import { Divider, Flex, Heading, Slider, Switch, Text } from '@geti/ui';
import { useIsPipelineConfigured } from 'hooks/use-is-pipeline-configured.hook';
import { $api } from 'src/api/client';
-import { components } from 'src/api/openapi-spec';
import { useProjectIdentifier } from 'src/hooks/use-project-identifier.hook';
-type FixedRateDataCollectionPolicy = components['schemas']['FixedRateDataCollectionPolicy'];
+const DEFAULTS = {
+ RATE: 12,
+ CONFIDENCE_THRESHOLD: 0.5,
+ MIN_SAMPLING_INTERVAL: 2.5,
+} as const;
export const DataCollection = () => {
const projectId = useProjectIdentifier();
@@ -26,32 +29,46 @@ export const DataCollection = () => {
},
});
- const isAutoCapturingEnabled = pipelineQuery.data?.data_collection_policies[0]?.enabled ?? false;
- const defaultRate = 12;
- const serverRate =
- (pipelineQuery.data?.data_collection_policies[0] as FixedRateDataCollectionPolicy)?.rate ?? defaultRate;
+ const policies = pipelineQuery.data?.data_collection_policies ?? [];
+ const ratePolicy = policies.find((policy) => policy.type === 'fixed_rate');
+ const confidencePolicy = policies.find((policy) => policy.type === 'confidence_threshold');
- // TODO: add confidence_threshold slider
+ const serverRate = ratePolicy?.rate ?? DEFAULTS.RATE;
+ const serverConfidenceThreshold = confidencePolicy?.confidence_threshold ?? DEFAULTS.CONFIDENCE_THRESHOLD;
const [localRate, setLocalRate] = useState(serverRate);
+ const [localConfidenceThreshold, setLocalConfidenceThreshold] = useState(serverConfidenceThreshold);
+
+ useEffect(() => setLocalRate(serverRate), [serverRate]);
+ useEffect(() => setLocalConfidenceThreshold(serverConfidenceThreshold), [serverConfidenceThreshold]);
+
+ const updatePolicies = (updates: {
+ rateEnabled?: boolean;
+ rate?: number;
+ confidenceEnabled?: boolean;
+ confidenceThreshold?: number;
+ }) => {
+ const newPolicies = [
+ {
+ type: 'fixed_rate' as const,
+ rate: updates.rate ?? serverRate,
+ enabled: updates.rateEnabled ?? ratePolicy?.enabled ?? false,
+ },
+ {
+ type: 'confidence_threshold' as const,
+ confidence_threshold: updates.confidenceThreshold ?? serverConfidenceThreshold,
+ min_sampling_interval: confidencePolicy?.min_sampling_interval ?? DEFAULTS.MIN_SAMPLING_INTERVAL,
+ enabled: updates.confidenceEnabled ?? confidencePolicy?.enabled ?? false,
+ },
+ ];
- useEffect(() => {
- setLocalRate(serverRate);
- }, [serverRate]);
-
- const toggleAutoCapturing = (isEnabled: boolean) => {
patchPipelineMutation.mutate({
params: { path: { project_id: projectId } },
- body: { data_collection_policies: [{ rate: defaultRate, enabled: isEnabled }] },
+ body: { data_collection_policies: newPolicies },
});
};
- const updateRate = (value: number) => {
- patchPipelineMutation.mutate({
- params: { path: { project_id: projectId } },
- body: { data_collection_policies: [{ rate: value, enabled: isAutoCapturingEnabled }] },
- });
- };
+ const isDisabled = patchPipelineMutation.isPending || !canEditPipeline;
return (
{
Data collection
+
+ Capture rate
+
+
+ Capture frames while the stream is running
+
updatePolicies({ rateEnabled: enabled })}
+ isDisabled={isDisabled}
marginBottom={'size-200'}
>
Toggle auto capturing
- Capture frames while the stream is running
+ updatePolicies({ rate })}
+ label='Rate'
+ isDisabled={isDisabled || !ratePolicy?.enabled}
+ />
-
- Capture rate
+
+ Confidence threshold
+ Capture frames when confidence is below threshold
+
+ updatePolicies({ confidenceEnabled: enabled })}
+ isDisabled={isDisabled}
+ >
+ Confidence threshold
+
+
updatePolicies({ confidenceThreshold })}
marginY={'size-200'}
- label='Rate'
- isDisabled={patchPipelineMutation.isPending || !canEditPipeline}
+ label='Threshold'
+ isDisabled={isDisabled || !confidencePolicy?.enabled}
/>
diff --git a/application/ui/src/features/inference/aside/graphs.component.tsx b/application/ui/src/features/inference/aside/graphs.component.tsx
index e7c9e3ff13..d5a669f920 100644
--- a/application/ui/src/features/inference/aside/graphs.component.tsx
+++ b/application/ui/src/features/inference/aside/graphs.component.tsx
@@ -3,64 +3,71 @@
import { useEffect, useRef, useState } from 'react';
-import { Flex, Grid, Heading, View } from '@geti/ui';
+import { Content, Flex, Grid, Heading, IllustratedMessage, View } from '@geti/ui';
+import { useProjectIdentifier } from 'hooks/use-project-identifier.hook';
import { CartesianGrid, Label, Line, LineChart, ReferenceLine, XAxis, YAxis } from 'recharts';
-const generateData = (n: number) => {
- const result = new Array(n);
- result[0] = 100;
-
- for (let idx = 1; idx < n; idx++) {
- const dx = 1 + Math.random() * 0.3 - 0.15;
- result[idx] = Math.max(0, Math.min(100, result[idx - 1] * dx));
- }
-
- return result;
-};
-
-const useData = () => {
- const [data, setData] = useState(
- generateData(60).map((value, idx) => {
- return { name: `${idx}`, value };
- })
+import { $api } from '../../../api/client';
+
+interface DataPoint {
+ name: string;
+ value: number;
+}
+
+const POLLING_INTERVAL = 5000;
+const MAX_DATA_POINTS = 60; // Keep last 60 data points
+
+const useMetricsData = (projectId: string) => {
+ const [latencyData, setLatencyData] = useState([]);
+ const [throughputData, setThroughputData] = useState([]);
+ const counterRef = useRef(0);
+
+ const { data: metrics } = $api.useQuery(
+ 'get',
+ '/api/projects/{project_id}/pipeline/metrics',
+ {
+ params: { path: { project_id: projectId } },
+ },
+ {
+ refetchInterval: (query) => (query.state.status === 'success' ? POLLING_INTERVAL : false),
+ retry: false,
+ }
);
- const timeout = useRef>(undefined);
-
useEffect(() => {
- timeout.current = setTimeout(
- () => {
- const newData = data.slice(1);
- const previous = newData.at(-1);
-
- if (!previous) {
- return;
- }
-
- const dx = 1 + Math.random() * 0.3 - 0.15;
- newData.push({
- name: `${Number(previous.name) + 1}`,
- value: Math.max(0, Math.min(100, previous.value * dx)),
- });
-
- setData(newData);
- },
- Math.random() * 100 + 250
- );
-
- return () => {
- if (timeout.current) {
- clearTimeout(timeout.current);
- }
- };
- });
-
- return data;
+ if (!metrics) return;
+
+ const dataPointName = `${counterRef.current++}`;
+
+ setLatencyData((prev) => {
+ const newData = [
+ ...prev,
+ {
+ name: dataPointName,
+ value: metrics.inference.latency.avg_ms ?? 0,
+ },
+ ];
+
+ // Keep only last MAX_DATA_POINTS
+ return newData.slice(-MAX_DATA_POINTS);
+ });
+
+ setThroughputData((prev) => {
+ const newData = [
+ ...prev,
+ {
+ name: dataPointName,
+ value: metrics.inference.throughput.avg_requests_per_second ?? 0,
+ },
+ ];
+ return newData.slice(-MAX_DATA_POINTS);
+ });
+ }, [metrics]);
+
+ return { latencyData, throughputData, metrics };
};
-const Graph = ({ label }: { label: string }) => {
- const data = useData();
-
+const Graph = ({ label, data }: { label: string; data: DataPoint[] }) => {
return (
@@ -70,16 +77,8 @@ const Graph = ({ label }: { label: string }) => {
dataKey='name'
tickLine={false}
tickMargin={8}
- // tickFormatter={(value) => {
- // const date = new Date(value);
- // return date.toLocaleDateString('en-US', {
- // month: 'short',
- // day: 'numeric',
- // });
- // }}
/>
{
}}
>
- {/* */}
-
+ {data.length > 0 && (
+
+ )}
{
};
export const Graphs = () => {
+ const projectId = useProjectIdentifier();
+ const { latencyData, throughputData, metrics } = useMetricsData(projectId);
+
+ const hasData = latencyData.length > 0 || throughputData.length > 0;
+
return (
{
Model statistics
- {/* TODO: Extract these into a shared component */}
-
-
- Throughput
-
-
-
-
-
- Latency per frame
-
-
-
-
-
- Model confidence
-
-
-
-
-
- Resource utilization
-
-
-
+ {!hasData && !metrics ? (
+
+ No statistics available
+
+ Model statistics will appear here once the pipeline starts running and starts processing
+ data.
+
+
+ ) : (
+ <>
+
+
+ Throughput
+
+
+
+
+
+ Latency
+
+
+
+ >
+ )}
);
diff --git a/application/ui/src/features/models/page-header.component.tsx b/application/ui/src/features/models/page-header.component.tsx
index 80e37887d9..bf3ccbc24e 100644
--- a/application/ui/src/features/models/page-header.component.tsx
+++ b/application/ui/src/features/models/page-header.component.tsx
@@ -1,7 +1,7 @@
// Copyright (C) 2025 Intel Corporation
// SPDX-License-Identifier: Apache-2.0
-import { Flex, Grid, Item, Picker, SearchField, Text } from '@geti/ui';
+import { Flex, Grid, Item, Picker, Text } from '@geti/ui';
export const ModelsHeader = () => {
return (
@@ -18,7 +18,6 @@ export const ModelsHeader = () => {
- Sort: Architecture
-
);
};
diff --git a/application/ui/src/providers.tsx b/application/ui/src/providers.tsx
index 6b50984b8d..329755368a 100644
--- a/application/ui/src/providers.tsx
+++ b/application/ui/src/providers.tsx
@@ -29,7 +29,10 @@ export const queryClient = new QueryClient({
queries: {
gcTime: 30 * 60 * 1000,
staleTime: 5 * 60 * 1000,
- retry: 0,
+ retry: false,
+ },
+ mutations: {
+ retry: false,
},
},
mutationCache: new MutationCache({
diff --git a/application/ui/tests/inference.spec.ts b/application/ui/tests/inference.spec.ts
index f28c8f5080..547942b807 100644
--- a/application/ui/tests/inference.spec.ts
+++ b/application/ui/tests/inference.spec.ts
@@ -84,12 +84,11 @@ test.describe('Inference', () => {
// Open both tabs just to make sure everything works
await page.getByRole('button', { name: 'Toggle Model statistics tab' }).click();
- await expect(page.getByText('Model statistics')).toBeVisible();
+ await expect(page.getByText('Model statistics', { exact: true })).toBeVisible();
await page.getByRole('button', { name: 'Toggle Data collection policy' }).click();
await expect(page.getByRole('heading', { name: 'Data collection' })).toBeVisible();
- // Update values
await expect(page.getByRole('switch', { name: 'Toggle auto capturing' })).not.toBeChecked();
network.use(
@@ -105,6 +104,12 @@ test.describe('Inference', () => {
enabled: true,
rate: 12,
},
+ {
+ type: 'confidence_threshold',
+ enabled: false,
+ confidence_threshold: 0.5,
+ min_sampling_interval: 2.5,
+ },
],
})
);
@@ -116,29 +121,86 @@ test.describe('Inference', () => {
network.use(
http.get('/api/projects/{project_id}/pipeline', ({ response }) => {
- return response(200).json({
- project_id: 'id-1',
- status: 'idle',
- source: null,
- sink: null,
- model: null,
- data_collection_policies: [
- {
- type: 'fixed_rate',
- enabled: true,
- rate: 20,
- },
- ],
- });
+ return response(200).json(
+ getMockedPipeline({
+ data_collection_policies: [
+ {
+ type: 'fixed_rate',
+ enabled: true,
+ rate: 20,
+ },
+ {
+ type: 'confidence_threshold',
+ enabled: false,
+ confidence_threshold: 0.5,
+ min_sampling_interval: 2.5,
+ },
+ ],
+ })
+ );
})
);
- const sliderInput = page.locator('input[type="range"]');
+ const rateSlider = page.getByRole('slider', { name: 'Rate' });
+ await expect(rateSlider).toBeVisible();
+ await expect(rateSlider).toBeEnabled();
+ await rateSlider.fill('20');
+ await expect(rateSlider).toHaveValue('20');
+
+ await expect(page.getByRole('switch', { name: 'Confidence threshold' })).not.toBeChecked();
- await expect(sliderInput).toBeVisible();
- await sliderInput.fill('20');
+ network.use(
+ http.get('/api/projects/{project_id}/pipeline', ({ response }) => {
+ return response(200).json(
+ getMockedPipeline({
+ data_collection_policies: [
+ {
+ type: 'fixed_rate',
+ enabled: true,
+ rate: 20,
+ },
+ {
+ type: 'confidence_threshold',
+ enabled: true,
+ confidence_threshold: 0.5,
+ min_sampling_interval: 2.5,
+ },
+ ],
+ })
+ );
+ })
+ );
+
+ await page.getByRole('switch', { name: 'Confidence threshold' }).click();
+ await expect(page.getByRole('switch', { name: 'Confidence threshold' })).toBeChecked();
+
+ network.use(
+ http.get('/api/projects/{project_id}/pipeline', ({ response }) => {
+ return response(200).json(
+ getMockedPipeline({
+ data_collection_policies: [
+ {
+ type: 'fixed_rate',
+ enabled: true,
+ rate: 20,
+ },
+ {
+ type: 'confidence_threshold',
+ enabled: true,
+ confidence_threshold: 0.7,
+ min_sampling_interval: 2.5,
+ },
+ ],
+ })
+ );
+ })
+ );
- await expect(sliderInput).toHaveValue('20');
+ const confidenceSlider = page.getByRole('slider', { name: 'Threshold' });
+ await expect(confidenceSlider).toBeVisible();
+ await expect(confidenceSlider).toBeEnabled();
+ await confidenceSlider.fill('0.7');
+ await expect(confidenceSlider).toHaveValue('0.7');
});
test('updates input and output source', async ({ page, network }) => {