fix(cv2-capture): land getCaptureStats+capture-rearm to main (complete SoT) #40

Closed
triform-admin wants to merge 27 commits from capstats-plus-rearm into main

Durable follow-up: main already has the capture-rearm fix (b3359fb) but LACKS the getCaptureStats health-gate (CV2-CAPTURE-STATS). capstats-plus-rearm (f5dcb60) is the verified both-features tree currently deployed as cr7727-f5dcb6087fa9. Landing it makes main a complete source-of-truth so a future auto-rebuild from main cannot silently drop capstats. No functional change to what is deployed (same image).

Durable follow-up: main already has the capture-rearm fix (b3359fb) but LACKS the getCaptureStats health-gate (CV2-CAPTURE-STATS). capstats-plus-rearm (f5dcb60) is the verified both-features tree currently deployed as cr7727-f5dcb6087fa9. Landing it makes main a complete source-of-truth so a future auto-rebuild from main cannot silently drop capstats. No functional change to what is deployed (same image).
Adds a read-only Cb.getCaptureStats method exposing the FrameSink
frame-production counter (frames_received_from_capturer) so the isolator
can verify a renderer is actually PRODUCING before freezing it into a
warm-snapshot golden.

ROOT CAUSE this enables fixing (poisoned warm-golden): the isolator's
golden-capture warmup starts FrameSink capture then blindly sleeps 3s and
assumes production. When the ~8% per-boot GPU wedge (run11: BeginFrame
ack-loop healthy but renderer produces ZERO frames) strikes during
capture, startFrameSinkCapture still SUCCEEDS (peer-independent pipe) so a
wedged, zero-producing renderer gets frozen into the golden. Every restore
is born-frozen; the poison persists on the per-worker hostPath cache. One
node (triform-6) took out ~25% of prod video this way.

The counter is the SAME frames_received the guest's cb_begin_frame_driver
uses to decide VERDICT=PRODUCING vs RENDERER-STARVED. Growing => healthy;
flat 0 => wedged (do not publish that golden).

Reuses the already-plumbed track_source_getter_ (lazy dispatch-time
resolve, CV2-69 order-safe); zero main_parts/content_browser_client
changes. Lock-free GetStats() copy, safe off-sequence. int32-saturated
CBOR (crdtp has no uint64 encoder; gate only reads 0-vs-growing).

Marker: CV2-CAPTURE-STATS (grep -a on the linked binary).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
fix(cv2-capture): self-heal FrameSink capture across cross-doc nav RVH swap
Some checks failed
CI / Docs link check (pull_request) Failing after 33s
CI / Lint (pull_request) Failing after 34s
CodeQL / Analyze go (pull_request) Failing after 35s
CodeQL / Analyze javascript-typescript (pull_request) Failing after 36s
native-peer-gate / Build chromeless:ci for gate (pull_request) Failing after 48s
native-peer-gate / native-peer-gate-scaffold (permissive) (pull_request) Has been skipped
native-peer-gate / native-peer-gate-strict (M7 gate) (pull_request) Has been skipped
CI / Build container image (pull_request) Failing after 59s
CI / Container smoke test (pull_request) Has been skipped
E2E / docker-compose + Playwright (pull_request) Failing after 1m2s
f5dcb6087f
THE root cause of cold-boot frozen video. A cold-boot cb-chromium guest
starts on about:blank; capture is armed on that page's FrameSinkId; then
the navigation to real content swaps the RenderViewHost (and its
FrameSinkId), but nothing re-points the capturer at the new sink. It
stays bound to the dead pre-nav sink -> VERDICT=RENDERER-STARVED,
frames_encoded=0, no video. Warm-restored golden sessions dodge this
(they resume already-on-content with no about:blank->nav transition),
which is why cold-boot was the sole frozen path -- and why the isolator
cold-boot fallback, though correct at the allocation layer, produced a
connected-but-frozen stream.

Fix: make capture self-healing at the layer that owns both the observer
and the capturer. CbActiveWebContentsResolver already observes the
captured WebContents and receives RenderViewHostChanged, but is
content-layer glue with no capturer handle (the FrameSink capturer is
deliberately content-agnostic -- FrameSinkIds only). So the resolver
gains an optional re-arm closure (SetRecaptureOnRvhSwapCallback) that
CloudBrowserBrowserMainParts supplies; on an RVH swap of the active
capture the resolver POSTs it (the new RWH's view/FSID may not be the
WC's live primary synchronously at RenderViewHostChanged -- a same-turn
UI PostTask runs after the swap settles). The owner
(RearmCaptureAfterRvhSwap) re-resolves WC->RWHV->RWH->GetFrameSinkId,
forces the fresh view SHOWING (the post-nav RWH boots HIDDEN -> renderer
throttled, same gating Cb.startFrameSinkCapture applies), calls
cb_track_source_->StartCapture on the new sink, and refreshes the
resolver's active target.

