diff --git a/scripts/test-ssr-cache-stats.js b/scripts/test-ssr-cache-stats.js index 05644bf5c56..ceadb6a546f 100644 --- a/scripts/test-ssr-cache-stats.js +++ b/scripts/test-ssr-cache-stats.js @@ -23,7 +23,11 @@ test('snapshot reports entries, HTML byte sizes and TTL without changing LRU ord cache.set('/large', { page: 'larger', headers: {} }); const before = [...cache.keys()]; - const stats = snapshotCache('bot', cache, true); + const stats = snapshotCache('bot', cache, true, { hits: 3, misses: 1 }); + + assert.equal(stats.hits, 3); + assert.equal(stats.misses, 1); + assert.equal(stats.hitRate, 0.75); assert.equal(stats.entryCount, 2); assert.equal(stats.totalHtmlBytes, 8); @@ -51,7 +55,7 @@ test('snapshot counts expired entries even when the cache does not serve stale p test('snapshot of a missing cache and of a cache without TTL', () => { assert.deepEqual(snapshotCache('bot', undefined, false), { - name: 'bot', enabled: false, maxEntries: 0, configuredTtlMs: 0, entryCount: 0, staleCount: 0, totalHtmlBytes: 0, entries: [] + name: 'bot', enabled: false, hits: 0, misses: 0, hitRate: null, maxEntries: 0, configuredTtlMs: 0, entryCount: 0, staleCount: 0, totalHtmlBytes: 0, entries: [] }); const cache = new LRUCache({ max: 1 }); cache.set('/forever', { page: 'x' }); diff --git a/server.ts b/server.ts index 25432610ec1..47e20ae5423 100644 --- a/server.ts +++ b/server.ts @@ -91,6 +91,10 @@ let botCache: LRUCache; // cache of SSR pages for anonymous users. Disabled by default, and only available in production mode let anonymousCache: LRUCache; +const cacheCounters = { + bot: { hits: 0, misses: 0 }, + anonymous: { hits: 0, misses: 0 }, +}; // extend environment with app config for server extendEnvironmentWithAppConfig(environment, appConfig); @@ -510,13 +514,14 @@ function cacheCheck(req, res, next) { * @param next the next function * @returns cached copy (if found) or undefined (if not found) */ -function checkCacheForRequest(cacheName: string, cache: LRUCache, req, res, next): any { +function checkCacheForRequest(cacheName: 'bot' | 'anonymous', cache: LRUCache, req, res, next): any { // Get the cache key for this request const key = getCacheKey(req); // Check if this page is in our cache const cachedCopy = cache.get(key); if (cachedCopy) { + cacheCounters[cacheName].hits++; if (environment.cache.serverSide.debug) { console.log(`CACHE HIT FOR ${key} in ${cacheName} cache`); } // Check if cached copy is expired (If expired, the key will now be gone from cache) @@ -529,6 +534,7 @@ function checkCacheForRequest(cacheName: string, cache: LRUCache, r serverSideRender(req, res, next, false); } } else { + cacheCounters[cacheName].misses++; if (environment.cache.serverSide.debug) { console.log(`CACHE MISS FOR ${key} in ${cacheName} cache.`); } } @@ -682,8 +688,8 @@ function collectCacheStats(): SsrCacheStats { uptimeSeconds: Math.round(process.uptime()), memory: { rss: memory.rss, heapUsed: memory.heapUsed }, caches: [ - snapshotCache('bot', botCache, !!(environment.production && botCacheEnabled())), - snapshotCache('anonymous', anonymousCache, !!(environment.production && anonymousCacheEnabled())) + snapshotCache('bot', botCache, !!(environment.production && botCacheEnabled()), cacheCounters.bot), + snapshotCache('anonymous', anonymousCache, !!(environment.production && anonymousCacheEnabled()), cacheCounters.anonymous) ] }; } diff --git a/src/backend/ssr-cache-stats.ts b/src/backend/ssr-cache-stats.ts index f828bc2022a..443a3e3a108 100644 --- a/src/backend/ssr-cache-stats.ts +++ b/src/backend/ssr-cache-stats.ts @@ -18,9 +18,17 @@ export interface SsrCacheEntryStats { stale: boolean; } -export interface SsrCacheSnapshot { +/** Request lookups since worker start; stale pages served count as hits. */ +export interface SsrCacheCounters { + hits: number; + misses: number; +} + +export interface SsrCacheSnapshot extends SsrCacheCounters { name: string; enabled: boolean; + /** Hits / (hits + misses), or null before the first lookup. */ + hitRate: number | null; maxEntries: number; configuredTtlMs: number; entryCount: number; @@ -49,7 +57,7 @@ export const SSR_CACHE_STATS_PATH = '/app/cache-stats'; * `dump()` visits stale entries too (unlike `forEach`, which skips them when `allowStale` is false); * `dump()` and `has()` leave recency and TTL untouched. Byte counting is O(total cached HTML). */ -export function snapshotCache(name: string, cache: LRUCache | undefined, enabled: boolean): SsrCacheSnapshot { +export function snapshotCache(name: string, cache: LRUCache | undefined, enabled: boolean, counters?: SsrCacheCounters): SsrCacheSnapshot { const entries: SsrCacheEntryStats[] = []; if (cache) { for (const [key, { value }] of cache.dump()) { @@ -63,9 +71,14 @@ export function snapshotCache(name: string, cache: LRUCache b.htmlBytes - a.htmlBytes || a.key.localeCompare(b.key)); + const hits = counters?.hits ?? 0; + const misses = counters?.misses ?? 0; return { name, enabled, + hits, + misses, + hitRate: hits + misses > 0 ? hits / (hits + misses) : null, maxEntries: cache ? cache.max : 0, configuredTtlMs: cache ? cache.ttl : 0, entryCount: entries.length,