Add caching for api calls to speed them up. Added caching for cpu and gpu stat calls
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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
59
ui/src/server/apiCache.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user