capturer.cc:138-164 confirms StartCapture on a NEW FrameSinkId re-targets
(ChangeTarget), not a no-op -- so the re-arm is effective. For a terminal
nav whose fresh RWHV isn't attached by the task turn (no later swap to
retrigger), the owner re-posts itself up to 5x/50ms (bounded) rather than
strand the capturer. Stale "Start is a no-op" comments in
cb_devtools_agent.cc + cb_framesink_video_track_source.{h,cc} that
predated the retarget path are corrected so this fix can't be mistakenly
reverted as dead code.

Layering preserved (capturer stays content-agnostic; re-arm lives in
build-integration where content deps already exist). Optional callback:
unset -> legacy invalidate-only behaviour, so tests/no-capturer builds
are unaffected. Adversarially reviewed (lifetime/UAF, re-entrancy,
threading, StartCapture-idempotency, includes, Show() walk) -- no
correctness bugs. Composes with the isolator cold-boot fallback (now a
COMPLETE fix, not frozen) and the per-language warm-slot fix.

Guest change -- needs a cb-chromium rootfs bake to deploy.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Merge pull request 'cv2(gpu-death): compose guest GPU-death fix onto the deployed capstats+rearm lineage (deploy-ready superset)' (#42) from cv2/gpu-death-recycle into capstats-plus-rearm
Some checks failed
CI / Lint (pull_request) Failing after 3s
native-peer-gate / Build chromeless:ci for gate (pull_request) Failing after 18s
CI / Build container image (pull_request) Failing after 19s
native-peer-gate / native-peer-gate-scaffold (permissive) (pull_request) Has been skipped
CI / Container smoke test (pull_request) Has been skipped
native-peer-gate / native-peer-gate-strict (M7 gate) (pull_request) Has been skipped
CI / Docs link check (pull_request) Failing after 24s
CodeQL / Analyze javascript-typescript (pull_request) Failing after 25s
CodeQL / Analyze go (pull_request) Failing after 25s
E2E / docker-compose + Playwright (pull_request) Failing after 1m1s
78e5bb4138
Reviewed-on: #42
Author
Owner

Closing as superseded — every payload this branch carries is now on main by patch-id:

Payload This branch On main patch-id
capture-rearm (RVH-swap self-heal) f5dcb60 b3359fb 561b3361… identical
Cb.getCaptureStats a71b0d0 (via 151c078) 48adabc (PR #43) 8bf63f5a… identical
gpu-death self-detect → session_unhealthy 0a997b2 merged via PR #41 (cd741fb, on main) same branch commit

The residual origin/main...capstats-plus-rearm diff is merge plumbing only (no infra/build deltas remain). The deployed sw-pool image cr7727-78e5bb4 == this branch tip, and the convergence build onto main (C1/C2 of the current plan) replaces it with a main-lineage image — after which this branch is retired entirely.

Closing as superseded — every payload this branch carries is now on `main` by patch-id: | Payload | This branch | On main | patch-id | |---|---|---|---| | capture-rearm (RVH-swap self-heal) | f5dcb60 | b3359fb | `561b3361…` identical | | Cb.getCaptureStats | a71b0d0 (via 151c078) | 48adabc (PR #43) | `8bf63f5a…` identical | | gpu-death self-detect → session_unhealthy | 0a997b2 | merged via PR #41 (cd741fb, on main) | same branch commit | The residual `origin/main...capstats-plus-rearm` diff is merge plumbing only (no infra/build deltas remain). The deployed sw-pool image `cr7727-78e5bb4` == this branch tip, and the convergence build onto main (C1/C2 of the current plan) replaces it with a main-lineage image — after which this branch is retired entirely.
triform-admin closed this pull request 2026-07-30 02:56:05 +00:00
Some checks failed
CI / Lint (pull_request) Failing after 3s
native-peer-gate / Build chromeless:ci for gate (pull_request) Failing after 18s
CI / Build container image (pull_request) Failing after 19s
native-peer-gate / native-peer-gate-scaffold (permissive) (pull_request) Has been skipped
CI / Container smoke test (pull_request) Has been skipped
native-peer-gate / native-peer-gate-strict (M7 gate) (pull_request) Has been skipped
CI / Docs link check (pull_request) Failing after 24s
CodeQL / Analyze javascript-typescript (pull_request) Failing after 25s
CodeQL / Analyze go (pull_request) Failing after 25s
E2E / docker-compose + Playwright (pull_request) Failing after 1m1s

Pull request closed

Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
triform/chromeless!40
No description provided.