From 280ae26d775a97723c86b528795b666d1b964e84 Mon Sep 17 00:00:00 2001 From: lone-cloud Date: Wed, 10 Sep 2025 17:02:33 -0700 Subject: [PATCH] changing metrics because windows sucks --- eslint.config.ts | 1 + package.json | 2 +- src/components/StatusBar.tsx | 76 +++++--- src/main/ipc.ts | 9 +- src/main/modules/hardware.ts | 40 ++-- src/main/modules/monitoring.ts | 342 +++++++++------------------------ src/main/utils/assets.ts | 0 src/preload/index.ts | 20 +- src/types/electron.d.ts | 14 +- src/utils/gpu.ts | 235 ++++++++++++++++++++++ yarn.lock | 10 +- 11 files changed, 438 insertions(+), 311 deletions(-) delete mode 100644 src/main/utils/assets.ts create mode 100644 src/utils/gpu.ts diff --git a/eslint.config.ts b/eslint.config.ts index b125c1b..f51011a 100644 --- a/eslint.config.ts +++ b/eslint.config.ts @@ -144,6 +144,7 @@ const config = [ 'arrow-body-style': ['error', 'as-needed'], 'prefer-arrow-callback': ['error', { allowNamedFunctions: false }], 'object-shorthand': ['error', 'always'], + 'prefer-const': 'error', 'no-console': 'error', diff --git a/package.json b/package.json index 02eb663..b6627d2 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "@typescript-eslint/parser": "^8.43.0", "@vitejs/plugin-react": "^5.0.2", "cross-env": "^10.0.0", - "electron": "^38.0.0", + "electron": "^38.1.0", "electron-builder": "^26.0.12", "electron-vite": "^4.0.0", "eslint": "^9.35.0", diff --git a/src/components/StatusBar.tsx b/src/components/StatusBar.tsx index 9a8b958..cc7d996 100644 --- a/src/components/StatusBar.tsx +++ b/src/components/StatusBar.tsx @@ -13,7 +13,11 @@ import { Settings } from 'lucide-react'; import { SettingsModal } from '@/components/settings/SettingsModal'; import { safeExecute } from '@/utils/logger'; import type { FrontendPreference, Screen } from '@/types'; -import type { SystemMetrics } from '@/main/modules/monitoring'; +import type { + CpuMetrics, + MemoryMetrics, + GpuMetrics, +} from '@/main/modules/monitoring'; interface StatusBarProps { maxDataPoints?: number; @@ -28,9 +32,11 @@ export const StatusBar = ({ frontendPreference: _frontendPreference, onFrontendPreferenceChange, }: StatusBarProps) => { - const [currentMetrics, setCurrentMetrics] = useState( + const [cpuMetrics, setCpuMetrics] = useState(null); + const [memoryMetrics, setMemoryMetrics] = useState( null ); + const [gpuMetrics, setGpuMetrics] = useState(null); const [settingsModalOpen, setSettingsModalOpen] = useState(false); const theme = useMantineTheme(); const colorScheme = useComputedColorScheme('light', { @@ -40,29 +46,42 @@ export const StatusBar = ({ useEffect(() => { let isMounted = true; - const handleMetrics = async (newMetrics: SystemMetrics) => { + const handleCpuMetrics = async (metrics: CpuMetrics) => { if (!isMounted) return; - - setCurrentMetrics(newMetrics); + setCpuMetrics(metrics); }; - window.electronAPI.monitoring.onMetrics(handleMetrics); + const handleMemoryMetrics = async (metrics: MemoryMetrics) => { + if (!isMounted) return; + setMemoryMetrics(metrics); + }; + + const handleGpuMetrics = async (metrics: GpuMetrics) => { + if (!isMounted) return; + setGpuMetrics(metrics); + }; + + window.electronAPI.monitoring.onCpuMetrics(handleCpuMetrics); + window.electronAPI.monitoring.onMemoryMetrics(handleMemoryMetrics); + window.electronAPI.monitoring.onGpuMetrics(handleGpuMetrics); void window.electronAPI.monitoring.start(); return () => { isMounted = false; - window.electronAPI.monitoring.removeMetricsListener(); + window.electronAPI.monitoring.removeCpuMetricsListener(); + window.electronAPI.monitoring.removeMemoryMetricsListener(); + window.electronAPI.monitoring.removeGpuMetricsListener(); window.electronAPI.monitoring.stop(); }; }, [maxDataPoints]); - if (!currentMetrics) { + if (!cpuMetrics || !memoryMetrics) { return null; } return ( - - - CPU: {currentMetrics.cpu.usage.toFixed(1)}% + + + CPU: {cpuMetrics.usage.toFixed(1)}% - - RAM: {currentMetrics.memory.usage.toFixed(1)}% + + RAM: {memoryMetrics.usage.toFixed(1)}% - {currentMetrics.gpu?.map((gpu, index) => ( + {gpuMetrics?.gpus.map((gpu, index) => ( - + GPU: {gpu.usage.toFixed(1)}% - + VRAM: {gpu.memoryUsage.toFixed(1)}% diff --git a/src/main/ipc.ts b/src/main/ipc.ts index bf74594..bc6a62d 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -42,11 +42,7 @@ import { detectBackendSupport, getAvailableBackends, } from '@/main/modules/binary'; -import { - setMainWindow, - startMonitoring, - stopMonitoring, -} from '@/main/modules/monitoring'; +import { startMonitoring, stopMonitoring } from '@/main/modules/monitoring'; import type { FrontendPreference } from '@/types'; async function launchKoboldCppWithCustomFrontends(args: string[] = []) { @@ -238,8 +234,7 @@ export function setupIPCHandlers() { ipcMain.handle('monitoring:start', () => { const mainWindow = getMainWindow(); if (mainWindow) { - setMainWindow(mainWindow); - startMonitoring(); + startMonitoring(mainWindow); } }); diff --git a/src/main/modules/hardware.ts b/src/main/modules/hardware.ts index 7453524..8d3dcec 100644 --- a/src/main/modules/hardware.ts +++ b/src/main/modules/hardware.ts @@ -2,6 +2,7 @@ import si from 'systeminformation'; import { logError } from '@/main/modules/logging'; import { terminateProcess } from '@/utils/process'; +import { getGPUData } from '@/utils/gpu'; import type { CPUCapabilities, GPUCapabilities, @@ -58,35 +59,32 @@ export async function detectGPU() { } try { - const graphics = await si.graphics(); + const gpuData = await getGPUData(); let hasAMD = false; let hasNVIDIA = false; const gpuInfo: string[] = []; - for (const controller of graphics.controllers) { - if (controller.model) { - gpuInfo.push(formatDeviceName(controller.model)); + for (const gpu of gpuData) { + if (gpu.deviceName) { + gpuInfo.push(formatDeviceName(gpu.deviceName)); } - const vendor = controller.vendor?.toLowerCase() || ''; - const model = controller.model?.toLowerCase() || ''; + const deviceName = gpu.deviceName?.toLowerCase() || ''; if ( - vendor.includes('amd') || - vendor.includes('ati') || - model.includes('radeon') || - model.includes('amd') + deviceName.includes('amd') || + deviceName.includes('ati') || + deviceName.includes('radeon') ) { hasAMD = true; } if ( - vendor.includes('nvidia') || - model.includes('nvidia') || - model.includes('geforce') || - model.includes('gtx') || - model.includes('rtx') + deviceName.includes('nvidia') || + deviceName.includes('geforce') || + deviceName.includes('gtx') || + deviceName.includes('rtx') ) { hasNVIDIA = true; } @@ -434,18 +432,18 @@ export async function detectGPUMemory() { const memoryInfo: GPUMemoryInfo[] = []; try { - const graphics = await si.graphics(); + const gpuData = await getGPUData(); - for (const controller of graphics.controllers) { - if (controller.model) { - let vram = controller.vram; + for (const gpu of gpuData) { + if (gpu.deviceName) { + let vram: number | null = gpu.memoryTotal; - if (!vram || vram === 1) { + if (!vram || vram <= 1) { vram = null; } memoryInfo.push({ - deviceName: formatDeviceName(controller.model), + deviceName: formatDeviceName(gpu.deviceName), totalMemoryMB: vram, }); } diff --git a/src/main/modules/monitoring.ts b/src/main/modules/monitoring.ts index 9b396a7..9007147 100644 --- a/src/main/modules/monitoring.ts +++ b/src/main/modules/monitoring.ts @@ -1,13 +1,29 @@ import si from 'systeminformation'; import { BrowserWindow } from 'electron'; import { logError } from '@/main/modules/logging'; -import { readFile, readdir } from 'fs/promises'; -import { join } from 'path'; -import { spawn } from 'child_process'; -import { platform } from 'process'; +import { getGPUData } from '@/utils/gpu'; + +export interface CpuMetrics { + usage: number; +} + +export interface MemoryMetrics { + used: number; + total: number; + usage: number; +} + +export interface GpuMetrics { + gpus: { + name: string; + usage: number; + memoryUsed: number; + memoryTotal: number; + memoryUsage: number; + }[]; +} export interface SystemMetrics { - timestamp: number; cpu: { usage: number; }; @@ -25,275 +41,105 @@ export interface SystemMetrics { }[]; } -let interval: ReturnType | null = null; +let cpuInterval: ReturnType | null = null; +let memoryInterval: ReturnType | null = null; +let gpuInterval: ReturnType | null = null; let isRunning = false; -let updateFrequency = 1000; +const cpuUpdateFrequency = 1000; +const memoryUpdateFrequency = 1000; +const gpuUpdateFrequency = 2000; let mainWindow: BrowserWindow | null = null; -export function setMainWindow(window: BrowserWindow) { - mainWindow = window; -} - -export function startMonitoring() { +export function startMonitoring(window: BrowserWindow) { if (isRunning) return; + mainWindow = window; isRunning = true; - collectAndSendMetrics(); - interval = setInterval(() => { - collectAndSendMetrics(); - }, updateFrequency); + + collectAndSendCpuMetrics(); + cpuInterval = setInterval(() => { + collectAndSendCpuMetrics(); + }, cpuUpdateFrequency); + + collectAndSendMemoryMetrics(); + memoryInterval = setInterval(() => { + collectAndSendMemoryMetrics(); + }, memoryUpdateFrequency); + + collectAndSendGpuMetrics(); + gpuInterval = setInterval(() => { + collectAndSendGpuMetrics(); + }, gpuUpdateFrequency); } export function stopMonitoring() { - if (interval) { - clearInterval(interval); - interval = null; + if (cpuInterval) { + clearInterval(cpuInterval); + cpuInterval = null; + } + if (memoryInterval) { + clearInterval(memoryInterval); + memoryInterval = null; + } + if (gpuInterval) { + clearInterval(gpuInterval); + gpuInterval = null; } isRunning = false; } -async function collectAndSendMetrics() { +async function collectAndSendCpuMetrics() { try { - const metrics = await collectMetrics(); + const cpuData = await si.currentLoad(); + const metrics: CpuMetrics = { + usage: cpuData.currentLoad, + }; if (mainWindow && !mainWindow.isDestroyed()) { - mainWindow.webContents.send('system-metrics', metrics); + mainWindow.webContents.send('cpu-metrics', metrics); } } catch (error) { - logError('Failed to collect system metrics:', error as Error); + logError('Failed to collect CPU metrics:', error as Error); } } -async function getWindowsGPUData() { - return new Promise< - { - deviceName: string; - usage: number; - memoryUsed: number; - memoryTotal: number; - }[] - >((resolve) => { - const gpus: { - deviceName: string; - usage: number; - memoryUsed: number; - memoryTotal: number; - }[] = []; - - const powershellScript = ` - # Get GPU information using WMI and Performance Counters - $gpuInfo = Get-WmiObject -Class Win32_VideoController | Where-Object {$_.Name -notlike "*Microsoft*" -and $_.Name -notlike "*Remote*"} - - foreach ($gpu in $gpuInfo) { - $name = $gpu.Name - $vramTotal = if ($gpu.AdapterRAM) { $gpu.AdapterRAM } else { 0 } - - # Try to get GPU usage from performance counters - $usage = 0 - try { - $counter = Get-Counter "\\GPU Engine(*engtype_3D)\\Utilization Percentage" -ErrorAction SilentlyContinue - if ($counter) { - $usage = ($counter.CounterSamples | Measure-Object -Property CookedValue -Maximum).Maximum - } - } catch {} - - # Try alternative performance counter for GPU usage - if ($usage -eq 0) { - try { - $counter = Get-Counter "\\GPU Process Memory(*)\\Dedicated Usage" -ErrorAction SilentlyContinue - if ($counter) { - $memUsed = ($counter.CounterSamples | Measure-Object -Property CookedValue -Sum).Sum - $usage = if ($vramTotal -gt 0) { [math]::Round(($memUsed / $vramTotal) * 100, 2) } else { 0 } - } - } catch {} - } - - # Output as JSON - @{ - Name = $name - Usage = $usage - MemoryUsed = 0 - MemoryTotal = $vramTotal - } | ConvertTo-Json -Compress - } - `; - - const powershell = spawn( - 'powershell.exe', - [ - '-NoProfile', - '-ExecutionPolicy', - 'Bypass', - '-Command', - powershellScript, - ], - { timeout: 5000 } - ); - - let output = ''; - powershell.stdout.on('data', (data) => { - output += data.toString(); - }); - - powershell.on('close', (code) => { - if (code === 0 && output.trim()) { - try { - const lines = output - .trim() - .split('\n') - .filter((line) => line.trim()); - - for (const line of lines) { - try { - const gpuData = JSON.parse(line); - if (gpuData.Name) { - gpus.push({ - deviceName: 'GPU', - usage: parseFloat(gpuData.Usage) || 0, - memoryUsed: parseInt(gpuData.MemoryUsed) || 0, - memoryTotal: parseInt(gpuData.MemoryTotal) || 0, - }); - } - } catch { - continue; - } - } - } catch (error) { - logError('Failed to parse Windows GPU data:', error as Error); - } - } - - resolve(gpus); - }); - - powershell.on('error', () => { - resolve(gpus); - }); - - setTimeout(() => { - powershell.kill(); - resolve(gpus); - }, 5000); - }); -} - -async function getLinuxGPUData() { +async function collectAndSendMemoryMetrics() { try { - return getLinuxDrmMetrics(); - } catch (error) { - logError('Failed to get Linux GPU data:', error as Error); - return []; - } -} + const memData = await si.mem(); + const metrics: MemoryMetrics = { + used: memData.active || memData.used, + total: memData.total, + usage: ((memData.active || memData.used) / memData.total) * 100, + }; -async function getLinuxDrmMetrics() { - try { - const drmPath = '/sys/class/drm'; - const entries = await readdir(drmPath); - const cardEntries = entries.filter( - (entry) => entry.startsWith('card') && !entry.includes('-') - ); - - const gpus = []; - for (const card of cardEntries) { - const devicePath = join(drmPath, card, 'device'); - - try { - const usageData = await readFile( - `${devicePath}/gpu_busy_percent`, - 'utf8' - ); - const memUsedData = await readFile( - `${devicePath}/mem_info_vram_used`, - 'utf8' - ); - const memTotalData = await readFile( - `${devicePath}/mem_info_vram_total`, - 'utf8' - ); - - const usage = parseInt(usageData.trim(), 10) || 0; - const memoryUsed = parseInt(memUsedData.trim(), 10) || 0; - const memoryTotal = parseInt(memTotalData.trim(), 10) || 0; - - gpus.push({ - deviceName: 'GPU', - usage, - memoryUsed, - memoryTotal, - }); - } catch { - continue; - } + if (mainWindow && !mainWindow.isDestroyed()) { + mainWindow.webContents.send('memory-metrics', metrics); } - - return gpus; - } catch { - return []; + } catch (error) { + logError('Failed to collect memory metrics:', error as Error); } } -async function getGPUData() { - if (platform === 'win32') { - return getWindowsGPUData(); - } else if (platform === 'linux') { - return getLinuxGPUData(); - } else { - return []; +async function collectAndSendGpuMetrics() { + try { + const gpuData = await getGPUData(); + const metrics: GpuMetrics = { + gpus: gpuData.map((gpuInfo) => ({ + name: gpuInfo.deviceName, + usage: gpuInfo.usage, + memoryUsed: gpuInfo.memoryUsed, + memoryTotal: gpuInfo.memoryTotal, + memoryUsage: + gpuInfo.memoryTotal > 0 + ? (gpuInfo.memoryUsed / gpuInfo.memoryTotal) * 100 + : 0, + })), + }; + + if (mainWindow && !mainWindow.isDestroyed()) { + mainWindow.webContents.send('gpu-metrics', metrics); + } + } catch (error) { + logError('Failed to collect GPU metrics:', error as Error); } } - -async function collectMetrics(): Promise { - const timestamp = Date.now(); - - const [cpuData, memData, gpuData] = await Promise.allSettled([ - si.currentLoad(), - si.mem(), - getGPUData(), - ]); - - const cpu = { - usage: cpuData.status === 'fulfilled' ? cpuData.value.currentLoad : 0, - }; - - const memory = { - used: - memData.status === 'fulfilled' - ? memData.value.active || memData.value.used - : 0, - total: memData.status === 'fulfilled' ? memData.value.total : 0, - usage: - memData.status === 'fulfilled' - ? ((memData.value.active || memData.value.used) / memData.value.total) * - 100 - : 0, - }; - - let gpu: SystemMetrics['gpu']; - if (gpuData.status === 'fulfilled' && gpuData.value.length > 0) { - gpu = gpuData.value.map((gpuInfo: { - deviceName: string; - usage: number; - memoryUsed: number; - memoryTotal: number; - }) => ({ - name: gpuInfo.deviceName, - usage: gpuInfo.usage, - memoryUsed: gpuInfo.memoryUsed, - memoryTotal: gpuInfo.memoryTotal, - memoryUsage: - gpuInfo.memoryTotal > 0 - ? (gpuInfo.memoryUsed / gpuInfo.memoryTotal) * 100 - : 0, - })); - } else if (gpuData.status === 'rejected') { - logError('GPU detection failed', gpuData.reason as Error); - } - - return { - timestamp, - cpu, - memory, - gpu, - }; -} diff --git a/src/main/utils/assets.ts b/src/main/utils/assets.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/preload/index.ts b/src/preload/index.ts index aab7f7d..48d21bd 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -111,11 +111,23 @@ const openwebUIAPI: OpenWebUIAPI = { const monitoringAPI: MonitoringAPI = { start: () => ipcRenderer.invoke('monitoring:start'), stop: () => ipcRenderer.invoke('monitoring:stop'), - onMetrics: (callback) => { - ipcRenderer.on('system-metrics', (_, metrics) => callback(metrics)); + onCpuMetrics: (callback) => { + ipcRenderer.on('cpu-metrics', (_, metrics) => callback(metrics)); }, - removeMetricsListener: () => { - ipcRenderer.removeAllListeners('system-metrics'); + onMemoryMetrics: (callback) => { + ipcRenderer.on('memory-metrics', (_, metrics) => callback(metrics)); + }, + onGpuMetrics: (callback) => { + ipcRenderer.on('gpu-metrics', (_, metrics) => callback(metrics)); + }, + removeCpuMetricsListener: () => { + ipcRenderer.removeAllListeners('cpu-metrics'); + }, + removeMemoryMetricsListener: () => { + ipcRenderer.removeAllListeners('memory-metrics'); + }, + removeGpuMetricsListener: () => { + ipcRenderer.removeAllListeners('gpu-metrics'); }, }; diff --git a/src/types/electron.d.ts b/src/types/electron.d.ts index b00e294..f1217be 100644 --- a/src/types/electron.d.ts +++ b/src/types/electron.d.ts @@ -6,7 +6,11 @@ import type { } from '@/types/hardware'; import type { BackendOption, BackendSupport } from '@/types'; import type { MantineColorScheme } from '@mantine/core'; -import type { SystemMetrics } from '@/main/modules/monitoring'; +import type { + CpuMetrics, + MemoryMetrics, + GpuMetrics, +} from '@/main/modules/monitoring'; export interface GitHubAsset { name: string; @@ -176,8 +180,12 @@ export interface OpenWebUIAPI { export interface MonitoringAPI { start: () => Promise; stop: () => Promise; - onMetrics: (callback: (metrics: SystemMetrics) => void) => void; - removeMetricsListener: () => void; + onCpuMetrics: (callback: (metrics: CpuMetrics) => void) => void; + onMemoryMetrics: (callback: (metrics: MemoryMetrics) => void) => void; + onGpuMetrics: (callback: (metrics: GpuMetrics) => void) => void; + removeCpuMetricsListener: () => void; + removeMemoryMetricsListener: () => void; + removeGpuMetricsListener: () => void; } declare global { diff --git a/src/utils/gpu.ts b/src/utils/gpu.ts new file mode 100644 index 0000000..e85691e --- /dev/null +++ b/src/utils/gpu.ts @@ -0,0 +1,235 @@ +import { readFile, readdir } from 'fs/promises'; +import { join } from 'path'; +import { spawn } from 'child_process'; +import { platform } from 'process'; + +export async function getGPUData() { + if (platform === 'win32') { + return getWindowsGPUData(); + } else if (platform === 'linux') { + return getLinuxGPUData(); + } else { + return []; + } +} + +async function getWindowsGPUData() { + return new Promise< + { + deviceName: string; + usage: number; + memoryUsed: number; + memoryTotal: number; + }[] + >((resolve) => { + const script = ` +# Get GPU basic info and total VRAM from registry +$gpus = Get-WmiObject -Class Win32_VideoController | Where-Object {$_.Name -notlike "*Microsoft*"} +$correctVRAM = @{} + +try { + $regPath = "HKLM:\\SYSTEM\\CurrentControlSet\\Control\\Class\\{4d36e968-e325-11ce-bfc1-08002be10318}" + $adapterKeys = Get-ChildItem $regPath -ErrorAction SilentlyContinue | Where-Object { $_.Name -match '000[0-9]' } + + foreach ($key in $adapterKeys) { + $keyPath = $key.PSPath + $driverDesc = Get-ItemProperty -Path $keyPath -Name "DriverDesc" -ErrorAction SilentlyContinue + $vramSize = Get-ItemProperty -Path $keyPath -Name "HardwareInformation.qwMemorySize" -ErrorAction SilentlyContinue + + if ($driverDesc -and $vramSize -and $driverDesc.DriverDesc -notlike "*Microsoft*") { + $correctVRAM[$driverDesc.DriverDesc] = $vramSize.'HardwareInformation.qwMemorySize' + } + } +} catch {} + +# Get VRAM usage from GPU Adapter Memory counters (what Task Manager uses) +$vramUsageByLuid = @{} +try { + $adapterMemory = Get-Counter "\\GPU Adapter Memory(*)\\Dedicated Usage" -ErrorAction SilentlyContinue + if ($adapterMemory) { + foreach ($sample in $adapterMemory.CounterSamples) { + $instanceName = $sample.InstanceName + $usageBytes = $sample.CookedValue + $vramUsageByLuid[$instanceName] = $usageBytes + } + } +} catch {} + +# Get GPU utilization from performance counters +$gpuUtilization = @{} +try { + $engineCounters = Get-Counter "\\GPU Engine(*)\\Utilization Percentage" -ErrorAction SilentlyContinue + if ($engineCounters) { + $utilizationByLuid = @{} + foreach ($sample in $engineCounters.CounterSamples) { + $instanceName = $sample.InstanceName + $utilization = $sample.CookedValue + + if ($instanceName -match "luid_(0x[0-9a-fA-F]+_0x[0-9a-fA-F]+)") { + $luid = $matches[1] + if (-not $utilizationByLuid[$luid]) { + $utilizationByLuid[$luid] = 0 + } + $utilizationByLuid[$luid] = [Math]::Max($utilizationByLuid[$luid], $utilization) + } + } + + # Find the main GPU LUID (the one with actual VRAM usage) + $mainLuid = "" + $maxVramUsage = 0 + foreach ($luid in $vramUsageByLuid.Keys) { + if ($vramUsageByLuid[$luid] -gt $maxVramUsage) { + $maxVramUsage = $vramUsageByLuid[$luid] + $mainLuid = $luid + } + } + + # Extract LUID from main adapter + if ($mainLuid -match "luid_(0x[0-9a-fA-F]+_0x[0-9a-fA-F]+)") { + $mainLuidKey = $matches[1] + foreach ($gpu in $gpus) { + if ($utilizationByLuid[$mainLuidKey]) { + $gpuUtilization[$gpu.Name] = $utilizationByLuid[$mainLuidKey] + } + } + } + } +} catch {} + +# Output results +foreach ($gpu in $gpus) { + $totalVRAM = $correctVRAM[$gpu.Name] + if (-not $totalVRAM -or $totalVRAM -le 0) { + $totalVRAM = $gpu.AdapterRAM + } + + # Get VRAM usage for the main GPU (highest usage LUID) + $usedVRAM = 0 + $maxUsage = 0 + foreach ($luid in $vramUsageByLuid.Keys) { + $usage = $vramUsageByLuid[$luid] + if ($usage -gt $maxUsage) { + $maxUsage = $usage + $usedVRAM = $usage + } + } + + $utilization = 0 + if ($gpuUtilization[$gpu.Name]) { + $utilization = $gpuUtilization[$gpu.Name] + } + + Write-Output "$($gpu.Name)|$totalVRAM|$usedVRAM|$utilization" +} +`; + + const powershell = spawn( + 'powershell.exe', + [ + '-NoProfile', + '-NonInteractive', + '-ExecutionPolicy', + 'Bypass', + '-Command', + script, + ], + { + stdio: ['pipe', 'pipe', 'pipe'], + windowsHide: true, + shell: false, + } + ); + + let output = ''; + + powershell.stdout.on('data', (data) => { + output += data.toString(); + }); + + powershell.on('close', (code) => { + const gpus: { + deviceName: string; + usage: number; + memoryUsed: number; + memoryTotal: number; + }[] = []; + + if (code === 0 && output.trim()) { + const lines = output.trim().split('\n'); + for (const line of lines) { + const parts = line.split('|'); + if (parts.length === 4 && parts[0]) { + const name = parts[0].trim(); + const totalRAM = parseInt(parts[1]) || 0; + const usedRAM = parseInt(parts[2]) || 0; + const utilization = parseFloat(parts[3]) || 0; + + gpus.push({ + deviceName: name, + usage: Math.round(utilization * 100) / 100, + memoryUsed: usedRAM > 0 ? Math.round(usedRAM / (1024 * 1024)) : 0, + memoryTotal: + totalRAM > 0 ? Math.round(totalRAM / (1024 * 1024)) : 0, + }); + } + } + } + + resolve(gpus); + }); + + powershell.on('error', () => resolve([])); + + setTimeout(() => { + powershell.kill('SIGTERM'); + resolve([]); + }, 3000); + }); +} + +async function getLinuxGPUData() { + try { + const drmPath = '/sys/class/drm'; + const entries = await readdir(drmPath); + const cardEntries = entries.filter( + (entry) => entry.startsWith('card') && !entry.includes('-') + ); + + const gpus = []; + for (const card of cardEntries) { + const devicePath = join(drmPath, card, 'device'); + + try { + const usageData = await readFile( + `${devicePath}/gpu_busy_percent`, + 'utf8' + ); + const memUsedData = await readFile( + `${devicePath}/mem_info_vram_used`, + 'utf8' + ); + const memTotalData = await readFile( + `${devicePath}/mem_info_vram_total`, + 'utf8' + ); + + const usage = parseInt(usageData.trim(), 10) || 0; + const memoryUsed = parseInt(memUsedData.trim(), 10) || 0; + const memoryTotal = parseInt(memTotalData.trim(), 10) || 0; + + gpus.push({ + deviceName: 'GPU', + usage, + memoryUsed, + memoryTotal, + }); + } catch { + continue; + } + } + + return gpus; + } catch { + return []; + } +} diff --git a/yarn.lock b/yarn.lock index 0e7f7cb..f78dd23 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2698,16 +2698,16 @@ __metadata: languageName: node linkType: hard -"electron@npm:^38.0.0": - version: 38.0.0 - resolution: "electron@npm:38.0.0" +"electron@npm:^38.1.0": + version: 38.1.0 + resolution: "electron@npm:38.1.0" dependencies: "@electron/get": "npm:^2.0.0" "@types/node": "npm:^22.7.7" extract-zip: "npm:^2.0.1" bin: electron: cli.js - checksum: 10c0/df3e4eaead2b0de4612a1b593f46667dfd4aff4755727371b5630aada40094cddf82b3837ce0ba3babed8c503275da59187f11da9f4ab26cd20e1362dc103ea6 + checksum: 10c0/186082b15862b0e9617828ca395a79311ebe2a713bb2eb15a67cf6bea208e8cda892395a143e72915e189088ac71e91b29c1a46a0c4d343022cbc33004d4d302 languageName: node linkType: hard @@ -3637,7 +3637,7 @@ __metadata: "@vitejs/plugin-react": "npm:^5.0.2" axios: "npm:^1.11.0" cross-env: "npm:^10.0.0" - electron: "npm:^38.0.0" + electron: "npm:^38.1.0" electron-builder: "npm:^26.0.12" electron-vite: "npm:^4.0.0" eslint: "npm:^9.35.0"