Add caching for api calls to speed them up. Added caching for cpu and gpu stat calls

This commit is contained in:
Jaret Burkett
2026-07-16 16:47:10 -06:00
parent 3c75735ba2
commit bf15b65972
3 changed files with 158 additions and 88 deletions

View File

@@ -3,44 +3,32 @@ import si from 'systeminformation';
import { createRequire } from 'module';
import os from 'os';
import { CpuInfo } from '@/types';
import { cached } from '@/server/apiCache';
const isMac = os.platform() === 'darwin';
export async function GET() {
try {
const cpuInfoRaw = await si.cpu();
let cpuInfo: CpuInfo;
async function getCpuInfo(): Promise<CpuInfo> {
const cpuInfoRaw = await si.cpu();
let cpuInfo: CpuInfo;
if (isMac) {
try {
const nativeRequire = createRequire(import.meta.url);
const ms = nativeRequire('macstats') as any;
const ramData = ms.getRAMUsageSync();
const cpuData = ms.getCpuDataSync();
if (isMac) {
try {
const nativeRequire = createRequire(import.meta.url);
const ms = nativeRequire('macstats') as any;
const ramData = ms.getRAMUsageSync();
const cpuData = ms.getCpuDataSync();
cpuInfo = {
name: `${cpuInfoRaw.manufacturer} ${cpuInfoRaw.brand}`,
cores: cpuInfoRaw.cores,
temperature: cpuData.temperature || 0,
totalMemory: ramData.total / (1024 * 1024),
availableMemory: ramData.free / (1024 * 1024),
freeMemory: ramData.free / (1024 * 1024),
currentLoad: (await si.currentLoad()).currentLoad || 0,
};
} catch {
// Fallback to systeminformation if macstats fails
const memoryData = await si.mem();
cpuInfo = {
name: `${cpuInfoRaw.manufacturer} ${cpuInfoRaw.brand}`,
cores: cpuInfoRaw.cores,
temperature: (await si.cpuTemperature()).main || 0,
totalMemory: memoryData.total / (1024 * 1024),
availableMemory: memoryData.available / (1024 * 1024),
freeMemory: memoryData.free / (1024 * 1024),
currentLoad: (await si.currentLoad()).currentLoad || 0,
};
}
} else {
cpuInfo = {
name: `${cpuInfoRaw.manufacturer} ${cpuInfoRaw.brand}`,
cores: cpuInfoRaw.cores,
temperature: cpuData.temperature || 0,
totalMemory: ramData.total / (1024 * 1024),
availableMemory: ramData.free / (1024 * 1024),
freeMemory: ramData.free / (1024 * 1024),
currentLoad: (await si.currentLoad()).currentLoad || 0,
};
} catch {
// Fallback to systeminformation if macstats fails
const memoryData = await si.mem();
cpuInfo = {
name: `${cpuInfoRaw.manufacturer} ${cpuInfoRaw.brand}`,
@@ -52,7 +40,25 @@ export async function GET() {
currentLoad: (await si.currentLoad()).currentLoad || 0,
};
}
} else {
const memoryData = await si.mem();
cpuInfo = {
name: `${cpuInfoRaw.manufacturer} ${cpuInfoRaw.brand}`,
cores: cpuInfoRaw.cores,
temperature: (await si.cpuTemperature()).main || 0,
totalMemory: memoryData.total / (1024 * 1024),
availableMemory: memoryData.available / (1024 * 1024),
freeMemory: memoryData.free / (1024 * 1024),
currentLoad: (await si.currentLoad()).currentLoad || 0,
};
}
return cpuInfo;
}
export async function GET() {
try {
const cpuInfo = await cached('cpu-info', getCpuInfo);
return NextResponse.json(cpuInfo);
} catch (error) {
console.error('Error fetching CPU stats:', error);

View File

@@ -3,6 +3,7 @@ import { exec } from 'child_process';
import { promisify } from 'util';
import { createRequire } from 'module';
import os from 'os';
import { cached } from '@/server/apiCache';
const execAsync = promisify(exec);
@@ -93,68 +94,73 @@ async function getMacGpuInfo(): Promise<MacGpuResult | null> {
}
}
export async function GET() {
try {
// Get platform
const platform = os.platform();
const isWindows = platform === 'win32';
const isMac = platform === 'darwin';
async function getGpuInfo() {
// Get platform
const platform = os.platform();
const isWindows = platform === 'win32';
const isMac = platform === 'darwin';
if (isMac) {
const macGpu = await getMacGpuInfo();
if (macGpu) {
return NextResponse.json({
hasNvidiaSmi: false,
isMac: true,
gpus: [
{
index: 0,
name: macGpu.name,
driverVersion: 'macOS',
temperature: Math.round(macGpu.temperature),
utilization: {
gpu: macGpu.gpuLoad,
memory: macGpu.memTotal > 0 ? Math.round((macGpu.memUsed / macGpu.memTotal) * 100) : 0,
},
memory: {
total: Math.round(macGpu.memTotal),
free: Math.round(macGpu.memTotal - macGpu.memUsed),
used: Math.round(macGpu.memUsed),
},
power: { draw: macGpu.powerDraw, limit: 0 },
clocks: { graphics: 0, memory: 0 },
fan: { speed: macGpu.fanSpeed },
},
],
});
}
return NextResponse.json({
if (isMac) {
const macGpu = await getMacGpuInfo();
if (macGpu) {
return {
hasNvidiaSmi: false,
isMac: true,
gpus: [],
error: 'Could not read Mac GPU stats',
});
gpus: [
{
index: 0,
name: macGpu.name,
driverVersion: 'macOS',
temperature: Math.round(macGpu.temperature),
utilization: {
gpu: macGpu.gpuLoad,
memory: macGpu.memTotal > 0 ? Math.round((macGpu.memUsed / macGpu.memTotal) * 100) : 0,
},
memory: {
total: Math.round(macGpu.memTotal),
free: Math.round(macGpu.memTotal - macGpu.memUsed),
used: Math.round(macGpu.memUsed),
},
power: { draw: macGpu.powerDraw, limit: 0 },
clocks: { graphics: 0, memory: 0 },
fan: { speed: macGpu.fanSpeed },
},
],
};
}
return {
hasNvidiaSmi: false,
isMac: true,
gpus: [],
error: 'Could not read Mac GPU stats',
};
}
// Check if nvidia-smi is available
const hasNvidiaSmi = await checkNvidiaSmi(isWindows);
// Check if nvidia-smi is available
const hasNvidiaSmi = await checkNvidiaSmi(isWindows);
if (!hasNvidiaSmi) {
return NextResponse.json({
hasNvidiaSmi: false,
isMac: false,
gpus: [],
error: 'nvidia-smi not found or not accessible',
});
}
if (!hasNvidiaSmi) {
return {
hasNvidiaSmi: false,
isMac: false,
gpus: [],
error: 'nvidia-smi not found or not accessible',
};
}
// Get GPU stats
const gpuStats = await getGpuStats(isWindows);
// Get GPU stats
const gpuStats = await getGpuStats(isWindows);
return NextResponse.json({
hasNvidiaSmi: true,
gpus: gpuStats,
});
return {
hasNvidiaSmi: true,
gpus: gpuStats,
};
}
export async function GET() {
try {
const gpuInfo = await cached('gpu-info', getGpuInfo);
return NextResponse.json(gpuInfo);
} catch (error) {
console.error('Error fetching NVIDIA GPU stats:', error);
return NextResponse.json(
@@ -248,4 +254,3 @@ async function getGpuStats(isWindows: boolean) {
return gpus;
}

59
ui/src/server/apiCache.ts Normal file
View File

@@ -0,0 +1,59 @@
type CacheEntry = {
promise: Promise<unknown>;
timestamp: number;
};
const cache = new Map<string, CacheEntry>();
const DEFAULT_STALE_TIME_MS = 5000;
/**
* Universal cache for slow API work. Results are cached per key + params and
* reused until they go stale. Concurrent callers while a fetch is in flight
* share the same promise instead of triggering duplicate work.
*
* @param key Unique name for this cached operation (e.g. 'cpu-info')
* @param fetcher Function that produces a fresh value
* @param staleTimeMs How long a cached value stays fresh (default 5000ms)
* @param params Optional params; different params get separate cache entries
*/
export async function cached<T>(
key: string,
fetcher: () => Promise<T>,
staleTimeMs: number = DEFAULT_STALE_TIME_MS,
params: unknown = null,
): Promise<T> {
const cacheKey = params === null ? key : `${key}:${JSON.stringify(params)}`;
const now = Date.now();
const entry = cache.get(cacheKey);
if (entry && now - entry.timestamp < staleTimeMs) {
return entry.promise as Promise<T>;
}
const promise = fetcher();
cache.set(cacheKey, { promise, timestamp: now });
// Drop failed fetches so the next call retries instead of caching the error
promise.catch(() => {
if (cache.get(cacheKey)?.promise === promise) {
cache.delete(cacheKey);
}
});
return promise;
}
/** Remove a cached entry (all param variants if no params given). */
export function invalidateCache(key: string, params: unknown = null): void {
if (params !== null) {
cache.delete(`${key}:${JSON.stringify(params)}`);
return;
}
cache.delete(key);
for (const cacheKey of cache.keys()) {
if (cacheKey.startsWith(`${key}:`)) {
cache.delete(cacheKey);
}
}
}