diff --git a/cmd/pad/main.go b/cmd/pad/main.go index dc4b43d..18d59b5 100644 --- a/cmd/pad/main.go +++ b/cmd/pad/main.go @@ -7,6 +7,7 @@ import ( "log" "os" "path/filepath" + "runtime/debug" "strings" "sync" "time" @@ -156,9 +157,24 @@ func run(w *app.Window) error { var mu sync.Mutex var frame editor.Frame + firstFrameCh := make(chan struct{}) // closed when the first frame renders (watchdog) + go frameReceiver(w, &mu, &frame, logic.FrameChan()) go logic.Run() log.Printf("startup: logic running, entering frame loop") + // Startup watchdog: if no frame has been rendered 15 s after entering + // the frame loop, dump every goroutine's stack. A splash-screen hang is + // intermittent and usually gone by the time anyone can attach a + // debugger; the dump makes the next occurrence self-reporting — which + // goroutine is blocked where (FUSE read, channel, GL loop) is the whole + // diagnosis. + go func() { + select { + case <-firstFrameCh: + case <-time.After(15 * time.Second): + log.Printf("STARTUP WATCHDOG: no frame 15s after entering the frame loop — goroutine dump:\n%s", debug.Stack()) + } + }() // firstFrame logs the moment the UI actually drew something (splash // done): the anchor for bisecting startup hangs — the log line missing // after "entering frame loop" points at the blocked phase. @@ -200,6 +216,7 @@ func run(w *app.Window) error { case app.FrameEvent: if !firstFrame { firstFrame = true + close(firstFrameCh) log.Printf("startup: first frame rendered") } if perfOn {