From 407c49e017cf4ff84d826360308c1fcd27353e94 Mon Sep 17 00:00:00 2001 From: Kpa-clawbot <259247574+Kpa-clawbot@users.noreply.github.com> Date: Fri, 27 Mar 2026 11:04:43 -0700 Subject: [PATCH] fix(go): add eventLoop to /api/health with GC pause percentiles, fixes #147 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Go's /api/health was missing the eventLoop object that Node.js provides. The perf.js frontend reads health.eventLoop.p95Ms which crashed with 'Cannot read properties of undefined' when served by the Go server. Adds eventLoop field using GC pause data from runtime.MemStats.PauseNs (last 256 pauses) to compute p50Ms, p95Ms, p99Ms, currentLagMs, maxLagMs — matching the Node.js response shape exactly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- cmd/server/routes.go | 23 +++++++++++++++++++++++ cmd/server/routes_test.go | 11 +++++++++++ 2 files changed, 34 insertions(+) diff --git a/cmd/server/routes.go b/cmd/server/routes.go index c440da8e..0ddf87f6 100644 --- a/cmd/server/routes.go +++ b/cmd/server/routes.go @@ -297,6 +297,22 @@ func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) { } } + // Build eventLoop-equivalent from GC pause data (matches Node.js shape) + var gcPauses []float64 + n := int(m.NumGC) + if n > 256 { + n = 256 + } + for i := 0; i < n; i++ { + idx := (int(m.NumGC) - n + i) % 256 + gcPauses = append(gcPauses, float64(m.PauseNs[idx])/1e6) + } + sortedPauses := sortedCopy(gcPauses) + var lastPauseMs float64 + if m.NumGC > 0 { + lastPauseMs = float64(m.PauseNs[(m.NumGC+255)%256]) / 1e6 + } + writeJSON(w, map[string]interface{}{ "status": "ok", "engine": "go", @@ -311,6 +327,13 @@ func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) { "external": 0, "heapMB": round(float64(m.HeapAlloc)/1048576, 1), }, + "eventLoop": map[string]interface{}{ + "currentLagMs": round(lastPauseMs, 1), + "maxLagMs": round(percentile(sortedPauses, 1.0), 1), + "p50Ms": round(percentile(sortedPauses, 0.5), 1), + "p95Ms": round(percentile(sortedPauses, 0.95), 1), + "p99Ms": round(percentile(sortedPauses, 0.99), 1), + }, "goRuntime": map[string]interface{}{ "goroutines": runtime.NumGoroutine(), "gcPauses": m.NumGC, diff --git a/cmd/server/routes_test.go b/cmd/server/routes_test.go index 36301beb..1a8475e9 100644 --- a/cmd/server/routes_test.go +++ b/cmd/server/routes_test.go @@ -81,6 +81,17 @@ func TestHealthEndpoint(t *testing.T) { t.Error("expected estimatedMB in packetStore") } + // Verify eventLoop (GC pause metrics matching Node.js shape) + el, ok := body["eventLoop"].(map[string]interface{}) + if !ok { + t.Fatal("expected eventLoop object in health response") + } + for _, field := range []string{"currentLagMs", "maxLagMs", "p50Ms", "p95Ms", "p99Ms"} { + if _, ok := el[field]; !ok { + t.Errorf("expected %s in eventLoop", field) + } + } + // Verify cache has real structure cache, ok := body["cache"].(map[string]interface{}) if !ok {