From 0b903267a5022b5874890da9b587d01845d53bd5 Mon Sep 17 00:00:00 2001 From: omartelo Date: Sun, 1 Nov 2026 18:28:16 -0300 Subject: [PATCH] native: count frames on the pprof server A CPU profile of a real window shows where a frame's time goes, never how many frames there are, and the window's cost is frames times that. /debug/vars on the LICH_NATIVE_PPROF address now carries the frames drawn since the window opened; two reads apart give the rate. --- docs/native-window.md | 5 +++-- native/cmd/lich-native/main.go | 1 + native/cmd/lich-native/pprof.go | 10 +++++++++- native/cmd/lich-native/pprof_test.go | 23 +++++++++++++++++++++++ 4 files changed, 36 insertions(+), 3 deletions(-) diff --git a/docs/native-window.md b/docs/native-window.md index eadc44229..66b8ba832 100644 --- a/docs/native-window.md +++ b/docs/native-window.md @@ -56,8 +56,9 @@ from `main` either drowns in merges or freezes the web app for months. `task gallery` builds it into `bin/` and opens it. - **`LICH_NATIVE_PPROF=127.0.0.1:6060`**, set where `lich native` starts, has `lich-native` serve Go's runtime profiles (`net/http/pprof`) on that address, for a heap or CPU profile of a real window: - `go tool pprof http://127.0.0.1:6060/debug/pprof/heap`. Only a loopback address is accepted, and any - process on the machine can read what the profiles carry. + `go tool pprof http://127.0.0.1:6060/debug/pprof/heap`. `/debug/vars` on the same address carries `frames`, + the frames drawn since the window opened: two reads a few seconds apart give the window's frame rate. Only a + loopback address is accepted, and any process on the machine can read what the profiles carry. - **`GOGC`** left unset, `lich-native` collects at 50 instead of Go's 100: a window with many sessions keeps a live heap of hundreds of MB, and at 100 half the resident size is slack the system swaps out. Set `GOGC` to choose another value. diff --git a/native/cmd/lich-native/main.go b/native/cmd/lich-native/main.go index e434e1ce5..e53ef5766 100644 --- a/native/cmd/lich-native/main.go +++ b/native/cmd/lich-native/main.go @@ -376,6 +376,7 @@ func run(w *app.Window, rt backend.Runtime) error { } } e.Frame(gtx.Ops) + frames.Add(1) } } } diff --git a/native/cmd/lich-native/pprof.go b/native/cmd/lich-native/pprof.go index 3ce138330..cd845495d 100644 --- a/native/cmd/lich-native/pprof.go +++ b/native/cmd/lich-native/pprof.go @@ -1,6 +1,7 @@ package main import ( + "expvar" "fmt" "log" "net" @@ -29,9 +30,16 @@ func pprofListener(addr string) (net.Listener, error) { return ln, nil } -// servePprof answers net/http/pprof's endpoints on ln until it closes. +// frames counts the frames the window drew, served at /debug/vars beside +// the profiles: two reads a few seconds apart give the window's frame rate, +// which a profile alone does not show. +var frames = expvar.NewInt("frames") + +// servePprof answers net/http/pprof's endpoints and /debug/vars on ln until +// it closes. func servePprof(ln net.Listener) { mux := http.NewServeMux() + mux.Handle("/debug/vars", expvar.Handler()) mux.HandleFunc("/debug/pprof/", pprof.Index) mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline) mux.HandleFunc("/debug/pprof/profile", pprof.Profile) diff --git a/native/cmd/lich-native/pprof_test.go b/native/cmd/lich-native/pprof_test.go index ad87d7a7a..6726ea801 100644 --- a/native/cmd/lich-native/pprof_test.go +++ b/native/cmd/lich-native/pprof_test.go @@ -1,6 +1,7 @@ package main import ( + "encoding/json" "io" "net/http" "strings" @@ -36,3 +37,25 @@ func TestPprofServesTheHeapOnLoopback(t *testing.T) { t.Fatalf("status %d, body starts %.80q", resp.StatusCode, body) } } + +func TestPprofServesTheFrameCount(t *testing.T) { + ln, err := pprofListener("127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + defer ln.Close() + go servePprof(ln) + frames.Add(3) + resp, err := http.Get("http://" + ln.Addr().String() + "/debug/vars") + if err != nil { + t.Fatal(err) + } + defer resp.Body.Close() + var vars struct{ Frames int64 } + if err := json.NewDecoder(resp.Body).Decode(&vars); err != nil { + t.Fatal(err) + } + if vars.Frames < 3 { + t.Fatalf("frames = %d, want the 3 drawn", vars.Frames) + } +}