mirror of
https://github.com/lone-cloud/gerbil
synced 2026-06-03 09:33:10 -07:00
more system monitoring optimizations, trying for 1 update/sec on windows too
This commit is contained in:
parent
ecff5ca4ae
commit
a99f93ce79
5 changed files with 437 additions and 207 deletions
|
|
@ -39,14 +39,14 @@
|
||||||
"license": "AGPL-3.0-or-later",
|
"license": "AGPL-3.0-or-later",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.35.0",
|
"@eslint/js": "^9.35.0",
|
||||||
"@types/node": "^24.5.0",
|
"@types/node": "^24.5.1",
|
||||||
"@types/react": "^19.1.13",
|
"@types/react": "^19.1.13",
|
||||||
"@types/react-dom": "^19.1.9",
|
"@types/react-dom": "^19.1.9",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.44.0",
|
"@typescript-eslint/eslint-plugin": "^8.44.0",
|
||||||
"@typescript-eslint/parser": "^8.44.0",
|
"@typescript-eslint/parser": "^8.44.0",
|
||||||
"@vitejs/plugin-react": "^5.0.2",
|
"@vitejs/plugin-react": "^5.0.2",
|
||||||
"cross-env": "^10.0.0",
|
"cross-env": "^10.0.0",
|
||||||
"electron": "^38.1.0",
|
"electron": "^38.1.1",
|
||||||
"electron-builder": "^26.0.12",
|
"electron-builder": "^26.0.12",
|
||||||
"electron-vite": "^4.0.0",
|
"electron-vite": "^4.0.0",
|
||||||
"eslint": "^9.35.0",
|
"eslint": "^9.35.0",
|
||||||
|
|
@ -74,7 +74,7 @@
|
||||||
"react": "^19.1.1",
|
"react": "^19.1.1",
|
||||||
"react-dom": "^19.1.1",
|
"react-dom": "^19.1.1",
|
||||||
"react-error-boundary": "^6.0.0",
|
"react-error-boundary": "^6.0.0",
|
||||||
"systeminformation": "^5.27.9",
|
"systeminformation": "^5.27.10",
|
||||||
"winston": "^3.17.0",
|
"winston": "^3.17.0",
|
||||||
"winston-daily-rotate-file": "^5.0.0",
|
"winston-daily-rotate-file": "^5.0.0",
|
||||||
"yauzl": "^3.2.0",
|
"yauzl": "^3.2.0",
|
||||||
|
|
|
||||||
|
|
@ -65,14 +65,14 @@ export const StatusBar = ({ maxDataPoints = 60 }: StatusBarProps) => {
|
||||||
<>
|
<>
|
||||||
<PerformanceBadge
|
<PerformanceBadge
|
||||||
label="CPU"
|
label="CPU"
|
||||||
value={`${cpuMetrics.usage.toFixed(1)}%`}
|
value={`${cpuMetrics.usage}%`}
|
||||||
tooltipLabel={`${cpuMetrics.usage.toFixed(1)}%`}
|
tooltipLabel={`${cpuMetrics.usage}%`}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<PerformanceBadge
|
<PerformanceBadge
|
||||||
label="RAM"
|
label="RAM"
|
||||||
value={`${memoryMetrics.usage.toFixed(1)}%`}
|
value={`${memoryMetrics.usage}%`}
|
||||||
tooltipLabel={`${memoryMetrics.used.toFixed(1)} GB / ${memoryMetrics.total.toFixed(1)} GB (${memoryMetrics.usage.toFixed(1)}%)`}
|
tooltipLabel={`${memoryMetrics.used.toFixed(2)} GB / ${memoryMetrics.total.toFixed(2)} GB (${memoryMetrics.usage}%)`}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
@ -81,14 +81,14 @@ export const StatusBar = ({ maxDataPoints = 60 }: StatusBarProps) => {
|
||||||
<Group gap="xs" key={`gpu-${index}`}>
|
<Group gap="xs" key={`gpu-${index}`}>
|
||||||
<PerformanceBadge
|
<PerformanceBadge
|
||||||
label={`GPU${gpuMetrics.gpus.length > 1 ? ` ${index + 1}` : ''}`}
|
label={`GPU${gpuMetrics.gpus.length > 1 ? ` ${index + 1}` : ''}`}
|
||||||
value={`${gpu.usage.toFixed(1)}%`}
|
value={`${gpu.usage}%`}
|
||||||
tooltipLabel={`${gpu.usage.toFixed(1)}%`}
|
tooltipLabel={`${gpu.usage}%`}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<PerformanceBadge
|
<PerformanceBadge
|
||||||
label={`VRAM${gpuMetrics.gpus.length > 1 ? ` ${index + 1}` : ''}`}
|
label={`VRAM${gpuMetrics.gpus.length > 1 ? ` ${index + 1}` : ''}`}
|
||||||
value={`${gpu.memoryUsage.toFixed(1)}%`}
|
value={`${gpu.memoryUsage}%`}
|
||||||
tooltipLabel={`${gpu.memoryUsed.toFixed(1)} GB / ${gpu.memoryTotal.toFixed(1)} GB (${gpu.memoryUsage.toFixed(1)}%)`}
|
tooltipLabel={`${gpu.memoryUsed.toFixed(2)} GB / ${gpu.memoryTotal.toFixed(2)} GB (${gpu.memoryUsage}%)`}
|
||||||
/>
|
/>
|
||||||
</Group>
|
</Group>
|
||||||
))}
|
))}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import { platform } from 'process';
|
|
||||||
import si from 'systeminformation';
|
import si from 'systeminformation';
|
||||||
import { BrowserWindow } from 'electron';
|
import { BrowserWindow } from 'electron';
|
||||||
import { logError } from '@/main/modules/logging';
|
import { logError } from '@/main/modules/logging';
|
||||||
|
|
@ -46,7 +45,7 @@ let cpuInterval: ReturnType<typeof setInterval> | null = null;
|
||||||
let memoryInterval: ReturnType<typeof setInterval> | null = null;
|
let memoryInterval: ReturnType<typeof setInterval> | null = null;
|
||||||
let gpuInterval: ReturnType<typeof setInterval> | null = null;
|
let gpuInterval: ReturnType<typeof setInterval> | null = null;
|
||||||
let isRunning = false;
|
let isRunning = false;
|
||||||
const updateFrequency = platform === 'win32' ? 2000 : 1000;
|
const updateFrequency = 1000;
|
||||||
let mainWindow: BrowserWindow | null = null;
|
let mainWindow: BrowserWindow | null = null;
|
||||||
|
|
||||||
export function startMonitoring(window: BrowserWindow) {
|
export function startMonitoring(window: BrowserWindow) {
|
||||||
|
|
@ -91,7 +90,7 @@ async function collectAndSendCpuMetrics() {
|
||||||
try {
|
try {
|
||||||
const cpuData = await si.currentLoad();
|
const cpuData = await si.currentLoad();
|
||||||
const metrics: CpuMetrics = {
|
const metrics: CpuMetrics = {
|
||||||
usage: cpuData.currentLoad,
|
usage: Math.round(cpuData.currentLoad),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
|
|
@ -108,9 +107,9 @@ async function collectAndSendMemoryMetrics() {
|
||||||
const usedBytes = memData.active || memData.used;
|
const usedBytes = memData.active || memData.used;
|
||||||
const totalBytes = memData.total;
|
const totalBytes = memData.total;
|
||||||
const metrics: MemoryMetrics = {
|
const metrics: MemoryMetrics = {
|
||||||
used: Math.round((usedBytes / (1024 * 1024 * 1024)) * 10) / 10,
|
used: usedBytes / (1024 * 1024 * 1024),
|
||||||
total: Math.round((totalBytes / (1024 * 1024 * 1024)) * 10) / 10,
|
total: totalBytes / (1024 * 1024 * 1024),
|
||||||
usage: (usedBytes / totalBytes) * 100,
|
usage: Math.round((usedBytes / totalBytes) * 100),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
|
|
@ -127,12 +126,12 @@ async function collectAndSendGpuMetrics() {
|
||||||
const metrics: GpuMetrics = {
|
const metrics: GpuMetrics = {
|
||||||
gpus: gpuData.map((gpuInfo) => ({
|
gpus: gpuData.map((gpuInfo) => ({
|
||||||
name: gpuInfo.deviceName,
|
name: gpuInfo.deviceName,
|
||||||
usage: gpuInfo.usage,
|
usage: Math.round(gpuInfo.usage),
|
||||||
memoryUsed: gpuInfo.memoryUsed,
|
memoryUsed: gpuInfo.memoryUsed,
|
||||||
memoryTotal: gpuInfo.memoryTotal,
|
memoryTotal: gpuInfo.memoryTotal,
|
||||||
memoryUsage:
|
memoryUsage:
|
||||||
gpuInfo.memoryTotal > 0
|
gpuInfo.memoryTotal > 0
|
||||||
? (gpuInfo.memoryUsed / gpuInfo.memoryTotal) * 100
|
? Math.round((gpuInfo.memoryUsed / gpuInfo.memoryTotal) * 100)
|
||||||
: 0,
|
: 0,
|
||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,31 @@ import { join } from 'path';
|
||||||
import { spawn } from 'child_process';
|
import { spawn } from 'child_process';
|
||||||
import { platform } from 'process';
|
import { platform } from 'process';
|
||||||
|
|
||||||
|
interface CachedGPUInfo {
|
||||||
|
deviceName: string;
|
||||||
|
devicePath: string;
|
||||||
|
memoryTotal: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface WindowsCachedGPUInfo {
|
||||||
|
deviceName: string;
|
||||||
|
memoryTotal: number;
|
||||||
|
luid: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GPUData {
|
||||||
|
deviceName: string;
|
||||||
|
usage: number;
|
||||||
|
memoryUsed: number;
|
||||||
|
memoryTotal: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
let linuxGpuCache: CachedGPUInfo[] | null = null;
|
||||||
|
let windowsGpuCache: WindowsCachedGPUInfo[] | null = null;
|
||||||
|
|
||||||
|
let linuxCachePromise: Promise<CachedGPUInfo[]> | null = null;
|
||||||
|
let windowsCachePromise: Promise<WindowsCachedGPUInfo[]> | null = null;
|
||||||
|
|
||||||
export async function getGPUData() {
|
export async function getGPUData() {
|
||||||
if (platform === 'win32') {
|
if (platform === 'win32') {
|
||||||
return getWindowsGPUData();
|
return getWindowsGPUData();
|
||||||
|
|
@ -13,19 +38,20 @@ export async function getGPUData() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getWindowsGPUData() {
|
async function initializeWindowsGPUCache() {
|
||||||
return new Promise<
|
if (windowsGpuCache !== null) {
|
||||||
{
|
return windowsGpuCache;
|
||||||
deviceName: string;
|
}
|
||||||
usage: number;
|
|
||||||
memoryUsed: number;
|
if (windowsCachePromise !== null) {
|
||||||
memoryTotal: number;
|
return windowsCachePromise;
|
||||||
}[]
|
}
|
||||||
>((resolve) => {
|
|
||||||
|
windowsCachePromise = new Promise((resolve) => {
|
||||||
const script = `
|
const script = `
|
||||||
# Get GPU basic info and total VRAM from registry
|
|
||||||
$gpus = Get-WmiObject -Class Win32_VideoController | Where-Object {$_.Name -notlike "*Microsoft*"}
|
$gpus = Get-WmiObject -Class Win32_VideoController | Where-Object {$_.Name -notlike "*Microsoft*"}
|
||||||
$correctVRAM = @{}
|
$correctVRAM = @{}
|
||||||
|
$gpuToLuid = @{}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$regPath = "HKLM:\\SYSTEM\\CurrentControlSet\\Control\\Class\\{4d36e968-e325-11ce-bfc1-08002be10318}"
|
$regPath = "HKLM:\\SYSTEM\\CurrentControlSet\\Control\\Class\\{4d36e968-e325-11ce-bfc1-08002be10318}"
|
||||||
|
|
@ -42,196 +68,342 @@ try {
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
|
|
||||||
# Get VRAM usage from GPU Adapter Memory counters (what Task Manager uses)
|
|
||||||
$vramUsageByLuid = @{}
|
|
||||||
try {
|
try {
|
||||||
$adapterMemory = Get-Counter "\\GPU Adapter Memory(*)\\Dedicated Usage" -ErrorAction SilentlyContinue
|
$adapterMemory = Get-Counter "\\GPU Adapter Memory(*)\\Dedicated Usage" -ErrorAction SilentlyContinue
|
||||||
if ($adapterMemory) {
|
if ($adapterMemory) {
|
||||||
foreach ($sample in $adapterMemory.CounterSamples) {
|
foreach ($sample in $adapterMemory.CounterSamples) {
|
||||||
$instanceName = $sample.InstanceName
|
$instanceName = $sample.InstanceName
|
||||||
$usageBytes = $sample.CookedValue
|
if ($instanceName -match "luid_(0x[0-9a-fA-F]+_0x[0-9a-fA-F]+)_([^_]+)") {
|
||||||
$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]
|
$luid = $matches[1]
|
||||||
if (-not $utilizationByLuid[$luid]) {
|
$gpuNameFromCounter = $matches[2]
|
||||||
$utilizationByLuid[$luid] = 0
|
|
||||||
}
|
foreach ($gpu in $gpus) {
|
||||||
$utilizationByLuid[$luid] = [Math]::Max($utilizationByLuid[$luid], $utilization)
|
$cleanGpuName = $gpu.Name -replace '[^a-zA-Z0-9]', ''
|
||||||
}
|
$cleanCounterName = $gpuNameFromCounter -replace '[^a-zA-Z0-9]', ''
|
||||||
}
|
|
||||||
|
if ($cleanGpuName -eq $cleanCounterName -or $gpu.Name -like "*$gpuNameFromCounter*") {
|
||||||
# Find the main GPU LUID (the one with actual VRAM usage)
|
$gpuToLuid[$gpu.Name] = $luid
|
||||||
$mainLuid = ""
|
break
|
||||||
$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 {}
|
} catch {}
|
||||||
|
|
||||||
# Output results
|
|
||||||
foreach ($gpu in $gpus) {
|
foreach ($gpu in $gpus) {
|
||||||
$totalVRAM = $correctVRAM[$gpu.Name]
|
$totalVRAM = $correctVRAM[$gpu.Name]
|
||||||
if (-not $totalVRAM -or $totalVRAM -le 0) {
|
if (-not $totalVRAM -or $totalVRAM -le 0) {
|
||||||
$totalVRAM = $gpu.AdapterRAM
|
$totalVRAM = $gpu.AdapterRAM
|
||||||
}
|
}
|
||||||
|
|
||||||
# Get VRAM usage for the main GPU (highest usage LUID)
|
$luid = $gpuToLuid[$gpu.Name]
|
||||||
$usedVRAM = 0
|
if ($luid) {
|
||||||
$maxUsage = 0
|
Write-Output "$($gpu.Name)|$totalVRAM|$luid"
|
||||||
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(
|
let timeoutHandle: ReturnType<typeof setTimeout> | null = null;
|
||||||
'powershell.exe',
|
let powershellProcess: ReturnType<typeof spawn> | null = null;
|
||||||
[
|
|
||||||
'-NoProfile',
|
const cleanup = () => {
|
||||||
'-NonInteractive',
|
if (timeoutHandle) {
|
||||||
'-ExecutionPolicy',
|
clearTimeout(timeoutHandle);
|
||||||
'Bypass',
|
timeoutHandle = null;
|
||||||
'-Command',
|
|
||||||
script,
|
|
||||||
],
|
|
||||||
{
|
|
||||||
stdio: ['pipe', 'pipe', 'pipe'],
|
|
||||||
windowsHide: true,
|
|
||||||
shell: false,
|
|
||||||
}
|
}
|
||||||
);
|
if (powershellProcess && !powershellProcess.killed) {
|
||||||
|
powershellProcess.kill('SIGTERM');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let output = '';
|
try {
|
||||||
|
powershellProcess = spawn(
|
||||||
|
'powershell.exe',
|
||||||
|
[
|
||||||
|
'-NoProfile',
|
||||||
|
'-NonInteractive',
|
||||||
|
'-ExecutionPolicy',
|
||||||
|
'Bypass',
|
||||||
|
'-Command',
|
||||||
|
script,
|
||||||
|
],
|
||||||
|
{
|
||||||
|
stdio: ['pipe', 'pipe', 'pipe'],
|
||||||
|
windowsHide: true,
|
||||||
|
shell: false,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
powershell.stdout.on('data', (data) => {
|
let output = '';
|
||||||
output += data.toString();
|
|
||||||
});
|
|
||||||
|
|
||||||
powershell.on('close', (code) => {
|
if (powershellProcess.stdout) {
|
||||||
const gpus: {
|
powershellProcess.stdout.on('data', (data) => {
|
||||||
deviceName: string;
|
output += data.toString();
|
||||||
usage: number;
|
});
|
||||||
memoryUsed: number;
|
}
|
||||||
memoryTotal: number;
|
|
||||||
}[] = [];
|
|
||||||
|
|
||||||
if (code === 0 && output.trim()) {
|
powershellProcess.on('close', (code) => {
|
||||||
const lines = output.trim().split('\n');
|
cleanup();
|
||||||
for (const line of lines) {
|
const gpus = [];
|
||||||
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({
|
if (code === 0 && output.trim()) {
|
||||||
deviceName: name,
|
const lines = output.trim().split('\n');
|
||||||
usage: Math.round(utilization * 100) / 100,
|
for (const line of lines) {
|
||||||
memoryUsed: usedRAM > 0 ? usedRAM / (1024 * 1024 * 1024) : 0,
|
const parts = line.split('|');
|
||||||
memoryTotal: totalRAM > 0 ? totalRAM / (1024 * 1024 * 1024) : 0,
|
if (parts.length === 3 && parts[0]) {
|
||||||
});
|
const name = parts[0].trim();
|
||||||
|
const totalRAM = parseInt(parts[1]) || 0;
|
||||||
|
const luid = parts[2].trim();
|
||||||
|
|
||||||
|
if (name && luid && totalRAM >= 0) {
|
||||||
|
gpus.push({
|
||||||
|
deviceName: name,
|
||||||
|
memoryTotal:
|
||||||
|
totalRAM > 0 ? totalRAM / (1024 * 1024 * 1024) : 0,
|
||||||
|
luid,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
resolve(gpus);
|
windowsGpuCache = gpus;
|
||||||
});
|
windowsCachePromise = null;
|
||||||
|
resolve(gpus);
|
||||||
|
});
|
||||||
|
|
||||||
powershell.on('error', () => resolve([]));
|
powershellProcess.on('error', () => {
|
||||||
|
cleanup();
|
||||||
|
windowsGpuCache = [];
|
||||||
|
windowsCachePromise = null;
|
||||||
|
resolve([]);
|
||||||
|
});
|
||||||
|
|
||||||
setTimeout(() => {
|
timeoutHandle = setTimeout(() => {
|
||||||
powershell.kill('SIGTERM');
|
cleanup();
|
||||||
|
windowsGpuCache = [];
|
||||||
|
windowsCachePromise = null;
|
||||||
|
resolve([]);
|
||||||
|
}, 8000);
|
||||||
|
} catch {
|
||||||
|
cleanup();
|
||||||
|
windowsGpuCache = [];
|
||||||
|
windowsCachePromise = null;
|
||||||
resolve([]);
|
resolve([]);
|
||||||
}, 5000);
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return windowsCachePromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getLinuxGPUData() {
|
async function getWindowsGPUData() {
|
||||||
try {
|
try {
|
||||||
const drmPath = '/sys/class/drm';
|
const cachedGPUs = await initializeWindowsGPUCache();
|
||||||
const entries = await readdir(drmPath);
|
|
||||||
const cardEntries = entries.filter(
|
|
||||||
(entry) => entry.startsWith('card') && !entry.includes('-')
|
|
||||||
);
|
|
||||||
|
|
||||||
const gpus = [];
|
return new Promise<GPUData[]>((resolve) => {
|
||||||
for (const card of cardEntries) {
|
const script = `
|
||||||
const devicePath = join(drmPath, card, 'device');
|
$vramUsageByLuid = @{}
|
||||||
|
$utilizationByLuid = @{}
|
||||||
|
$job1 = $null
|
||||||
|
$job2 = $null
|
||||||
|
|
||||||
let deviceName = 'Unknown GPU';
|
try {
|
||||||
|
$job1 = Start-Job -ScriptBlock {
|
||||||
|
try {
|
||||||
|
$adapterMemory = Get-Counter "\\GPU Adapter Memory(*)\\Dedicated Usage" -ErrorAction SilentlyContinue
|
||||||
|
if ($adapterMemory) {
|
||||||
|
$result = @{}
|
||||||
|
foreach ($sample in $adapterMemory.CounterSamples) {
|
||||||
|
$instanceName = $sample.InstanceName
|
||||||
|
$usageBytes = $sample.CookedValue
|
||||||
|
if ($instanceName -match "luid_(0x[0-9a-fA-F]+_0x[0-9a-fA-F]+)") {
|
||||||
|
$luid = $matches[1]
|
||||||
|
$result[$luid] = $usageBytes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $result
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
return @{}
|
||||||
|
}
|
||||||
|
|
||||||
|
$job2 = Start-Job -ScriptBlock {
|
||||||
|
try {
|
||||||
|
$engineCounters = Get-Counter "\\GPU Engine(*)\\Utilization Percentage" -ErrorAction SilentlyContinue
|
||||||
|
if ($engineCounters) {
|
||||||
|
$result = @{}
|
||||||
|
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 $result[$luid]) {
|
||||||
|
$result[$luid] = 0
|
||||||
|
}
|
||||||
|
$result[$luid] = [Math]::Max($result[$luid], $utilization)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $result
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
return @{}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($job1) { $vramUsageByLuid = Receive-Job $job1 -Wait }
|
||||||
|
if ($job2) { $utilizationByLuid = Receive-Job $job2 -Wait }
|
||||||
|
|
||||||
|
} catch {}
|
||||||
|
finally {
|
||||||
|
if ($job1) { Remove-Job $job1 -Force -ErrorAction SilentlyContinue }
|
||||||
|
if ($job2) { Remove-Job $job2 -Force -ErrorAction SilentlyContinue }
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($luid in $vramUsageByLuid.Keys) {
|
||||||
|
$vramUsed = $vramUsageByLuid[$luid]
|
||||||
|
$utilization = 0
|
||||||
|
if ($utilizationByLuid[$luid]) {
|
||||||
|
$utilization = $utilizationByLuid[$luid]
|
||||||
|
}
|
||||||
|
Write-Output "$luid|$vramUsed|$utilization"
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
let timeoutHandle: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
let powershellProcess: ReturnType<typeof spawn> | null = null;
|
||||||
|
|
||||||
|
const cleanup = () => {
|
||||||
|
if (timeoutHandle) {
|
||||||
|
clearTimeout(timeoutHandle);
|
||||||
|
timeoutHandle = null;
|
||||||
|
}
|
||||||
|
if (powershellProcess && !powershellProcess.killed) {
|
||||||
|
powershellProcess.kill('SIGTERM');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const deviceNameData = await readFile(`${devicePath}/device`, 'utf8');
|
powershellProcess = spawn(
|
||||||
const deviceId = deviceNameData.trim();
|
'powershell.exe',
|
||||||
|
[
|
||||||
|
'-NoProfile',
|
||||||
|
'-NonInteractive',
|
||||||
|
'-ExecutionPolicy',
|
||||||
|
'Bypass',
|
||||||
|
'-Command',
|
||||||
|
script,
|
||||||
|
],
|
||||||
|
{
|
||||||
|
stdio: ['pipe', 'pipe', 'pipe'],
|
||||||
|
windowsHide: true,
|
||||||
|
shell: false,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const vendorData = await readFile(`${devicePath}/vendor`, 'utf8');
|
let output = '';
|
||||||
const vendorId = vendorData.trim();
|
|
||||||
|
|
||||||
const modalias = await readFile(`${devicePath}/modalias`, 'utf8');
|
if (powershellProcess.stdout) {
|
||||||
|
powershellProcess.stdout.on('data', (data) => {
|
||||||
if (vendorId === '0x1002') {
|
output += data.toString();
|
||||||
deviceName = 'AMD GPU';
|
});
|
||||||
} else if (vendorId === '0x10de') {
|
|
||||||
deviceName = 'NVIDIA GPU';
|
|
||||||
} else if (vendorId === '0x8086') {
|
|
||||||
deviceName = 'Intel GPU';
|
|
||||||
} else {
|
|
||||||
deviceName = `GPU (${vendorId}:${deviceId})`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (modalias.includes('i915')) {
|
powershellProcess.on('close', (code) => {
|
||||||
deviceName = 'Intel GPU';
|
cleanup();
|
||||||
} else if (modalias.includes('amdgpu')) {
|
const gpus: GPUData[] = [];
|
||||||
deviceName = 'AMD GPU';
|
|
||||||
} else if (
|
if (code === 0 && output.trim() && cachedGPUs.length > 0) {
|
||||||
modalias.includes('nouveau') ||
|
const luidToData = new Map<
|
||||||
modalias.includes('nvidia')
|
string,
|
||||||
) {
|
{ vramUsed: number; utilization: number }
|
||||||
deviceName = 'NVIDIA GPU';
|
>();
|
||||||
}
|
|
||||||
|
const lines = output.trim().split('\n');
|
||||||
|
for (const line of lines) {
|
||||||
|
const parts = line.split('|');
|
||||||
|
if (parts.length === 3) {
|
||||||
|
const luid = parts[0].trim();
|
||||||
|
const vramUsed = Math.max(0, parseInt(parts[1]) || 0);
|
||||||
|
const utilization = Math.max(
|
||||||
|
0,
|
||||||
|
Math.min(100, parseFloat(parts[2]) || 0)
|
||||||
|
);
|
||||||
|
|
||||||
|
luidToData.set(luid, { vramUsed, utilization });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const cachedGPU of cachedGPUs) {
|
||||||
|
const gpuData = luidToData.get(cachedGPU.luid);
|
||||||
|
if (gpuData) {
|
||||||
|
gpus.push({
|
||||||
|
deviceName: cachedGPU.deviceName,
|
||||||
|
usage: gpuData.utilization,
|
||||||
|
memoryUsed:
|
||||||
|
gpuData.vramUsed > 0
|
||||||
|
? parseFloat(
|
||||||
|
(gpuData.vramUsed / (1024 * 1024 * 1024)).toFixed(2)
|
||||||
|
)
|
||||||
|
: 0,
|
||||||
|
memoryTotal: parseFloat(
|
||||||
|
Math.max(0, cachedGPU.memoryTotal).toFixed(2)
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(gpus);
|
||||||
|
});
|
||||||
|
|
||||||
|
powershellProcess.on('error', () => {
|
||||||
|
cleanup();
|
||||||
|
resolve([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
timeoutHandle = setTimeout(() => {
|
||||||
|
cleanup();
|
||||||
|
resolve([]);
|
||||||
|
}, 8000);
|
||||||
} catch {
|
} catch {
|
||||||
|
cleanup();
|
||||||
|
resolve([]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function initializeLinuxGPUCache() {
|
||||||
|
if (linuxGpuCache !== null) {
|
||||||
|
return linuxGpuCache;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (linuxCachePromise !== null) {
|
||||||
|
return linuxCachePromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
linuxCachePromise = (async () => {
|
||||||
|
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');
|
||||||
|
|
||||||
|
let deviceName = 'Unknown GPU';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const modalias = await readFile(`${devicePath}/modalias`, 'utf8');
|
const [modalias] = await Promise.all([
|
||||||
|
readFile(`${devicePath}/modalias`, 'utf8').catch(() => ''),
|
||||||
|
]);
|
||||||
|
|
||||||
if (modalias.includes('i915')) {
|
if (modalias.includes('i915')) {
|
||||||
deviceName = 'Intel GPU';
|
deviceName = 'Intel GPU';
|
||||||
} else if (modalias.includes('amdgpu')) {
|
} else if (modalias.includes('amdgpu')) {
|
||||||
|
|
@ -241,37 +413,96 @@ async function getLinuxGPUData() {
|
||||||
modalias.includes('nvidia')
|
modalias.includes('nvidia')
|
||||||
) {
|
) {
|
||||||
deviceName = 'NVIDIA GPU';
|
deviceName = 'NVIDIA GPU';
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
const [vendorData, deviceData] = await Promise.all([
|
||||||
|
readFile(`${devicePath}/vendor`, 'utf8').catch(() => ''),
|
||||||
|
readFile(`${devicePath}/device`, 'utf8').catch(() => ''),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const vendorId = vendorData.trim();
|
||||||
|
const deviceId = deviceData.trim();
|
||||||
|
|
||||||
|
if (vendorId === '0x1002') {
|
||||||
|
deviceName = 'AMD GPU';
|
||||||
|
} else if (vendorId === '0x10de') {
|
||||||
|
deviceName = 'NVIDIA GPU';
|
||||||
|
} else if (vendorId === '0x8086') {
|
||||||
|
deviceName = 'Intel GPU';
|
||||||
|
} else {
|
||||||
|
deviceName = `GPU (${vendorId}:${deviceId})`;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
void 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
void 0;
|
void 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const memTotalData = await readFile(
|
||||||
|
`${devicePath}/mem_info_vram_total`,
|
||||||
|
'utf8'
|
||||||
|
);
|
||||||
|
const memoryTotal = Math.max(
|
||||||
|
0,
|
||||||
|
(parseInt(memTotalData.trim(), 10) || 0) / (1024 * 1024 * 1024)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (memoryTotal > 0) {
|
||||||
|
gpus.push({
|
||||||
|
deviceName,
|
||||||
|
devicePath,
|
||||||
|
memoryTotal,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
linuxGpuCache = gpus;
|
||||||
const usageData = await readFile(
|
linuxCachePromise = null;
|
||||||
`${devicePath}/gpu_busy_percent`,
|
return gpus;
|
||||||
'utf8'
|
} catch {
|
||||||
);
|
linuxGpuCache = [];
|
||||||
const memUsedData = await readFile(
|
linuxCachePromise = null;
|
||||||
`${devicePath}/mem_info_vram_used`,
|
return [];
|
||||||
'utf8'
|
}
|
||||||
);
|
})();
|
||||||
const memTotalData = await readFile(
|
|
||||||
`${devicePath}/mem_info_vram_total`,
|
|
||||||
'utf8'
|
|
||||||
);
|
|
||||||
|
|
||||||
const usage = parseInt(usageData.trim(), 10) || 0;
|
return linuxCachePromise;
|
||||||
const memoryUsed =
|
}
|
||||||
(parseInt(memUsedData.trim(), 10) || 0) / (1024 * 1024 * 1024);
|
|
||||||
const memoryTotal =
|
async function getLinuxGPUData() {
|
||||||
(parseInt(memTotalData.trim(), 10) || 0) / (1024 * 1024 * 1024);
|
try {
|
||||||
|
const cachedGPUs = await initializeLinuxGPUCache();
|
||||||
|
|
||||||
|
const gpus: GPUData[] = [];
|
||||||
|
for (const cachedGPU of cachedGPUs) {
|
||||||
|
try {
|
||||||
|
const [usageData, memUsedData] = await Promise.all([
|
||||||
|
readFile(`${cachedGPU.devicePath}/gpu_busy_percent`, 'utf8'),
|
||||||
|
readFile(`${cachedGPU.devicePath}/mem_info_vram_used`, 'utf8'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const usage = Math.max(
|
||||||
|
0,
|
||||||
|
Math.min(100, parseInt(usageData.trim(), 10) || 0)
|
||||||
|
);
|
||||||
|
const memoryUsed = Math.max(
|
||||||
|
0,
|
||||||
|
(parseInt(memUsedData.trim(), 10) || 0) / (1024 * 1024 * 1024)
|
||||||
|
);
|
||||||
|
|
||||||
gpus.push({
|
gpus.push({
|
||||||
deviceName,
|
deviceName: cachedGPU.deviceName,
|
||||||
usage,
|
usage,
|
||||||
memoryUsed,
|
memoryUsed: parseFloat(memoryUsed.toFixed(2)),
|
||||||
memoryTotal,
|
memoryTotal: parseFloat(
|
||||||
|
Math.max(0, cachedGPU.memoryTotal).toFixed(2)
|
||||||
|
),
|
||||||
});
|
});
|
||||||
} catch {
|
} catch {
|
||||||
continue;
|
continue;
|
||||||
|
|
|
||||||
30
yarn.lock
30
yarn.lock
|
|
@ -1287,12 +1287,12 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@types/node@npm:*, @types/node@npm:^24.5.0":
|
"@types/node@npm:*, @types/node@npm:^24.5.1":
|
||||||
version: 24.5.0
|
version: 24.5.1
|
||||||
resolution: "@types/node@npm:24.5.0"
|
resolution: "@types/node@npm:24.5.1"
|
||||||
dependencies:
|
dependencies:
|
||||||
undici-types: "npm:~7.12.0"
|
undici-types: "npm:~7.12.0"
|
||||||
checksum: 10c0/c5beff68481e2cc667279a1478b34a1cfd048dbff914219cb5888967938d134907836b6c4d6d141dc862489cb09ef28f7d446c7a3b475181fd126c0fcd2916fa
|
checksum: 10c0/5f0cb038be789b58170e616452ba1f8ebb85bf2fbce58a7e32b1eb08391f64f5e31a9cdbccefbfcd9e6d73b66b564b5e037a1d678ab20213559a32e1d7b6ce17
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
|
@ -2705,16 +2705,16 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"electron@npm:^38.1.0":
|
"electron@npm:^38.1.1":
|
||||||
version: 38.1.0
|
version: 38.1.1
|
||||||
resolution: "electron@npm:38.1.0"
|
resolution: "electron@npm:38.1.1"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@electron/get": "npm:^2.0.0"
|
"@electron/get": "npm:^2.0.0"
|
||||||
"@types/node": "npm:^22.7.7"
|
"@types/node": "npm:^22.7.7"
|
||||||
extract-zip: "npm:^2.0.1"
|
extract-zip: "npm:^2.0.1"
|
||||||
bin:
|
bin:
|
||||||
electron: cli.js
|
electron: cli.js
|
||||||
checksum: 10c0/186082b15862b0e9617828ca395a79311ebe2a713bb2eb15a67cf6bea208e8cda892395a143e72915e189088ac71e91b29c1a46a0c4d343022cbc33004d4d302
|
checksum: 10c0/0b1e5955679de6b9a9a12fa7a51beaaa5dcf15655888670c39e3a49914f8dba580a1f94b46d83e7f8e5dc7fd22c16b130dd5040208d7a86431cf06e16933d9e8
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
|
@ -3648,7 +3648,7 @@ __metadata:
|
||||||
"@fontsource/inter": "npm:^5.2.7"
|
"@fontsource/inter": "npm:^5.2.7"
|
||||||
"@mantine/core": "npm:^8.3.1"
|
"@mantine/core": "npm:^8.3.1"
|
||||||
"@mantine/hooks": "npm:^8.3.1"
|
"@mantine/hooks": "npm:^8.3.1"
|
||||||
"@types/node": "npm:^24.5.0"
|
"@types/node": "npm:^24.5.1"
|
||||||
"@types/react": "npm:^19.1.13"
|
"@types/react": "npm:^19.1.13"
|
||||||
"@types/react-dom": "npm:^19.1.9"
|
"@types/react-dom": "npm:^19.1.9"
|
||||||
"@types/yauzl": "npm:^2.10.3"
|
"@types/yauzl": "npm:^2.10.3"
|
||||||
|
|
@ -3657,7 +3657,7 @@ __metadata:
|
||||||
"@vitejs/plugin-react": "npm:^5.0.2"
|
"@vitejs/plugin-react": "npm:^5.0.2"
|
||||||
axios: "npm:^1.12.2"
|
axios: "npm:^1.12.2"
|
||||||
cross-env: "npm:^10.0.0"
|
cross-env: "npm:^10.0.0"
|
||||||
electron: "npm:^38.1.0"
|
electron: "npm:^38.1.1"
|
||||||
electron-builder: "npm:^26.0.12"
|
electron-builder: "npm:^26.0.12"
|
||||||
electron-vite: "npm:^4.0.0"
|
electron-vite: "npm:^4.0.0"
|
||||||
eslint: "npm:^9.35.0"
|
eslint: "npm:^9.35.0"
|
||||||
|
|
@ -3676,7 +3676,7 @@ __metadata:
|
||||||
react-dom: "npm:^19.1.1"
|
react-dom: "npm:^19.1.1"
|
||||||
react-error-boundary: "npm:^6.0.0"
|
react-error-boundary: "npm:^6.0.0"
|
||||||
rollup-plugin-visualizer: "npm:^6.0.3"
|
rollup-plugin-visualizer: "npm:^6.0.3"
|
||||||
systeminformation: "npm:^5.27.9"
|
systeminformation: "npm:^5.27.10"
|
||||||
typescript: "npm:^5.9.2"
|
typescript: "npm:^5.9.2"
|
||||||
vite: "npm:^7.1.5"
|
vite: "npm:^7.1.5"
|
||||||
winston: "npm:^3.17.0"
|
winston: "npm:^3.17.0"
|
||||||
|
|
@ -6694,12 +6694,12 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"systeminformation@npm:^5.27.9":
|
"systeminformation@npm:^5.27.10":
|
||||||
version: 5.27.9
|
version: 5.27.10
|
||||||
resolution: "systeminformation@npm:5.27.9"
|
resolution: "systeminformation@npm:5.27.10"
|
||||||
bin:
|
bin:
|
||||||
systeminformation: lib/cli.js
|
systeminformation: lib/cli.js
|
||||||
checksum: 10c0/d65aeb68e40d432fb1ec6e1723fa147959cbeb1b43a25c2014b768f0ee099266da6370b3f1593a6a7ca77643f58c9b754d470d225a7afd466d1108c846a67a3c
|
checksum: 10c0/aaaafb3d5738150e4d63908b4516f712810ed9047dc69ba061eacb1c2aa2824bfc352a1dff596e9d81db8a75c2f27da1a3e5daaa88fffb658a3f5a68288a1d02
|
||||||
conditions: (os=darwin | os=linux | os=win32 | os=freebsd | os=openbsd | os=netbsd | os=sunos | os=android)
|
conditions: (os=darwin | os=linux | os=win32 | os=freebsd | os=openbsd | os=netbsd | os=sunos | os=android)
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue