Bound the startup browser-dir probe and log startup phases
The relaunch restore probes the saved browser dir with os.ReadDir on the UI thread before the first frame. A FUSE stall on external storage there would hang the app on the splash screen (reported on device, intermittent; the 17:15 occurrence rotated out of logcat before it could be pulled and left no pad.pad ANR). Two changes: - dirReadable: run the probe in a helper goroutine with a 2 s deadline; a stalled read now costs at most 2 s and falls back to the startup directory instead of hanging the splash. - Phase logs (session load duration, restore, entering frame loop, first frame rendered) so the next hang bisects itself: the missing line names the blocked phase. Verified on device: full chain logs within 40 ms of process start.
This commit is contained in:
parent
6bd5588ade
commit
3afeb0f8d4
|
|
@ -117,7 +117,10 @@ func run(w *app.Window) error {
|
||||||
// the browser at the saved directory.
|
// the browser at the saved directory.
|
||||||
sessPath := sessionFilePath()
|
sessPath := sessionFilePath()
|
||||||
logic.SetSessionSaver(newSessionSaver(sessPath))
|
logic.SetSessionSaver(newSessionSaver(sessPath))
|
||||||
if sess, ok := loadSession(sessPath); ok {
|
tSess := time.Now()
|
||||||
|
sess, ok := loadSession(sessPath)
|
||||||
|
log.Printf("startup: session load %s (%v)", map[bool]string{true: "restorable", false: "none"}[ok], time.Since(tSess))
|
||||||
|
if ok {
|
||||||
if sess.InBrowser {
|
if sess.InBrowser {
|
||||||
log.Printf("restoring session: browser path=%q", sess.BrowserPath)
|
log.Printf("restoring session: browser path=%q", sess.BrowserPath)
|
||||||
// The saved directory may have vanished (deleted, synced away) or
|
// The saved directory may have vanished (deleted, synced away) or
|
||||||
|
|
@ -126,8 +129,11 @@ func run(w *app.Window) error {
|
||||||
// media_rw:media_rw 0750; only /storage/emulated/0 is app-exposed).
|
// media_rw:media_rw 0750; only /storage/emulated/0 is app-exposed).
|
||||||
// Probe with a real directory read — the same operation the
|
// Probe with a real directory read — the same operation the
|
||||||
// browser's index does — rather than os.Stat, which passes for
|
// browser's index does — rather than os.Stat, which passes for
|
||||||
// unreadable dirs. Fall back to the startup directory otherwise.
|
// unreadable dirs. The probe runs BOUNDED: it happens on the UI
|
||||||
if _, err := os.ReadDir(sess.BrowserPath); err == nil {
|
// thread before the first frame, and a FUSE stall on external
|
||||||
|
// storage must not turn into a splash-screen hang — if the read
|
||||||
|
// is not done in time, start in the startup directory instead.
|
||||||
|
if dirReadable(sess.BrowserPath) {
|
||||||
logic.BeginBrowserRestore(sess.BrowserPath)
|
logic.BeginBrowserRestore(sess.BrowserPath)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -158,6 +164,10 @@ func run(w *app.Window) error {
|
||||||
|
|
||||||
go frameReceiver(w, &mu, &frame, logic.FrameChan())
|
go frameReceiver(w, &mu, &frame, logic.FrameChan())
|
||||||
go logic.Run()
|
go logic.Run()
|
||||||
|
log.Printf("startup: logic running, entering frame loop")
|
||||||
|
// 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.
|
||||||
|
|
||||||
// lastExcl tracks the selection-handle grab boxes last sent to
|
// lastExcl tracks the selection-handle grab boxes last sent to
|
||||||
// SetGestureExclusions (Android: keeps the system back gesture from
|
// SetGestureExclusions (Android: keeps the system back gesture from
|
||||||
|
|
@ -176,6 +186,7 @@ func run(w *app.Window) error {
|
||||||
// lastFrameW/H hold the previous FrameEvent's window size (px); 0 = no
|
// lastFrameW/H hold the previous FrameEvent's window size (px); 0 = no
|
||||||
// frame yet. Used to spot shrink frames (see ZeroWheelScroll below).
|
// frame yet. Used to spot shrink frames (see ZeroWheelScroll below).
|
||||||
var lastFrameW, lastFrameH int
|
var lastFrameW, lastFrameH int
|
||||||
|
var firstFrame bool
|
||||||
|
|
||||||
for {
|
for {
|
||||||
e := w.Event()
|
e := w.Event()
|
||||||
|
|
@ -193,6 +204,10 @@ func run(w *app.Window) error {
|
||||||
PixelHeight: e.Config.Size.Y,
|
PixelHeight: e.Config.Size.Y,
|
||||||
}
|
}
|
||||||
case app.FrameEvent:
|
case app.FrameEvent:
|
||||||
|
if !firstFrame {
|
||||||
|
firstFrame = true
|
||||||
|
log.Printf("startup: first frame rendered")
|
||||||
|
}
|
||||||
if perfOn {
|
if perfOn {
|
||||||
presentN++
|
presentN++
|
||||||
if presentStart.IsZero() {
|
if presentStart.IsZero() {
|
||||||
|
|
@ -467,6 +482,29 @@ func run(w *app.Window) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// dirReadable reports whether path can be listed by this app, with a 2 s
|
||||||
|
// deadline (see the startup probe in run): it runs os.ReadDir in a helper
|
||||||
|
// goroutine so a FUSE stall on external storage cannot hold the UI thread
|
||||||
|
// before the first frame (a splash-screen hang). The helper goroutine leaks
|
||||||
|
// on timeout — a deliberate, one-per-launch, only-on-stall trade.
|
||||||
|
func dirReadable(path string) bool {
|
||||||
|
ch := make(chan error, 1)
|
||||||
|
go func() {
|
||||||
|
_, err := os.ReadDir(path)
|
||||||
|
ch <- err
|
||||||
|
}()
|
||||||
|
select {
|
||||||
|
case err := <-ch:
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("startup: browser dir %q not readable: %v (starting in the startup directory)", path, err)
|
||||||
|
}
|
||||||
|
return err == nil
|
||||||
|
case <-time.After(2 * time.Second):
|
||||||
|
log.Printf("startup: browser dir probe for %q timed out (starting in the startup directory)", path)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// exclEqual reports whether two exclusion-rect sets are identical.
|
// exclEqual reports whether two exclusion-rect sets are identical.
|
||||||
// newSessionSaver builds the relaunch-session file writer registered with
|
// newSessionSaver builds the relaunch-session file writer registered with
|
||||||
// the logic goroutine (spec §7). The file is a tiny JSON snapshot; a torn
|
// the logic goroutine (spec §7). The file is a tiny JSON snapshot; a torn
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user