feat(perf): add cache warmup for improved performance
Implement automatic cache warming when users first connect to views. Pre-renders all URLs in a view to eliminate initial load latency. Also adds periodic warmup for active devices and removes cache TTL refresh on cache hits to preserve original expiration timing.
This commit is contained in:
+70
-2
@@ -16,6 +16,7 @@ const CONFIG = {
|
|||||||
MAX_CONCURRENT_RENDERS: 16,
|
MAX_CONCURRENT_RENDERS: 16,
|
||||||
QUEUE_TIMEOUT_MS: 30000,
|
QUEUE_TIMEOUT_MS: 30000,
|
||||||
CACHE_CLEANUP_INTERVAL_MS: 30000,
|
CACHE_CLEANUP_INTERVAL_MS: 30000,
|
||||||
|
WARMUP_INTERVAL_MS: 60000,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════
|
||||||
@@ -215,6 +216,7 @@ function viewCacheAll(): View[] {
|
|||||||
// ═══════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
const devices = new Map<string, DeviceEntry>();
|
const devices = new Map<string, DeviceEntry>();
|
||||||
|
const warmedUpViews = new Set<string>();
|
||||||
const DEVICE_TTL_MS = 120_000;
|
const DEVICE_TTL_MS = 120_000;
|
||||||
|
|
||||||
function deviceTrack(ip: string, viewId: string, viewName: string, userAgent: string): void {
|
function deviceTrack(ip: string, viewId: string, viewName: string, userAgent: string): void {
|
||||||
@@ -227,6 +229,14 @@ function deviceTrack(ip: string, viewId: string, viewName: string, userAgent: st
|
|||||||
existing.requestCount++;
|
existing.requestCount++;
|
||||||
} else {
|
} else {
|
||||||
devices.set(ip, { ip, viewId, viewName, userAgent, lastSeen: Date.now(), requestCount: 1 });
|
devices.set(ip, { ip, viewId, viewName, userAgent, lastSeen: Date.now(), requestCount: 1 });
|
||||||
|
|
||||||
|
// First user connecting to this view - trigger warmup
|
||||||
|
if (!warmedUpViews.has(viewId)) {
|
||||||
|
warmedUpViews.add(viewId);
|
||||||
|
warmupView(viewId).catch(err => {
|
||||||
|
console.error(`[warmup] Failed to warm up view ${viewId}:`, err);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -467,6 +477,64 @@ function cachePurgeExpired(): void {
|
|||||||
|
|
||||||
setInterval(cachePurgeExpired, CONFIG.CACHE_CLEANUP_INTERVAL_MS);
|
setInterval(cachePurgeExpired, CONFIG.CACHE_CLEANUP_INTERVAL_MS);
|
||||||
|
|
||||||
|
async function warmupView(viewId: string): Promise<void> {
|
||||||
|
const view = viewCacheGet(viewId);
|
||||||
|
if (!view) {
|
||||||
|
console.warn(`[warmup] View ${viewId} not found`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[warmup] Starting precache for view "${view.name}" (${view.urls.length} URLs)`);
|
||||||
|
|
||||||
|
for (let i = 0; i < view.urls.length; i++) {
|
||||||
|
const key = renderCacheKey(view.id, i);
|
||||||
|
const cached = renderCache.get(key);
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
if (!cached || now >= cached.expiresAt) {
|
||||||
|
try {
|
||||||
|
await getOrRender(view, i);
|
||||||
|
console.log(`[warmup] Precached ${view.name} URL ${i}: ${view.urls[i].url}`);
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error(`[warmup] Failed to precache ${view.name} URL ${i}: ${err.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[warmup] Precache complete for view "${view.name}"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function warmupCache(): Promise<void> {
|
||||||
|
const activeDevices = devicesSnapshot();
|
||||||
|
if (activeDevices.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[warmup] Starting periodic precache for ${activeDevices.length} active device(s)`);
|
||||||
|
|
||||||
|
const views = viewCacheAll();
|
||||||
|
for (const view of views) {
|
||||||
|
for (let i = 0; i < view.urls.length; i++) {
|
||||||
|
const key = renderCacheKey(view.id, i);
|
||||||
|
const cached = renderCache.get(key);
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
if (!cached || now >= cached.expiresAt) {
|
||||||
|
try {
|
||||||
|
await getOrRender(view, i);
|
||||||
|
console.log(`[warmup] Precached ${view.name} URL ${i}: ${view.urls[i].url}`);
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error(`[warmup] Failed to precache ${view.name} URL ${i}: ${err.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[warmup] Periodic precache complete`);
|
||||||
|
}
|
||||||
|
|
||||||
|
setInterval(warmupCache, CONFIG.WARMUP_INTERVAL_MS);
|
||||||
|
|
||||||
function cacheInvalidate(viewId: string): void {
|
function cacheInvalidate(viewId: string): void {
|
||||||
const prefix = `${viewId}:`;
|
const prefix = `${viewId}:`;
|
||||||
for (const key of renderCache.keys()) {
|
for (const key of renderCache.keys()) {
|
||||||
@@ -478,6 +546,8 @@ function cacheInvalidate(viewId: string): void {
|
|||||||
renderQueue.splice(i, 1);
|
renderQueue.splice(i, 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Allow re-warming if view is recreated
|
||||||
|
warmedUpViews.delete(viewId);
|
||||||
}
|
}
|
||||||
|
|
||||||
function drainRenderQueue(): void {
|
function drainRenderQueue(): void {
|
||||||
@@ -504,7 +574,6 @@ function drainRenderQueue(): void {
|
|||||||
|
|
||||||
const cached = renderCache.get(next.key);
|
const cached = renderCache.get(next.key);
|
||||||
if (cached && Date.now() < cached.expiresAt) {
|
if (cached && Date.now() < cached.expiresAt) {
|
||||||
cached.expiresAt = Date.now() + view.cacheTtlSec * 1000;
|
|
||||||
next.resolve(cached.body);
|
next.resolve(cached.body);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -564,7 +633,6 @@ async function getOrRender(view: View, urlIndex: number): Promise<string> {
|
|||||||
|
|
||||||
const cached = renderCache.get(key);
|
const cached = renderCache.get(key);
|
||||||
if (cached && now < cached.expiresAt) {
|
if (cached && now < cached.expiresAt) {
|
||||||
cached.expiresAt = now + view.cacheTtlSec * 1000;
|
|
||||||
return cached.body;
|
return cached.body;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user