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 { createRequire } from 'module';
|
||||||
import os from 'os';
|
import os from 'os';
|
||||||
import { CpuInfo } from '@/types';
|
import { CpuInfo } from '@/types';
|
||||||
|
import { cached } from '@/server/apiCache';
|
||||||
|
|
||||||
const isMac = os.platform() === 'darwin';
|
const isMac = os.platform() === 'darwin';
|
||||||
|
|
||||||
export async function GET() {
|
async function getCpuInfo(): Promise<CpuInfo> {
|
||||||
try {
|
const cpuInfoRaw = await si.cpu();
|
||||||
const cpuInfoRaw = await si.cpu();
|
let cpuInfo: CpuInfo;
|
||||||
let cpuInfo: CpuInfo;
|
|
||||||
|
|
||||||
if (isMac) {
|
if (isMac) {
|
||||||
try {
|
try {
|
||||||
const nativeRequire = createRequire(import.meta.url);
|
const nativeRequire = createRequire(import.meta.url);
|
||||||
const ms = nativeRequire('macstats') as any;
|
const ms = nativeRequire('macstats') as any;
|
||||||
const ramData = ms.getRAMUsageSync();
|
const ramData = ms.getRAMUsageSync();
|
||||||
const cpuData = ms.getCpuDataSync();
|
const cpuData = ms.getCpuDataSync();
|
||||||
|
|
||||||
cpuInfo = {
|
cpuInfo = {
|
||||||
name: `${cpuInfoRaw.manufacturer} ${cpuInfoRaw.brand}`,
|
name: `${cpuInfoRaw.manufacturer} ${cpuInfoRaw.brand}`,
|
||||||
cores: cpuInfoRaw.cores,
|
cores: cpuInfoRaw.cores,
|
||||||
temperature: cpuData.temperature || 0,
|
temperature: cpuData.temperature || 0,
|
||||||
totalMemory: ramData.total / (1024 * 1024),
|
totalMemory: ramData.total / (1024 * 1024),
|
||||||
availableMemory: ramData.free / (1024 * 1024),
|
availableMemory: ramData.free / (1024 * 1024),
|
||||||
freeMemory: ramData.free / (1024 * 1024),
|
freeMemory: ramData.free / (1024 * 1024),
|
||||||
currentLoad: (await si.currentLoad()).currentLoad || 0,
|
currentLoad: (await si.currentLoad()).currentLoad || 0,
|
||||||
};
|
};
|
||||||
} catch {
|
} catch {
|
||||||
// Fallback to systeminformation if macstats fails
|
// 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 {
|
|
||||||
const memoryData = await si.mem();
|
const memoryData = await si.mem();
|
||||||
cpuInfo = {
|
cpuInfo = {
|
||||||
name: `${cpuInfoRaw.manufacturer} ${cpuInfoRaw.brand}`,
|
name: `${cpuInfoRaw.manufacturer} ${cpuInfoRaw.brand}`,
|
||||||
@@ -52,7 +40,25 @@ export async function GET() {
|
|||||||
currentLoad: (await si.currentLoad()).currentLoad || 0,
|
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);
|
return NextResponse.json(cpuInfo);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching CPU stats:', error);
|
console.error('Error fetching CPU stats:', error);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { exec } from 'child_process';
|
|||||||
import { promisify } from 'util';
|
import { promisify } from 'util';
|
||||||
import { createRequire } from 'module';
|
import { createRequire } from 'module';
|
||||||
import os from 'os';
|
import os from 'os';
|
||||||
|
import { cached } from '@/server/apiCache';
|
||||||
|
|
||||||
const execAsync = promisify(exec);
|
const execAsync = promisify(exec);
|
||||||
|
|
||||||
@@ -93,68 +94,73 @@ async function getMacGpuInfo(): Promise<MacGpuResult | null> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function GET() {
|
async function getGpuInfo() {
|
||||||
try {
|
// Get platform
|
||||||
// Get platform
|
const platform = os.platform();
|
||||||
const platform = os.platform();
|
const isWindows = platform === 'win32';
|
||||||
const isWindows = platform === 'win32';
|
const isMac = platform === 'darwin';
|
||||||
const isMac = platform === 'darwin';
|
|
||||||
|
|
||||||
if (isMac) {
|
if (isMac) {
|
||||||
const macGpu = await getMacGpuInfo();
|
const macGpu = await getMacGpuInfo();
|
||||||
if (macGpu) {
|
if (macGpu) {
|
||||||
return NextResponse.json({
|
return {
|
||||||
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({
|
|
||||||
hasNvidiaSmi: false,
|
hasNvidiaSmi: false,
|
||||||
isMac: true,
|
isMac: true,
|
||||||
gpus: [],
|
gpus: [
|
||||||
error: 'Could not read Mac GPU stats',
|
{
|
||||||
});
|
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
|
// Check if nvidia-smi is available
|
||||||
const hasNvidiaSmi = await checkNvidiaSmi(isWindows);
|
const hasNvidiaSmi = await checkNvidiaSmi(isWindows);
|
||||||
|
|
||||||
if (!hasNvidiaSmi) {
|
if (!hasNvidiaSmi) {
|
||||||
return NextResponse.json({
|
return {
|
||||||
hasNvidiaSmi: false,
|
hasNvidiaSmi: false,
|
||||||
isMac: false,
|
isMac: false,
|
||||||
gpus: [],
|
gpus: [],
|
||||||
error: 'nvidia-smi not found or not accessible',
|
error: 'nvidia-smi not found or not accessible',
|
||||||
});
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get GPU stats
|
// Get GPU stats
|
||||||
const gpuStats = await getGpuStats(isWindows);
|
const gpuStats = await getGpuStats(isWindows);
|
||||||
|
|
||||||
return NextResponse.json({
|
return {
|
||||||
hasNvidiaSmi: true,
|
hasNvidiaSmi: true,
|
||||||
gpus: gpuStats,
|
gpus: gpuStats,
|
||||||
});
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
try {
|
||||||
|
const gpuInfo = await cached('gpu-info', getGpuInfo);
|
||||||
|
return NextResponse.json(gpuInfo);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching NVIDIA GPU stats:', error);
|
console.error('Error fetching NVIDIA GPU stats:', error);
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
@@ -248,4 +254,3 @@ async function getGpuStats(isWindows: boolean) {
|
|||||||
|
|
||||||
return gpus;
|
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