docs(findings): the resize latch, read from viz source — and the gateway cert manifest fix #117

Open
triform-admin wants to merge 2 commits from docs/resize-latch-probe-a into main

Answers the resize defect's open question with a mechanism read from the pinned
Chromium source, plus one live probe. No C++ — attempt 3's code is
deliberately left unlanded.

The latch

Three attempts adjusted when we issue BeginFrames around a resize. The
blocker is upstream of that, and no ordering of our own issues can reach it:

Display::Resize                             display.cc:473
  -> damage_tracker_->DisplayResized()
       expecting_root_surface_damage_because_of_resize_ = true
                                            display_damage_tracker.cc:82

While set, DesiredBeginFrameDeadlineMode returns kLate
(display_scheduler.cc:618) and all_surfaces_ready is false (:597). The
Display never reaches a deadline, never runs the pending external-BeginFrame
callback, and our ack never arrives.

It clears in exactly two places: root-surface damage
(display_damage_tracker.cc:101), or SetNewRootSurface (:61-66) — whose
only path is Display::SetLocalSurfaceId (display.cc:432) from
RootCompositorFrameSinkImpl::SubmitCompositorFrame (:569), and only when
the submitted LocalSurfaceId differs from the last activated one.

That retro-explains every attempt, including why attempt 2's nudge could never
work: Compositor::ScheduleFullRedraw damages the browser's own layer tree
(compositor.cc:466) — neither root-surface damage nor a new LocalSurfaceId.

Probe A: the renderer relays out

Fresh worker, live viewer, guest cr7727-92539396945c (pre-attempt-3, so a
clean control). Measured twice on two separate workers:

NEGATIVE CONTROL   framesReceived 628 -> 678   delta=50 in 5 s
innerWidth BEFORE  [1279, 719]
Cb.setViewport 854x590  -> applied 854x590 @1x
  t+1s  inner=[854,590]  framesReceived=680
  t+8s  inner=[854,590]  framesReceived=680

innerWidth 1279 → 854 while capture freezes. The resize reaches Blink;
the fault is viz-side. That closes the branch where a renderer that never
painted would have meant RenderWidgetHostView::SetSize was the bug.

It also makes the [diag] line legible: issued=52 acked=51 at the resize,
then issued=0 acked=0 on every tick after — the driver holding the chain open
for an ack the latch guarantees will never come.

Probe B failed BY INSTRUMENT, and that is worth recording

CHROMELESS_CHROME_VMODULE set for display_damage_tracker/display_scheduler
produced zero lines, with known-positive controls non-zero in the same read
(cb_devtools_agent 17, cb_offerer_driver 42). chromium.err.log carries no
line from any --type=gpu-process source at all, and those classes live in the
GPU process. A silent result means "wrong log", not "no damage". Recorded
because the next person will reach for the same knob; the setting is reverted.

Where a fourth attempt starts

Nothing in capture/ ever allocates a LocalSurfaceId — grep returns one
comment. We rely entirely on the async X11 OnBoundsChanged callback, which a
headless window with no WM may never deliver. The finding now carries the
candidate fix and the cheap confirmation that must precede it (log the id
across a resize before building anything).

Also: the gateway cert design was documented but never implemented

stack.yaml had a 20-line comment describing a Secret-mounted cert with a
seed-cert initContainer, and then declared a bare certs emptyDir. deploy.sh
failed outright:

The Deployment "chromeless-standalone-gateway" is invalid:
spec.template.spec.initContainers[0].volumeMounts[1].name: Not found: "tmp"

Worse, it failed after rotating the login password and Ed25519 keypair into
the Secret — an "aborted" run that was not a no-op, and the new password
existed nowhere but the Secret because the line recording it is the script's
last. Same class as #114, through a door that fix did not cover.

Verified with kubectl apply --dry-run=server (which caught two mistakes in
this change: a duplicate securityContext, then a livenessProbe that leaked
onto the initContainer), a parsed shape match against the live gateway, and a
key-set match against main.

Verification

