Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions scripts/test-ssr-cache-stats.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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' });
Expand Down
12 changes: 9 additions & 3 deletions server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,10 @@ let botCache: LRUCache<string, SsrCacheEntry>;

// cache of SSR pages for anonymous users. Disabled by default, and only available in production mode
let anonymousCache: LRUCache<string, SsrCacheEntry>;
const cacheCounters = {
bot: { hits: 0, misses: 0 },
anonymous: { hits: 0, misses: 0 },
};

// extend environment with app config for server
extendEnvironmentWithAppConfig(environment, appConfig);
Expand Down Expand Up @@ -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<string, any>, req, res, next): any {
function checkCacheForRequest(cacheName: 'bot' | 'anonymous', cache: LRUCache<string, any>, 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)
Expand All @@ -529,6 +534,7 @@ function checkCacheForRequest(cacheName: string, cache: LRUCache<string, any>, r
serverSideRender(req, res, next, false);
}
} else {
cacheCounters[cacheName].misses++;
if (environment.cache.serverSide.debug) { console.log(`CACHE MISS FOR ${key} in ${cacheName} cache.`); }
}

Expand Down Expand Up @@ -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)
]
};
}
Expand Down
17 changes: 15 additions & 2 deletions src/backend/ssr-cache-stats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<string, SsrCacheEntry> | undefined, enabled: boolean): SsrCacheSnapshot {
export function snapshotCache(name: string, cache: LRUCache<string, SsrCacheEntry> | undefined, enabled: boolean, counters?: SsrCacheCounters): SsrCacheSnapshot {
const entries: SsrCacheEntryStats[] = [];
if (cache) {
for (const [key, { value }] of cache.dump()) {
Expand All @@ -63,9 +71,14 @@ export function snapshotCache(name: string, cache: LRUCache<string, SsrCacheEntr
}
}
entries.sort((a, b) => 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,
Expand Down
Loading