fix(models): use useReducer for token usage fetch state to fix lint errors and reduce loading time (#586)
This commit is contained in:
committed by
GitHub
Unverified
parent
1b527d2f49
commit
73343d16ea
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useReducer, useRef, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import {
|
import {
|
||||||
ChevronLeft,
|
ChevronLeft,
|
||||||
@@ -19,8 +19,8 @@ import {
|
|||||||
type UsageHistoryEntry,
|
type UsageHistoryEntry,
|
||||||
type UsageWindow,
|
type UsageWindow,
|
||||||
} from './usage-history';
|
} from './usage-history';
|
||||||
const DEFAULT_USAGE_FETCH_MAX_ATTEMPTS = 6;
|
const DEFAULT_USAGE_FETCH_MAX_ATTEMPTS = 2;
|
||||||
const WINDOWS_USAGE_FETCH_MAX_ATTEMPTS = 10;
|
const WINDOWS_USAGE_FETCH_MAX_ATTEMPTS = 3;
|
||||||
const USAGE_FETCH_RETRY_DELAY_MS = 1500;
|
const USAGE_FETCH_RETRY_DELAY_MS = 1500;
|
||||||
|
|
||||||
export function Models() {
|
export function Models() {
|
||||||
@@ -32,23 +32,39 @@ export function Models() {
|
|||||||
? WINDOWS_USAGE_FETCH_MAX_ATTEMPTS
|
? WINDOWS_USAGE_FETCH_MAX_ATTEMPTS
|
||||||
: DEFAULT_USAGE_FETCH_MAX_ATTEMPTS;
|
: DEFAULT_USAGE_FETCH_MAX_ATTEMPTS;
|
||||||
|
|
||||||
const [usageHistory, setUsageHistory] = useState<UsageHistoryEntry[]>([]);
|
|
||||||
const [usageGroupBy, setUsageGroupBy] = useState<UsageGroupBy>('model');
|
const [usageGroupBy, setUsageGroupBy] = useState<UsageGroupBy>('model');
|
||||||
const [usageWindow, setUsageWindow] = useState<UsageWindow>('7d');
|
const [usageWindow, setUsageWindow] = useState<UsageWindow>('7d');
|
||||||
const [usagePage, setUsagePage] = useState(1);
|
const [usagePage, setUsagePage] = useState(1);
|
||||||
const [selectedUsageEntry, setSelectedUsageEntry] = useState<UsageHistoryEntry | null>(null);
|
const [selectedUsageEntry, setSelectedUsageEntry] = useState<UsageHistoryEntry | null>(null);
|
||||||
const [usageFetchDoneKey, setUsageFetchDoneKey] = useState<string | null>(null);
|
|
||||||
|
type FetchState = {
|
||||||
|
status: 'idle' | 'loading' | 'done';
|
||||||
|
data: UsageHistoryEntry[];
|
||||||
|
};
|
||||||
|
type FetchAction =
|
||||||
|
| { type: 'start' }
|
||||||
|
| { type: 'done'; data: UsageHistoryEntry[] }
|
||||||
|
| { type: 'reset' };
|
||||||
|
|
||||||
|
const [fetchState, dispatchFetch] = useReducer(
|
||||||
|
(state: FetchState, action: FetchAction): FetchState => {
|
||||||
|
switch (action.type) {
|
||||||
|
case 'start':
|
||||||
|
return { status: 'loading', data: state.data };
|
||||||
|
case 'done':
|
||||||
|
return { status: 'done', data: action.data };
|
||||||
|
case 'reset':
|
||||||
|
return { status: 'idle', data: [] };
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ status: 'idle' as const, data: [] as UsageHistoryEntry[] },
|
||||||
|
);
|
||||||
|
|
||||||
const usageFetchTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
const usageFetchTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
const usageFetchGenerationRef = useRef(0);
|
const usageFetchGenerationRef = useRef(0);
|
||||||
|
|
||||||
// Stable key derived from the effect's dependencies — changes whenever a new
|
|
||||||
// fetch cycle should start. Comparing this to `usageFetchDoneKey` lets us
|
|
||||||
// derive the loading state without calling setState in the effect body or
|
|
||||||
// reading refs during render.
|
|
||||||
const usageFetchKey = isGatewayRunning
|
|
||||||
? `${gatewayStatus.pid ?? 'na'}:${gatewayStatus.connectedAt ?? 'na'}:${usageFetchMaxAttempts}`
|
|
||||||
: null;
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
trackUiEvent('models.page_viewed');
|
trackUiEvent('models.page_viewed');
|
||||||
}, []);
|
}, []);
|
||||||
@@ -60,10 +76,11 @@ export function Models() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!isGatewayRunning) {
|
if (!isGatewayRunning) {
|
||||||
|
dispatchFetch({ type: 'reset' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchKey = `${gatewayStatus.pid ?? 'na'}:${gatewayStatus.connectedAt ?? 'na'}:${usageFetchMaxAttempts}`;
|
dispatchFetch({ type: 'start' });
|
||||||
const generation = usageFetchGenerationRef.current + 1;
|
const generation = usageFetchGenerationRef.current + 1;
|
||||||
usageFetchGenerationRef.current = generation;
|
usageFetchGenerationRef.current = generation;
|
||||||
const restartMarker = `${gatewayStatus.pid ?? 'na'}:${gatewayStatus.connectedAt ?? 'na'}`;
|
const restartMarker = `${gatewayStatus.pid ?? 'na'}:${gatewayStatus.connectedAt ?? 'na'}`;
|
||||||
@@ -72,6 +89,17 @@ export function Models() {
|
|||||||
restartMarker,
|
restartMarker,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Safety timeout: if the fetch cycle hasn't resolved after 30 s,
|
||||||
|
// force-resolve to "done" with empty data to avoid an infinite spinner.
|
||||||
|
const safetyTimeout = setTimeout(() => {
|
||||||
|
if (usageFetchGenerationRef.current !== generation) return;
|
||||||
|
trackUiEvent('models.token_usage_fetch_safety_timeout', {
|
||||||
|
generation,
|
||||||
|
restartMarker,
|
||||||
|
});
|
||||||
|
dispatchFetch({ type: 'done', data: [] });
|
||||||
|
}, 30_000);
|
||||||
|
|
||||||
const fetchUsageHistoryWithRetry = async (attempt: number) => {
|
const fetchUsageHistoryWithRetry = async (attempt: number) => {
|
||||||
trackUiEvent('models.token_usage_fetch_attempt', {
|
trackUiEvent('models.token_usage_fetch_attempt', {
|
||||||
generation,
|
generation,
|
||||||
@@ -83,7 +111,6 @@ export function Models() {
|
|||||||
if (usageFetchGenerationRef.current !== generation) return;
|
if (usageFetchGenerationRef.current !== generation) return;
|
||||||
|
|
||||||
const normalized = Array.isArray(entries) ? entries : [];
|
const normalized = Array.isArray(entries) ? entries : [];
|
||||||
setUsageHistory(normalized);
|
|
||||||
setUsagePage(1);
|
setUsagePage(1);
|
||||||
trackUiEvent('models.token_usage_fetch_succeeded', {
|
trackUiEvent('models.token_usage_fetch_succeeded', {
|
||||||
generation,
|
generation,
|
||||||
@@ -111,7 +138,7 @@ export function Models() {
|
|||||||
restartMarker,
|
restartMarker,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
setUsageFetchDoneKey(fetchKey);
|
dispatchFetch({ type: 'done', data: normalized });
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (usageFetchGenerationRef.current !== generation) return;
|
if (usageFetchGenerationRef.current !== generation) return;
|
||||||
@@ -133,8 +160,7 @@ export function Models() {
|
|||||||
}, USAGE_FETCH_RETRY_DELAY_MS);
|
}, USAGE_FETCH_RETRY_DELAY_MS);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setUsageHistory([]);
|
dispatchFetch({ type: 'done', data: [] });
|
||||||
setUsageFetchDoneKey(fetchKey);
|
|
||||||
trackUiEvent('models.token_usage_fetch_exhausted', {
|
trackUiEvent('models.token_usage_fetch_exhausted', {
|
||||||
generation,
|
generation,
|
||||||
attempt,
|
attempt,
|
||||||
@@ -147,6 +173,7 @@ export function Models() {
|
|||||||
void fetchUsageHistoryWithRetry(1);
|
void fetchUsageHistoryWithRetry(1);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
clearTimeout(safetyTimeout);
|
||||||
if (usageFetchTimerRef.current) {
|
if (usageFetchTimerRef.current) {
|
||||||
clearTimeout(usageFetchTimerRef.current);
|
clearTimeout(usageFetchTimerRef.current);
|
||||||
usageFetchTimerRef.current = null;
|
usageFetchTimerRef.current = null;
|
||||||
@@ -154,6 +181,7 @@ export function Models() {
|
|||||||
};
|
};
|
||||||
}, [isGatewayRunning, gatewayStatus.connectedAt, gatewayStatus.pid, usageFetchMaxAttempts]);
|
}, [isGatewayRunning, gatewayStatus.connectedAt, gatewayStatus.pid, usageFetchMaxAttempts]);
|
||||||
|
|
||||||
|
const usageHistory = fetchState.data;
|
||||||
const visibleUsageHistory = isGatewayRunning ? usageHistory : [];
|
const visibleUsageHistory = isGatewayRunning ? usageHistory : [];
|
||||||
const filteredUsageHistory = filterUsageHistoryByWindow(visibleUsageHistory, usageWindow);
|
const filteredUsageHistory = filterUsageHistoryByWindow(visibleUsageHistory, usageWindow);
|
||||||
const usageGroups = groupUsageHistory(filteredUsageHistory, usageGroupBy);
|
const usageGroups = groupUsageHistory(filteredUsageHistory, usageGroupBy);
|
||||||
@@ -161,7 +189,7 @@ export function Models() {
|
|||||||
const usageTotalPages = Math.max(1, Math.ceil(filteredUsageHistory.length / usagePageSize));
|
const usageTotalPages = Math.max(1, Math.ceil(filteredUsageHistory.length / usagePageSize));
|
||||||
const safeUsagePage = Math.min(usagePage, usageTotalPages);
|
const safeUsagePage = Math.min(usagePage, usageTotalPages);
|
||||||
const pagedUsageHistory = filteredUsageHistory.slice((safeUsagePage - 1) * usagePageSize, safeUsagePage * usagePageSize);
|
const pagedUsageHistory = filteredUsageHistory.slice((safeUsagePage - 1) * usagePageSize, safeUsagePage * usagePageSize);
|
||||||
const usageLoading = isGatewayRunning && usageFetchDoneKey !== usageFetchKey;
|
const usageLoading = isGatewayRunning && fetchState.status === 'loading';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col -m-6 dark:bg-background h-[calc(100vh-2.5rem)] overflow-hidden">
|
<div className="flex flex-col -m-6 dark:bg-background h-[calc(100vh-2.5rem)] overflow-hidden">
|
||||||
|
|||||||
Reference in New Issue
Block a user