make verify / make lint pass
manifest applies --dry-run=server: all three Deployments "configured"
stack after the fix ice=connected, 128 frames, 22571 audio bytes
C++ in this PR none
release pins touched none — #116 owns them
Answers the resize defect's open question with a mechanism read from the pinned Chromium source, plus one live probe. **No C++** — attempt 3's code is deliberately left unlanded. ## The latch Three attempts adjusted *when* we issue BeginFrames around a resize. The blocker is upstream of that, and no ordering of our own issues can reach it: ``` Display::Resize display.cc:473 -> damage_tracker_->DisplayResized() expecting_root_surface_damage_because_of_resize_ = true display_damage_tracker.cc:82 ``` While set, `DesiredBeginFrameDeadlineMode` returns `kLate` (`display_scheduler.cc:618`) and `all_surfaces_ready` is false (`:597`). The Display never reaches a deadline, never runs the pending external-BeginFrame callback, and **our ack never arrives**. It clears in exactly two places: root-surface damage (`display_damage_tracker.cc:101`), or `SetNewRootSurface` (`:61-66`) — whose only path is `Display::SetLocalSurfaceId` (`display.cc:432`) from `RootCompositorFrameSinkImpl::SubmitCompositorFrame` (`:569`), and only when the submitted `LocalSurfaceId` **differs** from the last activated one. That retro-explains every attempt, including why attempt 2's nudge could never work: `Compositor::ScheduleFullRedraw` damages the browser's own layer tree (`compositor.cc:466`) — neither root-surface damage nor a new `LocalSurfaceId`. ## Probe A: the renderer relays out Fresh worker, live viewer, guest `cr7727-92539396945c` (pre-attempt-3, so a clean control). Measured twice on two separate workers: ``` NEGATIVE CONTROL framesReceived 628 -> 678 delta=50 in 5 s innerWidth BEFORE [1279, 719] Cb.setViewport 854x590 -> applied 854x590 @1x t+1s inner=[854,590] framesReceived=680 t+8s inner=[854,590] framesReceived=680 ``` `innerWidth` 1279 → **854** while capture freezes. The resize reaches Blink; the fault is viz-side. That closes the branch where a renderer that never painted would have meant `RenderWidgetHostView::SetSize` was the bug. It also makes the `[diag]` line legible: `issued=52 acked=51` at the resize, then `issued=0 acked=0` on every tick after — the driver holding the chain open for an ack the latch guarantees will never come. ## Probe B failed BY INSTRUMENT, and that is worth recording `CHROMELESS_CHROME_VMODULE` set for `display_damage_tracker`/`display_scheduler` produced **zero lines**, with known-positive controls non-zero in the same read (`cb_devtools_agent` 17, `cb_offerer_driver` 42). `chromium.err.log` carries no line from any `--type=gpu-process` source at all, and those classes live in the GPU process. A silent result means "wrong log", not "no damage". Recorded because the next person will reach for the same knob; the setting is reverted. ## Where a fourth attempt starts Nothing in `capture/` ever allocates a `LocalSurfaceId` — grep returns one comment. We rely entirely on the async X11 `OnBoundsChanged` callback, which a headless window with no WM may never deliver. The finding now carries the candidate fix **and** the cheap confirmation that must precede it (log the id across a resize before building anything). ## Also: the gateway cert design was documented but never implemented `stack.yaml` had a 20-line comment describing a Secret-mounted cert with a `seed-cert` initContainer, and then declared a bare `certs` emptyDir. `deploy.sh` failed outright: ``` The Deployment "chromeless-standalone-gateway" is invalid: spec.template.spec.initContainers[0].volumeMounts[1].name: Not found: "tmp" ``` Worse, it failed **after** rotating the login password and Ed25519 keypair into the Secret — an "aborted" run that was not a no-op, and the new password existed nowhere but the Secret because the line recording it is the script's last. Same class as #114, through a door that fix did not cover. Verified with `kubectl apply --dry-run=server` (which caught two mistakes in this change: a duplicate `securityContext`, then a `livenessProbe` that leaked onto the initContainer), a parsed shape match against the live gateway, and a key-set match against main. ## Verification | | | | --- | --- | | `make verify` / `make lint` | pass | | manifest applies | `--dry-run=server`: all three Deployments "configured" | | stack after the fix | `ice=connected`, 128 frames, 22571 audio bytes | | C++ in this PR | none | | release pins touched | none — #116 owns them |
Three attempts adjusted WHEN we issue BeginFrames around a resize. The
pinned source says the blocker is upstream of that, and no ordering of
our own issues can reach it.

Display::Resize sets expecting_root_surface_damage_because_of_resize_
(display.cc:473 -> display_damage_tracker.cc:82). While set,
DesiredBeginFrameDeadlineMode returns kLate (display_scheduler.cc:618)
and all_surfaces_ready is false (:597), so the Display never reaches a
deadline, never runs the pending external-BeginFrame callback, and our
ack never arrives. The latch clears only via root-surface damage
(display_damage_tracker.cc:101) or SetNewRootSurface (:61-66) — whose
only path is Display::SetLocalSurfaceId (display.cc:432) from
RootCompositorFrameSinkImpl::SubmitCompositorFrame (:569), and only when
the submitted LocalSurfaceId DIFFERS from the last activated one.

That retro-explains all three attempts, including why attempt 2's
ScheduleFullRedraw could never work: it damages the browser's own layer
tree (compositor.cc:466), which is neither root-surface damage nor a new
LocalSurfaceId.

Probe A, measured twice on two fresh workers: innerWidth goes 1279 ->
854 while framesReceived freezes. The renderer relays out, so the fault
is viz-side — closing the branch where a renderer that never painted
would have meant RenderWidgetHostView::SetSize was the bug.

