Skip to content

osr: expose present liveness, and stop a renderer crash-loop from wedging silently - #33

Merged
wenkaifan0720 merged 1 commit into
mainfrom
fix/cef-freeze-recovery
Aug 6, 2026
Merged

osr: expose present liveness, and stop a renderer crash-loop from wedging silently#33
wenkaifan0720 merged 1 commit into
mainfrom
fix/cef-freeze-recovery

Conversation

@wenkaifan0720

Copy link
Copy Markdown
Collaborator

Two halves of the same problem: a frozen webview tile that nothing can see and nothing can recover. From an investigation of a long-standing "webview freezes, no way back" report (Campus side: FlutterFlow/work_canvas#361).

1. sessionStats() — pixel liveness had no signal at all

A consumer could prove the renderer still executed JavaScript (an eval round-trip) while the texture had been stale for minutes. That is the actual user-visible freeze — JS alive, pixels dead — and no probe in the stack could see it.

CefWebSession now counts presents where they already arrive, so this adds no host IPC and no protocol change:

final s = await controller.sessionStats();
// presentCount, lastPresentAgoMs, firstPresentSeen, frozen

Two readings bracket the truth: presentCount unchanged while a page should be animating means the pixels are stuck, whatever a JS probe says. frozen is included deliberately — a discarded session produces no presents by design, and that must stay distinguishable from a wedge.

2. Renderer crash-loop suicide

A renderer that dies is normally recoverable: OnRenderProcessTerminated reloads and the fresh child takes over. But if the child can no longer be spawned, that reload re-crashes instantly and loops forever — and the host notices nothing:

  • the browser process is healthy, so the IPC pipe stays up,
  • no processGone is ever emitted,
  • the embedder sees a tile stuck on its last frame, with no signal and no way back.

The observed trigger is the app bundle being replaced under a running host (any rebuild/relaunch), after which every child SIGTRAPs at startup resolving the framework it was launched from. Crash-log-proven — two such reports on the reporting machine in a single morning:

base::apple::SetOverrideFrameworkBundlePath
util_mac::BasicStartupComplete
ChromeMainDelegateCef::BasicStartupComplete

So make the unrecoverable case look like the recoverable one. Past a burst threshold (4 deaths in 10s — unreachable by ordinary flakiness; an isolated renderer crash still just reloads), exit deliberately. The pipe EOFs → the plugin reports processGone → the embedder's existing recreate funnel spawns a fresh host, which re-resolves the binary from disk and therefore picks up the new bundle.

A silent wedge becomes the recovery path that already works.

Deliberately not changed

The steady-state liveness sweep still accepts a post-establishment no-present browser as healthy-static. Its audited comment is right that a nudge cannot distinguish "nothing to paint" from "can't paint", and naive escalation previously recreate-stormed every static tile (36 stalls / 33 browsers in one session). sessionStats() instead gives consumers the data to make that judgement with more context than the sweep has.

Verification

flutter analyze clean. cef_host compiles clean (Developer-ID, renderer sandbox on). Exercised through Campus against this branch: a new cef_pixel_liveness e2e asserts presents advance for a visible animating tile (observed 2 → 26 over 6s), and the existing agent-soak is unregressed at 15/15 with gaveUp=0.

Protocol version is unchanged (v6) — no new ops. The host binary content hash does change, so consumers need make publish-cef-host + make pin-cef as usual.

…ging silently

Two halves of the same problem — a frozen tile that nothing can see or recover.

sessionStats(): pixel liveness had no observable signal at all. A consumer
could prove the renderer executed JavaScript while the texture had been stale
for minutes. CefWebSession now counts presents where they already arrive, so
this adds no host IPC and no protocol change, and reports presentCount,
lastPresentAgoMs, firstPresentSeen and frozen. `frozen` matters: a discarded
session produces no presents BY DESIGN, and that must stay distinguishable from
a wedge.

Renderer crash-loop suicide: a renderer that dies is normally recoverable —
OnRenderProcessTerminated reloads and the fresh child takes over. But if the
child can no longer be SPAWNED, that reload re-crashes instantly and loops
forever while the host notices nothing: the browser process is healthy, the IPC
pipe stays up, no processGone is emitted, and the embedder sees a tile stuck on
its last frame with no signal and no way back. The observed trigger is the app
bundle being replaced under a running host (any rebuild/relaunch), after which
every child SIGTRAPs at startup resolving the framework it was launched from —
two such crash reports on the reporting machine in one morning.

So make the unrecoverable case look like the recoverable one: past a burst
threshold (4 deaths in 10s, unreachable by ordinary flakiness), exit
deliberately. The pipe EOFs, the plugin reports processGone, and the embedder's
existing recreate funnel spawns a fresh host — which re-resolves the binary
from disk and so picks up the NEW bundle. A silent wedge becomes the recovery
path that already works.

Deliberately NOT changed: the steady-state liveness sweep still accepts a
post-establishment no-present browser as healthy-static. Its audited comment is
right that a nudge cannot distinguish "nothing to paint" from "can't paint",
and naive escalation previously recreate-stormed every static tile. sessionStats
gives consumers the data to make that call with better context than the sweep
has.
@wenkaifan0720
wenkaifan0720 merged commit 05bb923 into main Aug 6, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant