[object Object]

← back to Ticket System

TK-11506: bound cache retention with idle eviction

965470bec7cb8293bd19527180a5890d4c0fb6c2 · 2026-09-11 13:46:27 -0700 · Steve Abrams

Caching the fold costs ~350MB of steady RSS — measured hot-vs-hot against the
pre-fix build on a scratch port, 98-176MB before vs 444MB after, not the 233MB
cold-start figure which understates it. This box runs its swap-pressure canary at
WARN, so the retention gets a ceiling: an entry nobody has read for 120s is
dropped and rebuilt on next use at one ~190ms fold.

Proven to actually fire, on an unpolled scratch instance with the window shortened
through the TK_VIEW_IDLE_MS test seam: warm 0.0039s -> post-sweep 0.281s (a real
rebuild) -> re-warm 0.0037s. On the LIVE board it rarely fires, because the board
is continuously polled by the UI and TicketBar, so live RSS stays ~450MB; the
bound that matters there is the measured plateau (465/662/663/524/525MB across
960 requests), not eviction. Stating that rather than claiming memory it does not
actually give back.

The env override is a test seam only; no plist or pm2 config passes it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

Files touched

Diff

commit 965470bec7cb8293bd19527180a5890d4c0fb6c2
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Fri Sep 11 13:46:27 2026 -0700

    TK-11506: bound cache retention with idle eviction
    
    Caching the fold costs ~350MB of steady RSS — measured hot-vs-hot against the
    pre-fix build on a scratch port, 98-176MB before vs 444MB after, not the 233MB
    cold-start figure which understates it. This box runs its swap-pressure canary at
    WARN, so the retention gets a ceiling: an entry nobody has read for 120s is
    dropped and rebuilt on next use at one ~190ms fold.
    
    Proven to actually fire, on an unpolled scratch instance with the window shortened
    through the TK_VIEW_IDLE_MS test seam: warm 0.0039s -> post-sweep 0.281s (a real
    rebuild) -> re-warm 0.0037s. On the LIVE board it rarely fires, because the board
    is continuously polled by the UI and TicketBar, so live RSS stays ~450MB; the
    bound that matters there is the measured plateau (465/662/663/524/525MB across
    960 requests), not eviction. Stating that rather than claiming memory it does not
    actually give back.
    
    The env override is a test seam only; no plist or pm2 config passes it.
    
    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---
 server.js | 14 ++++++++++++--
 1 file changed, 12 insertions(+), 2 deletions(-)

diff --git a/server.js b/server.js
index 0f85242d..f21636fb 100644
--- a/server.js
+++ b/server.js
@@ -105,15 +105,25 @@ function eventsKey() {
 }
 // The board's own writes must be visible to the very next read, so skip the stat TTL.
 function invalidateViews() { viewStat.at = 0; }
+// Retention is bounded in BOTH directions: a generation is replaced when the log changes,
+// and an entry nobody has read for VIEW_IDLE_MS is dropped. Caching the fold costs ~350MB
+// of steady RSS (measured hot-vs-hot: 98-176MB before, 444MB after), which is worth paying
+// while the board is actually being polled and not worth holding when it is idle — this box
+// runs its swap-pressure canary at WARN. A rebuild after idling costs one ~190ms fold.
+const VIEW_IDLE_MS = Number(process.env.TK_VIEW_IDLE_MS) || 120000; // env override is a TEST SEAM; no plist/pm2 config passes it
 const views = new Map();
 function cachedView(name, build) {
   const key = eventsKey();
   const hit = views.get(name);
-  if (hit && hit.key === key) return hit.value;
+  if (hit && hit.key === key) { hit.seen = Date.now(); return hit.value; }
   const value = build();
-  views.set(name, { key, value });
+  views.set(name, { key, value, seen: Date.now() });
   return value;
 }
+setInterval(() => {
+  const cutoff = Date.now() - VIEW_IDLE_MS;
+  for (const [name, entry] of views) if (entry.seen < cutoff) views.delete(name);
+}, 60000).unref();
 // A cached payload is the encoded body, not the object — stringify is ~31ms of the cost.
 function cachedPayload(name, build) {
   return cachedView('payload:' + name, () => {

← 81f857a4 TK-11506: ship the head-of-line harness with its negative te  ·  back to Ticket System  ·  run-ticket: add local-qwen-14b-mac1 runner to offload qwen t 516f09bb →