Probe B was inconclusive BY INSTRUMENT, which is the useful part.
CHROMELESS_CHROME_VMODULE set for display_damage_tracker/display_scheduler
produced ZERO lines while known-positive controls in the same read were
non-zero (cb_devtools_agent 17, cb_offerer_driver 42). chromium.err.log
carries no line from any --type=gpu-process source at all, and those
classes live in the GPU process. A silent result there means "wrong
log", not "no damage". Recorded because the next person will reach for
the same knob; the setting has been reverted.

Rewrote the stale next-steps section: nothing in capture/ ever allocates
a LocalSurfaceId — we rely entirely on the async X11 OnBoundsChanged
callback, which a headless window with no WM may never deliver. The
candidate fix and the cheap confirmation that must precede it are both
written down.

No C++ here. Attempt 3's code is deliberately left unlanded: it was
measured not to fix the defect, and this finding now says why the layer
it targeted was never the blocker.

Also fixed two findings/README rows that contradicted their own files:
audio still read "tried and refuted" when the file says FIXED AND
VERIFIED, and resize said "two attempts" when there are three.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
fix(stack): the gateway cert design was documented but never implemented
All checks were successful
CI / Container smoke test (pull_request) Successful in 1m24s
CI / Docs link check (pull_request) Successful in 48s
E2E / docker-compose + Playwright (pull_request) Successful in 2m23s
CodeQL / Analyze go (pull_request) Has been skipped
CodeQL / Analyze javascript-typescript (pull_request) Has been skipped
Public security / secrets (pull_request) Has been skipped
native-peer-gate / native-peer-gate-scaffold (permissive) (pull_request) Successful in 13s
native-peer-gate / native-peer-gate-strict (M7 gate) (pull_request) Successful in 13s
Public security / ownership (pull_request) Successful in 5s
CI / Lint (pull_request) Successful in 6m57s
0c642bdaba
stack.yaml carried a 20-line comment explaining that the live gateway
mounts its TLS cert from a Secret and copies it into writable /tmp via a
seed-cert initContainer — and then declared a bare `certs` emptyDir. The
prose described the cluster; the YAML fought it.

deploy.sh failed outright on that today:

  The Deployment "chromeless-standalone-gateway" is invalid:
  spec.template.spec.initContainers[0].volumeMounts[1].name:
  Not found: "tmp"

A server-side merge keeps the live initContainer while taking this
file's volume list, so the init container referenced a volume the
manifest had just removed.

Worse than a failed apply: it failed AFTER deploy.sh had already rotated
the login password and the Ed25519 keypair into the Secret, so the
"aborted" run was not a no-op. The new password existed only in the
Secret, because the line that records it to .standalone-creds is the
last in the script and never ran — recovery was to read it back out by
hand. Same class as the image-guard ordering fixed in #114, reached
through a door that fix did not cover.

Now declares what the cluster actually runs: the seed-cert
initContainer, a `tls` Secret volume (chromeless-gateway-tls, mode 0444)
and a 16Mi `tmp` emptyDir.

Verified three ways rather than by reading:
  - `kubectl apply --dry-run=server` reports "configured" for all three
    Deployments. It caught two mistakes in this very change — a
    duplicate securityContext, then a livenessProbe that leaked onto the
    initContainer, which k8s forbids without restartPolicy=Always;
  - the parsed manifest shape (initContainers, volumes, mounts) matches
    the live gateway exactly;
  - the gateway container's key set matches origin/main's, so the edit
    lost nothing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
All checks were successful
CI / Container smoke test (pull_request) Successful in 1m24s
CI / Docs link check (pull_request) Successful in 48s
E2E / docker-compose + Playwright (pull_request) Successful in 2m23s
CodeQL / Analyze go (pull_request) Has been skipped
CodeQL / Analyze javascript-typescript (pull_request) Has been skipped
Public security / secrets (pull_request) Has been skipped
native-peer-gate / native-peer-gate-scaffold (permissive) (pull_request) Successful in 13s
native-peer-gate / native-peer-gate-strict (M7 gate) (pull_request) Successful in 13s
Public security / ownership (pull_request) Successful in 5s
CI / Lint (pull_request) Successful in 6m57s
This pull request can be merged automatically.
This branch is out-of-date with the base branch
You are not authorized to merge this pull request.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin docs/resize-latch-probe-a:docs/resize-latch-probe-a
git switch docs/resize-latch-probe-a

Merge

Merge the changes and update on Forgejo.

Warning: The "Autodetect manual merge" setting is not enabled for this repository, you will have to mark this pull request as manually merged afterwards.

git switch main
git merge --no-ff docs/resize-latch-probe-a
git switch docs/resize-latch-probe-a
git rebase main
git switch main
git merge --ff-only docs/resize-latch-probe-a
git switch docs/resize-latch-probe-a
git rebase main
git switch main
git merge --no-ff docs/resize-latch-probe-a
git switch main
git merge --squash docs/resize-latch-probe-a
git switch main
git merge --ff-only docs/resize-latch-probe-a
git switch main
git merge docs/resize-latch-probe-a
git push origin main
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!117
No description provided.