import { CircuitBoard, Cpu, Thermometer, Zap } from 'lucide-react'; import React, { memo, useCallback, useEffect, useMemo, useState } from 'react'; import { useI18n } from '../../application/i18n/I18nProvider'; import type { useSystemManagerBackend } from '../../application/state/useSystemManagerBackend'; import type { AcceleratorDeviceInfo, AcceleratorProcessInfo, AcceleratorSnapshot, } from '../../domain/systemManager/types'; import { cn } from '../../lib/utils'; import { ResourceBar } from './ResourceBar'; import { GpuVendorBadge, vendorDisplayLabel } from './GpuVendorBadge'; import { SystemPanelEmpty, SystemPanelError, SystemPanelInlineError, SystemPanelList, SystemPanelLoading, SystemPanelRefreshButton, SystemPanelRow, SystemPanelShell, SystemPanelStatusBadge, SystemPanelToolbar, } from './SystemPanelUi'; import { usePolling, useStableTranslate } from '../../application/state/useSystemManager'; type Backend = ReturnType; interface GpuManagerTabProps { sessionId: string; isVisible: boolean; backend: Backend; refreshIntervalSec: number; } const HISTORY_LIMIT = 24; interface DeviceHistorySample { util: number; memory: number; } function formatMb(mb: number | null | undefined): string { if (!Number.isFinite(mb)) return '--'; const value = Number(mb); if (value >= 1024) return `${(value / 1024).toFixed(1)} GB`; return `${Math.round(value)} MB`; } function memoryPercent(device: AcceleratorDeviceInfo): number | null { if (!Number.isFinite(device.memoryUsedMb) || !Number.isFinite(device.memoryTotalMb)) return null; if (!device.memoryTotalMb || device.memoryTotalMb <= 0) return null; return Math.max(0, Math.min(100, (Number(device.memoryUsedMb) / Number(device.memoryTotalMb)) * 100)); } function deviceKey(device: AcceleratorDeviceInfo): string { return `${device.vendor}-${device.index}-${device.uuid || device.name}`; } /** Compact nvtop-style sparkline (area + stroke), reusing the overview chart feel. */ function GpuSparkline({ values, className, }: { values: number[]; className?: string; }) { const width = 120; const height = 28; const safeValues = values.length > 1 ? values : [values[0] ?? 0, values[0] ?? 0]; const points = safeValues.map((value, index) => { const x = safeValues.length === 1 ? width : (index / (safeValues.length - 1)) * width; const clamped = Math.max(0, Math.min(100, Number.isFinite(value) ? value : 0)); const y = height - (clamped / 100) * (height - 4) - 2; return `${x.toFixed(1)},${y.toFixed(1)}`; }); const area = `M0,${height} L${points.join(' L')} L${width},${height} Z`; return ( ); } const DeviceCard = memo(function DeviceCard({ device, utilHistory, memHistory, }: { device: AcceleratorDeviceInfo; utilHistory: number[]; memHistory: number[]; }) { const { t } = useI18n(); const memPct = memoryPercent(device); const util = Number.isFinite(device.utilizationPercent) ? Number(device.utilizationPercent) : null; const tone = device.vendor === 'ascend' ? 'text-orange-500' : 'text-emerald-500'; return (
[{device.index}] {device.name} {device.health ? ( {device.health} ) : null}
{device.driverVersion ? (
{t('systemManager.gpu.driver', { version: device.driverVersion })}
) : null}
{Number.isFinite(device.temperatureC) ? ( {Math.round(Number(device.temperatureC))}°C ) : null} {Number.isFinite(device.powerDrawW) ? ( {Number(device.powerDrawW).toFixed(0)} {Number.isFinite(device.powerLimitW) ? `/${Math.round(Number(device.powerLimitW))}` : ''} W ) : null}
{formatMb(device.memoryUsedMb)} / {formatMb(device.memoryTotalMb)}
{/* History sparklines sit under the bars so they do not compete for row width. */}
{t('systemManager.gpu.util')}
{device.vendor === 'ascend' ? t('systemManager.gpu.hbm') : t('systemManager.gpu.memory')}
{Number.isFinite(device.fanPercent) ? (
{t('systemManager.gpu.fan', { value: Math.round(Number(device.fanPercent)) })}
) : null}
); }); const ProcessRow = memo(function ProcessRow({ process, }: { process: AcceleratorProcessInfo; }) { const { t } = useI18n(); return ( PID {process.pid} {formatMb(process.memoryUsedMb)} )} /> ); }); export const GpuManagerTab = memo(function GpuManagerTab({ sessionId, isVisible, backend, refreshIntervalSec, }: GpuManagerTabProps) { const { t } = useI18n(); const stableT = useStableTranslate(); const intervalMs = Math.max(2, refreshIntervalSec) * 1000; const [gpuListPending, setGpuListPending] = useState(false); const [historyByDevice, setHistoryByDevice] = useState>({}); useEffect(() => { setGpuListPending(false); setHistoryByDevice({}); }, [sessionId]); const fetcher = useCallback(async (): Promise => { const result = await backend.listAccelerators(sessionId); if (result.pending) { setGpuListPending(true); return null; } setGpuListPending(false); if (!result.success) { throw new Error(result.error || stableT('systemManager.errors.loadGpu')); } return { devices: result.devices || [], processes: result.processes || [], nvidiaDriverVersion: result.nvidiaDriverVersion ?? null, probedAt: result.probedAt || Date.now(), }; }, [backend, sessionId, stableT]); const { data, error, loading, refresh } = usePolling( fetcher, intervalMs, isVisible, undefined, { resetKey: sessionId }, ); const devices = useMemo(() => data?.devices ?? [], [data?.devices]); const processes = useMemo(() => data?.processes ?? [], [data?.processes]); const isRefreshActive = loading || gpuListPending; useEffect(() => { if (!isVisible || !devices.length) return; setHistoryByDevice((prev) => { const next: Record = { ...prev }; for (const device of devices) { const key = deviceKey(device); const sample: DeviceHistorySample = { util: Number.isFinite(device.utilizationPercent) ? Number(device.utilizationPercent) : 0, memory: memoryPercent(device) ?? 0, }; const series = [...(next[key] || []), sample].slice(-HISTORY_LIMIT); next[key] = series; } return next; }); }, [devices, isVisible, data?.probedAt]); const meta = useMemo(() => { const nvidiaCount = devices.filter((d) => d.vendor === 'nvidia').length; const ascendCount = devices.filter((d) => d.vendor === 'ascend').length; return t('systemManager.gpu.meta', { devices: devices.length, processes: processes.length, nvidia: nvidiaCount, ascend: ascendCount, }); }, [devices, processes, t]); if (!isVisible && !data) { return null; } if (!data) { if (error && !isRefreshActive) { return ( void refresh()} /> ); } return ( ); } if (!devices.length && !processes.length) { return ( void refresh()} /> )}> {meta} ); } return ( void refresh()} /> )}> {meta} {error && !isRefreshActive ? ( void refresh()} /> ) : null}
{t('systemManager.gpu.devices')}
{devices.length === 0 ? (
{t('systemManager.gpu.empty')}
) : ( devices.map((device) => { const key = deviceKey(device); const series = historyByDevice[key] || []; return ( sample.util)} memHistory={series.map((sample) => sample.memory)} /> ); }) )}
{t('systemManager.gpu.processes')}
{processes.length === 0 ? (
{t('systemManager.gpu.noProcesses')}
) : ( processes.map((process) => ( )) )}
); });