From ddb6f806ac2d6eaf215501de34b65e4dfc2a02bf Mon Sep 17 00:00:00 2001 From: yetone Date: Thu, 8 Oct 2026 21:20:54 +0800 Subject: [PATCH] gui: the sessions warm-up hands its garbage back to the system when it ends, so a magpie just started doesn't read ~470 MB at rest for its first two minutes (H20 on Discord) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The warm-up reads every session file for the Sessions page and Usage › Requests. On a 13.5k-session history it allocates ~830 MB (QueryPage ~570, StatsFor ~260) to keep ~80 MB of indexes, and Go's scavenger returns the freed pages only over the next minutes. warmUp now calls debug.FreeOSMemory once its reads are done: one forced GC and scavenge, once, at start. RSS of a linux nogui `magpie web` in Docker on a copy of a real config with those sessions mounted read-only, sampled every 15 s from start (with the dsh round change before it): 15 s 472 -> 197 MB, 30 s 475 -> 285, 60 s 417 -> 281, 2 min 337 -> 281, 3.5 min 266 -> 218, 12 min ~250 -> ~204. TestWarmUpReturnsItsGarbage fails without the FreeOSMemory call (10 of 10: 46-334 MB of freed heap still held, or the heap not yet collected) and passes 10 of 10 with it. go test -tags nogui ./internal/gui ./internal/sessions (Docker golang:1.26.8, temp HOME), go vet -tags nogui ./internal/gui for linux, windows and darwin, and gofmt -l pass. Not measured on macOS: a fresh build there hangs at exec behind syspolicyd today. --- internal/gui/sessions.go | 22 +++++++++++++++++++--- internal/gui/sessions_warm_test.go | 28 ++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 3 deletions(-) create mode 100644 internal/gui/sessions_warm_test.go diff --git a/internal/gui/sessions.go b/internal/gui/sessions.go index f9fe3c44..c89a6fb6 100644 --- a/internal/gui/sessions.go +++ b/internal/gui/sessions.go @@ -8,6 +8,7 @@ import ( "net/http" "os" "runtime" + "runtime/debug" "strconv" "strings" "sync" @@ -320,12 +321,27 @@ func warmSessions() { } go func() { time.Sleep(3 * time.Second) - statsFor(0) - statsFor(30) - usage.QueryPage(usage.All, usage.Filter{}, 0, 50) + warmUp( + func() { statsFor(0) }, + func() { statsFor(30) }, + func() { usage.QueryPage(usage.All, usage.Filter{}, 0, 50) }, + ) }() } +// warmUp runs the warm-up's reads, then hands what they threw away back to +// the system at once. Reading a long history allocates many times what it +// keeps (a 13k-session Codex and Claude Code history: ~830 MB allocated to +// keep ~80 MB of indexes), and Go's scavenger returns those pages only over +// the next minutes: a magpie that had just started read ~470 MB for its +// first two minutes at rest. +func warmUp(steps ...func()) { + for _, step := range steps { + step() + } + debug.FreeOSMemory() +} + // openTerminal runs a command in the chosen Mac terminal through a .command // file, as it would open from Finder: no Automation consent. // The shell is left open when the agent quits. diff --git a/internal/gui/sessions_warm_test.go b/internal/gui/sessions_warm_test.go new file mode 100644 index 00000000..97b68f1a --- /dev/null +++ b/internal/gui/sessions_warm_test.go @@ -0,0 +1,28 @@ +package gui + +import ( + "runtime" + "testing" +) + +// The warm-up's garbage goes back to the system when it ends, not over the +// scavenger's next minutes: an idle magpie that just started read ~470 MB. +func TestWarmUpReturnsItsGarbage(t *testing.T) { + var keep [][]byte + warmUp(func() { + // a read that allocates far more than it keeps + for range 256 { + keep = append(keep, make([]byte, 1<<20)) + } + keep = [][]byte{keep[0]} + }) + var m runtime.MemStats + runtime.ReadMemStats(&m) + if held := m.HeapIdle - m.HeapReleased; held > 32<<20 { + t.Fatalf("after the warm-up %d MB of freed heap is still held from the system", held>>20) + } + if m.HeapAlloc > 128<<20 { + t.Fatalf("after the warm-up %d MB of heap is still in use", m.HeapAlloc>>20) + } + runtime.KeepAlive(keep) +}