diff --git a/internal/gateway/gateway_test.go b/internal/gateway/gateway_test.go index 2e2e636..6a8e9d6 100644 --- a/internal/gateway/gateway_test.go +++ b/internal/gateway/gateway_test.go @@ -999,3 +999,19 @@ func TestCannotDeleteOwnKey(t *testing.T) { t.Errorf("unexpected message: %s", rr.Body.String()) } } + +// TestGatewayRingStaysSmall pins the resident record ring at the small default. +// The gateway used to construct Stats with a 10000-record ring, which after the +// full-history aggregate scan meant 10000 resident Req structs — putting back the +// ~25 MB the on-demand paging work removed. Aggregates cover all of history; +// the ring only needs the first screen plus the 5-minute status windows. +func TestGatewayRingStaysSmall(t *testing.T) { + g := newTestGateway(t) + g.stats.mu.Lock() + max := g.stats.maxRecs + g.stats.mu.Unlock() + if max != defaultRingSize { + t.Fatalf("gateway record ring = %d, want %d (a big ring reintroduces the resident-memory cost)", + max, defaultRingSize) + } +} diff --git a/internal/gateway/server.go b/internal/gateway/server.go index 97b29d4..4f6dec0 100644 --- a/internal/gateway/server.go +++ b/internal/gateway/server.go @@ -120,7 +120,13 @@ func New(c *core.Core, gatewayKeys []string) (*Gateway, error) { if err != nil { return nil, err } - st := NewStats(10000) + // The record ring is deliberately small: aggregates are built from the full + // audit history at startup (see Stats.LoadAudit), so the ring only has to + // cover the status page's 5-minute source windows and the dashboard's first + // screen. Everything older is paged from disk by /api/stats/records. + // A large ring here would put the ~25 MB of resident Req structs straight + // back, which is exactly what the on-demand paging removed. + st := NewStats(defaultRingSize) if cfg := c.Config(); cfg != nil && cfg.RuntimeFile != "" { st.LoadAudit(cfg.RuntimeFile + ".audit.jsonl") }