mirror of
https://github.com/lone-cloud/gerbil
synced 2026-06-03 09:33:10 -07:00
changing metrics because windows sucks
This commit is contained in:
parent
f3dbabd98a
commit
280ae26d77
11 changed files with 438 additions and 311 deletions
|
|
@ -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',
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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<SystemMetrics | null>(
|
||||
const [cpuMetrics, setCpuMetrics] = useState<CpuMetrics | null>(null);
|
||||
const [memoryMetrics, setMemoryMetrics] = useState<MemoryMetrics | null>(
|
||||
null
|
||||
);
|
||||
const [gpuMetrics, setGpuMetrics] = useState<GpuMetrics | null>(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 (
|
||||
<Box
|
||||
pr="xs"
|
||||
px="xs"
|
||||
style={{
|
||||
borderTop: `1px solid ${colorScheme === 'dark' ? theme.colors.dark[4] : theme.colors.gray[3]}`,
|
||||
backgroundColor:
|
||||
|
|
@ -73,37 +92,50 @@ export const StatusBar = ({
|
|||
>
|
||||
<Flex align="center" justify="space-between">
|
||||
<Group gap="xs" wrap="nowrap">
|
||||
<Tooltip
|
||||
label={`${currentMetrics.cpu.usage.toFixed(1)}%`}
|
||||
position="top"
|
||||
<Tooltip label={`${cpuMetrics.usage.toFixed(1)}%`} position="top">
|
||||
<Badge
|
||||
size="sm"
|
||||
variant="light"
|
||||
style={{ minWidth: '5rem', textAlign: 'center' }}
|
||||
>
|
||||
<Badge size="sm" variant="light">
|
||||
CPU: {currentMetrics.cpu.usage.toFixed(1)}%
|
||||
CPU: {cpuMetrics.usage.toFixed(1)}%
|
||||
</Badge>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip
|
||||
label={`${(currentMetrics.memory.used / 1024 ** 3).toFixed(1)} GB / ${(currentMetrics.memory.total / 1024 ** 3).toFixed(1)} GB (${currentMetrics.memory.usage.toFixed(1)}%)`}
|
||||
label={`${(memoryMetrics.used / 1024 ** 3).toFixed(1)} GB / ${(memoryMetrics.total / 1024 ** 3).toFixed(1)} GB (${memoryMetrics.usage.toFixed(1)}%)`}
|
||||
position="top"
|
||||
>
|
||||
<Badge size="sm" variant="light">
|
||||
RAM: {currentMetrics.memory.usage.toFixed(1)}%
|
||||
<Badge
|
||||
size="sm"
|
||||
variant="light"
|
||||
style={{ minWidth: '5rem', textAlign: 'center' }}
|
||||
>
|
||||
RAM: {memoryMetrics.usage.toFixed(1)}%
|
||||
</Badge>
|
||||
</Tooltip>
|
||||
|
||||
{currentMetrics.gpu?.map((gpu, index) => (
|
||||
{gpuMetrics?.gpus.map((gpu, index) => (
|
||||
<Group key={`gpu-${index}`} gap={4} wrap="nowrap">
|
||||
<Tooltip label={`${gpu.usage.toFixed(1)}%`} position="top">
|
||||
<Badge size="sm" variant="light">
|
||||
<Badge
|
||||
size="sm"
|
||||
variant="light"
|
||||
style={{ minWidth: '5rem', textAlign: 'center' }}
|
||||
>
|
||||
GPU: {gpu.usage.toFixed(1)}%
|
||||
</Badge>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip
|
||||
label={`${(gpu.memoryUsed / 1024 ** 2).toFixed(1)} MB / ${(gpu.memoryTotal / 1024 ** 2).toFixed(1)} MB (${gpu.memoryUsage.toFixed(1)}%)`}
|
||||
label={`${(gpu.memoryUsed / 1024).toFixed(1)} GB / ${(gpu.memoryTotal / 1024).toFixed(1)} GB (${gpu.memoryUsage.toFixed(1)}%)`}
|
||||
position="top"
|
||||
>
|
||||
<Badge size="sm" variant="light">
|
||||
<Badge
|
||||
size="sm"
|
||||
variant="light"
|
||||
style={{ minWidth: '5rem', textAlign: 'center' }}
|
||||
>
|
||||
VRAM: {gpu.memoryUsage.toFixed(1)}%
|
||||
</Badge>
|
||||
</Tooltip>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,258 +41,90 @@ export interface SystemMetrics {
|
|||
}[];
|
||||
}
|
||||
|
||||
let interval: ReturnType<typeof setInterval> | null = null;
|
||||
let cpuInterval: ReturnType<typeof setInterval> | null = null;
|
||||
let memoryInterval: ReturnType<typeof setInterval> | null = null;
|
||||
let gpuInterval: ReturnType<typeof setInterval> | 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
|
||||
async function collectAndSendMemoryMetrics() {
|
||||
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() {
|
||||
try {
|
||||
return getLinuxDrmMetrics();
|
||||
} catch (error) {
|
||||
logError('Failed to get Linux GPU data:', error as Error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
return gpus;
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async function getGPUData() {
|
||||
if (platform === 'win32') {
|
||||
return getWindowsGPUData();
|
||||
} else if (platform === 'linux') {
|
||||
return getLinuxGPUData();
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async function collectMetrics(): Promise<SystemMetrics> {
|
||||
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 memData = await si.mem();
|
||||
const metrics: MemoryMetrics = {
|
||||
used: memData.active || memData.used,
|
||||
total: memData.total,
|
||||
usage: ((memData.active || memData.used) / memData.total) * 100,
|
||||
};
|
||||
|
||||
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,
|
||||
};
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
mainWindow.webContents.send('memory-metrics', metrics);
|
||||
}
|
||||
} catch (error) {
|
||||
logError('Failed to collect memory metrics:', error as Error);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}) => ({
|
||||
async function collectAndSendGpuMetrics() {
|
||||
try {
|
||||
const gpuData = await getGPUData();
|
||||
const metrics: GpuMetrics = {
|
||||
gpus: gpuData.map((gpuInfo) => ({
|
||||
name: gpuInfo.deviceName,
|
||||
usage: gpuInfo.usage,
|
||||
memoryUsed: gpuInfo.memoryUsed,
|
||||
|
|
@ -285,15 +133,13 @@ async function collectMetrics(): Promise<SystemMetrics> {
|
|||
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,
|
||||
})),
|
||||
};
|
||||
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
mainWindow.webContents.send('gpu-metrics', metrics);
|
||||
}
|
||||
} catch (error) {
|
||||
logError('Failed to collect GPU metrics:', error as Error);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
|||
14
src/types/electron.d.ts
vendored
14
src/types/electron.d.ts
vendored
|
|
@ -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<void>;
|
||||
stop: () => Promise<void>;
|
||||
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 {
|
||||
|
|
|
|||
235
src/utils/gpu.ts
Normal file
235
src/utils/gpu.ts
Normal file
|
|
@ -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 [];
|
||||
}
|
||||
}
|
||||
10
yarn.lock
10
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"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue