Frugalité : supprime le CPU consommé à vide (sondage groupé et adaptatif)

À vide, l'app chauffait le CPU en continu : trois sondages navigateur
séparés (5/8/10 s) tournaient en permanence, y compris onglet caché, et
chaque tick relançait un sous-processus nvidia-smi (cache TTL 5 s = la
cadence, donc jamais efficace) plus deux connexions vers Ollama.

- Nouveau /api/system/pulse : statut + ressources + modèles chargés en
  UNE requête (appels Ollama en parallèle, dégradation propre si muet),
  avec un cache court qui absorbe les rafales multi-onglets.
- Front : un seul sondage, SUSPENDU quand l'onglet est caché, cadence
  adaptative (4 s pendant un flux, 20 s au repos) et rafraîchissement
  immédiat au retour sur l'onglet.
- Panneau Matériel : 5 s -> 15 s et rien onglet caché.
- Caches serveur alignés sur la cadence : nvidia-smi mis en cache 12 s,
  et _all_local_gpus (vue Matériel) n'était pas caché du tout.

Effet au repos : ~25 requêtes/min et ~12 nvidia-smi/min -> ~3 et ~3
onglet visible, et ZÉRO onglet caché. Le serveur n'a aucune boucle de
fond : sans navigateur ouvert, il ne consomme plus rien.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SVay7z3y7q2gEe54ByAE6N
This commit is contained in:
Claude committed 2026-08-03 05:40:22 +00:00
1 parent e7797b1b09
commit 9077bd2422
5 files changed
+150 -25

No files matched your search

+37 -22
View File
@@ -10,33 +10,48 @@ import { useStore } from "./store/useStore";
export default function App() {
const [view, setView] = useState<View>("chat");
const {
refreshStatus,
refreshSystemStats,
refreshModels,
refreshLoadedModels,
refreshSessions,
} = useStore();
const { refreshPulse, refreshModels, refreshSessions } = useStore();
// Au démarrage : statut Ollama + modèles + sessions. La config est chargée
// par setSelectedModel (via refreshModels) — pas de double fetch. Poll du
// statut, des stats système (5 s, apparié au cache GPU serveur) et des
// modèles chargés en VRAM (indicateur de préchargement).
// Sondage frugal. Trois principes, contre l'app qui chauffait le CPU à vide :
// - UNE seule requête groupée (/api/system/pulse) au lieu de trois ;
// - RIEN quand l'onglet est caché (l'utilisateur ne regarde pas) ;
// - cadence adaptative : réactif pendant le travail, lent au repos.
// Le serveur, lui, ne tourne jamais en boucle : sans navigateur, zéro CPU.
useEffect(() => {
refreshStatus();
refreshSystemStats();
refreshPulse();
refreshModels();
refreshLoadedModels();
refreshSessions();
const statusId = setInterval(refreshStatus, 10000);
const statsId = setInterval(refreshSystemStats, 5000);
const loadedId = setInterval(refreshLoadedModels, 8000);
return () => {
clearInterval(statusId);
clearInterval(statsId);
clearInterval(loadedId);
let timer: number | undefined;
const tick = () => {
// Onglet caché : on ne sonde pas du tout, on repassera au retour.
if (document.hidden) return schedule();
refreshPulse().finally(schedule);
};
}, [refreshStatus, refreshSystemStats, refreshModels, refreshLoadedModels, refreshSessions]);
const schedule = () => {
if (document.hidden) {
timer = window.setTimeout(schedule, 30000);
return;
}
// Pendant un flux, les indicateurs (VRAM, GPU) doivent suivre.
const busy = useStore.getState().streaming;
timer = window.setTimeout(tick, busy ? 4000 : 20000);
};
schedule();
// Retour sur l'onglet : rafraîchit tout de suite plutôt que d'attendre.
const onVisible = () => {
if (!document.hidden) {
window.clearTimeout(timer);
tick();
}
};
document.addEventListener("visibilitychange", onVisible);
return () => {
window.clearTimeout(timer);
document.removeEventListener("visibilitychange", onVisible);
};
}, [refreshPulse, refreshModels, refreshSessions]);
return (
<div className="flex h-full flex-col bg-base text-ink">
+13
View File
@@ -41,6 +41,19 @@ export async function getSystemStats(): Promise<SystemStats> {
return res.json();
}
/** Battement groupé : statut + ressources + modèles chargés en UNE requête. */
export interface Pulse {
status: OllamaStatus;
stats: SystemStats;
loaded: LoadedModel[];
}
export async function getPulse(): Promise<Pulse> {
const res = await fetch("/api/system/pulse");
if (!res.ok) throw new Error(`pulse ${res.status}`);
return res.json();
}
// ── Git du workspace ─────────────────────────────────────────────────────
export interface GitCommit {
hash: string;
+5 -1
View File
@@ -627,9 +627,13 @@ function HardwareCard() {
const [hw, setHw] = useState<HardwareInfo | null>(null);
const refresh = () => getHardware().then(setHw).catch(() => {});
// 15 s et rien quand l'onglet est caché : chaque appel lance nvidia-smi côté
// serveur. Le bouton ↻ reste là pour un rafraîchissement immédiat.
useEffect(() => {
refresh();
const id = setInterval(refresh, 5000);
const id = setInterval(() => {
if (!document.hidden) refresh();
}, 15000);
return () => clearInterval(id);
}, []);
+17
View File
@@ -11,6 +11,7 @@ import {
getStatus,
getSystemStats,
getLoadedModels,
getPulse,
warmModel,
fileContent,
listFiles,
@@ -71,6 +72,7 @@ interface LokiState {
refreshStatus: () => Promise<void>;
refreshSystemStats: () => Promise<void>;
refreshLoadedModels: () => Promise<void>;
refreshPulse: () => Promise<void>;
refreshModels: () => Promise<void>;
refreshFiles: () => Promise<void>;
@@ -261,6 +263,21 @@ export const useStore = create<LokiState>((set, get) => ({
set({ loadedModels: await getLoadedModels() });
},
// Sondage unique (statut + ressources + modèles chargés). Une seule requête
// au lieu de trois, et une seule mise à jour du store : moins de réveils
// navigateur et moins de rendus React quand l'app est ouverte à vide.
refreshPulse: async () => {
try {
const { status, stats, loaded } = await getPulse();
set({ status, systemStats: stats, loadedModels: loaded });
} catch {
set({
status: { connected: false, host: "", default_model: "" },
systemStats: null,
});
}
},
refreshModels: async () => {
set({ loadingModels: true });
try {