feat(dev): frame log, recording, and window and state verbs for the screenshot harness

NO
2h ago
+3117-0

Stacked PR. This branch is based on the sync branch sync/helium-0.20.1.1 (Chromium 156) and targets it. Retarget it to main once the sync merges. It also carries Ship's fix(devutils): keep string files' mtimes… commit (PR #35); that commit drops out of the diff once #35 merges.

Problem

Phase A of the UI overhaul showed that rac's dev screenshot harness couldn't measure motion or reach most surfaces:

  • burst was only accurate for its first frame. The PNG encode ran on the UI thread (~110 ms at 2400x1472), so it stalled the compositor it was measuring.
  • Nothing could drive the edge reveal. It reads the global cursor, and the harness can't move it.
  • The harness could only drive and capture the first window. App, incognito, second-profile, PiP and onboarding windows were out of reach.
  • Light mode and Increase Contrast couldn't be forced on a dark Mac.
  • Nothing kept the harness's switches out of release builds.

Change

Everything sits behind Core's BUILDFLAG(RAC_DEV_SWITCHES) (rac_dev_switches = !is_official_build). New files are listed only inside if (rac_dev_switches) in chrome/browser/ui/views/rac/BUILD.gn. The existing harness body in window_screenshot.mm compiles down to two empty hooks in official builds.

PatchAdds
rac/core/dev-switches-buildflag (Core)dev_switches.gni and //chrome/browser/rac/dev:buildflags
rac/dev/gate-dev-switchesWraps every existing switch (rac-window-screenshot, -delay, -command, -requests, -activate, rac-launch-in-background) and every existing verb in the flag
rac/dev/frame-log--rac-frame-log=PATH: one JSON line per presented frame (frame), the sidebar and page bounds each commit put on screen (layout), each harness request (req), and a clock pairing. Writes happen off the UI thread. Also adds rac::dev::LogAnimationStep() for rac's own motion code, which is a no-op in release.
rac/dev/window-screenshot-recordrecord DIR MS [SCALE]: copies every frame to fNNNN.png plus frames.jsonl, with a 512 MB in-flight cap. Every PNG encode, including burst and single shots, moves to the thread pool.
rac/dev/forced-appearance--rac-force-color-mode=light|dark, --rac-force-increased-contrast, and the verbs appearance light|dark|system and contrast on|off. These affect this process only.
rac/dev/window-screenshot-windowswindows OUT.txt, capture-window SEL OUT.png, target SEL (later input verbs act on that window), and profile-window NAME, which creates profiles only under --user-data-dir and refuses folders that aren't profiles. Selectors: #N, first, normal, app[:ID], incognito, profile:NAME, pip, onboarding, name:WIDGET.
rac/dev/window-screenshot-statespace-theme PRESET|- [auto|light|dark], space-new, open-urls URL… (through -[NSApp.delegate application:openURLs:], the Launch Services path), pointer X Y, pointer-path X0 Y0 X1 Y1 MS and pointer off. The pointer verbs hold +[NSEvent mouseLocation] and post a native mouse-moved event, so the real cursor never moves.

Other harness behavior in window-screenshot-state:

  • Captures of non-browser windows, such as the welcome tour, paint each WebView's surface in.
  • Any --rac-window-screenshot* switch keeps occluded windows painting.
  • A run that sends requests stays in the background unless it passes -activate.
  • Unknown requests are logged instead of silently captured.

Also added: devutils/rac/motion_trace.py, which reads a frame log, and devutils/rac/frames_to_video.swift, which turns burst/record PNGs into an .mp4.

Net upstream hunks (D17): 0. All 46 hunks are in rac-owned files: chrome/browser/rac/dev/*, chrome/browser/ui/views/rac/BUILD.gn, window_screenshot.mm and the new views/rac/dev/* files. A draft had moved the occlusion switch into ChromeBrowserMainParts::PreMainMessageLoopRun, which would have added 3 upstream hunks (two in chrome_browser_main.cc, one in chrome/browser/BUILD.gn). The final review moved it back into the harness's own pre-show hook, and the runtime check below shows that hook is early enough.

Fixes from the final review

  • Capture overpaint (regression in the last draft). Painting every WebView's surface over browser-window captures had three effects: it squared off the content card's rounded corners, it would have covered views drawn over the page (such as the revealed sidebar), and it mixed a later frame into burst shots. The composite now runs only for widgets that aren't browser windows, which covers the tour. Before and after: 05-card-corners-before-fix.png and 06-card-corners-after-fix.png.
  • Thread safety. The CopyFromSurface result now goes through base::BindPostTaskToCurrentDefault, because content doesn't promise it arrives on the calling sequence. The widget, WebContents and bitmap are only touched on the UI thread, through WeakPtrs. If the window closes mid-capture, the shot is written without the composite. If the WebContents goes away, it is skipped.
  • Recorder. The recorder drops its widget pointer when the window is destroyed, so no raw_ptr dangles until DeleteSoon.
  • Occlusion hook. Moved back into rac-owned code (see above).
  • Patch hygiene. Two patches had been refreshed with the wrong quilt prefix (src.orig/). They are re-refreshed with the repo's quilt args and clang-formatted.

How it was verified

Builds went through devutils/rac/build-queue.sh he build on the 156 clone. The final round settled at about 520 planned steps and ran 4. Runs went through the queue's run slot with worktree.sh run and --rac-launch-in-background, against the worktree's own profile. he validate series passes.

Final review runtime check, without --disable-backgrounding-occluded-windows:

  • document.visibilityState is visible and the page paints.
  • burst 10 16 request gaps are 16 ± 6 ms under a load average of about 100.
  • record 500 0.5 across Cmd+S produced 29 frames with 0 skipped and an 18 ms median gap.
  • pointer 2 400 reveals the hidden sidebar over the page.
  • capture-window onboarding matches a CDP capture of the tour.

Scorecard (17 pass, 0 fail, 1 unknown of 18)

  1. Compiles: pass.
  2. Series valid: pass.
  3. Gate covers every switch: pass on the dev side. All five new switch names are in the dev libchrome_dll.dylib.
  4. Frame log: pass. Every line is valid JSON. A static page idle for 21.4 s logs 0 frames.
  5. record: pass. 40 to 43 frames per 700 ms, 0 skipped, 16.6 ms median gap. burst gaps went from 110 ms to 15.7 ms.
  6. Forced contrast: pass. The page sees prefers-contrast: more, and the UI draws the outlined omnibox.
  7. Forced light/dark: pass. Browser windows switch live, and so does an installed web-app window.
  8. windows, capture-window and target: pass. A targeted command 34014 landed in the incognito window.
    • 8b. Onboarding capture: pass.
  9. space-theme and space-new: pass.
  10. open-urls: pass. Tabs open in the existing window.
  11. pointer verbs: pass.
    • Sidebar reveal: reveals after the 100 ms dwell, hides after the grace period. A sub-dwell path never reveals.
    • Real cursor: never moved.
  12. Existing verbs: pass. As on 154, key escape doesn't close the omnibox popup (nskey escape does). wheel changed nothing visible.
  13. Switches compile out with is_official_build=true: unknown. This is by gn and code reading only, since no release binary was built. Ship's G6 strings check on the release framework is the real test.
  14. 156 motion baselines recorded: pass.
  15. PWA re-check: pass.
  16. 0 upstream hunks: pass.
  17. profile-window on a fresh profile opens a normal window: pass.

Proof (worktree .scratch/proof/, not committed)

Final review build:

  • /Users/knewton/.t3/worktrees/rac/tools-156/.scratch/proof/review/01-browser-window-no-occlusion-flag.png
  • /Users/knewton/.t3/worktrees/rac/tools-156/.scratch/proof/review/02-pointer-edge-reveal.png
  • /Users/knewton/.t3/worktrees/rac/tools-156/.scratch/proof/review/03-capture-window-onboarding.png
  • /Users/knewton/.t3/worktrees/rac/tools-156/.scratch/proof/review/04-record-cmds-hide.mp4 and 04-record-cmds-hide-4x.mp4
  • /Users/knewton/.t3/worktrees/rac/tools-156/.scratch/proof/review/05-card-corners-before-fix.png and 06-card-corners-after-fix.png

Earlier rounds:

  • /Users/knewton/.t3/worktrees/rac/tools-156/.scratch/proof/after/forced/01-forced-light-contrast-media.png
  • /Users/knewton/.t3/worktrees/rac/tools-156/.scratch/proof/after/verbs/19-incognito-after-targeted-newtab.png
  • /Users/knewton/.t3/worktrees/rac/tools-156/.scratch/proof/after/round2/07-profile-window.png
  • /Users/knewton/.t3/worktrees/rac/tools-156/.scratch/proof/pwa-156/side-by-side-app-and-normal.png
  • /Users/knewton/.t3/worktrees/rac/tools-156/.scratch/proof/after/idle/frames.jsonl
  • Baselines on 156: /Users/knewton/.t3/worktrees/rac/tools-156/.scratch/proof/baseline-156/baseline-156.md, with sidebar-reveal-in.mp4, sidebar-reveal-in-4x.mp4, reduced-reveal-in.mp4 and topbar-show.mp4 in the same folder.

Written by Claude Sonnet 5.5 and Devin SWE-2 (drafts), with final review and fixes by Claude Opus 5.5 in Claude Code.

Merge order

Stacked on the sync branch sync/helium-0.20.1.1 (Chromium 156). Nothing under patches/ merges before the sync (D62). After the sync merges, retarget this PR to main before deleting the sync branch. It carries Core's rac/core/dev-switches-buildflag.patch commit (same content as Core's dbaa394) and Ship's dev_names fix (PR #35) unchanged, so each lands once.

🤖 Generated with Claude Code

Onboarding window captured by the harness on 156

Edge reveal driven by the new pointer verb

Browser window paints with no occlusion flag

Card corners squared by the round-2c composite (fixed)

Card corners after the fix

https://github.com/user-attachments/assets/bb137717-d143-48af-bd03-32941711214d

Installed web app next to a normal window, dark and forced light (G8)


Devin Review


[!NOTE]
Medium Risk
Large dev-only surface area including Obj-C runtime overrides for pointer and contrast, but gated from official builds via RAC_DEV_SWITCHES; main product risk is accidental exposure if the build flag is misconfigured.

Overview
Adds BUILDFLAG(RAC_DEV_SWITCHES) (rac_dev_switches = !is_official_build) so the screenshot harness and new --rac-* switches compile out of official builds; window_screenshot.mm becomes no-op hooks in release.

Behind that flag, the harness gains frame logging (--rac-frame-log), record DIR MS [SCALE] with frames.jsonl, stale-frame cleanup, skipped frames on write failure, and PNG encode on the thread pool (including burst). New request verbs cover window targeting (windows, target, profile-window, capture-window), appearance (CLI + appearance / contrast), and state (space-theme, pointer / pointer-path / pointer off, open-urls). Pointer tests hold +[NSEvent mouseLocation] without moving the real cursor; profile-window only auto-completes onboarding for profiles it creates.

Capture behavior changes: non-browser windows composite WebView surfaces (browser windows skip this to avoid corner/regression issues), occluded windows keep painting when any screenshot switch is set, request-driven runs stay background unless -activate, and unknown requests log errors instead of defaulting to PNG capture.

New devutils/rac/ tools: motion_trace.py, frames_to_video.swift, harness_selftest.py (+ unit tests). patches/series lists seven new rac dev patches.

Reviewed by Cursor Bugbot for commit 3d0cc55cca043c0d4bea3cc30c7edc3e260df27f. Bugbot is set up for automated code reviews on this repo. Configure here.


Summary by cubic

Expands the dev screenshot harness so it can measure motion, reach any window type, and drive state the old harness couldn't touch. Everything sits behind BUILDFLAG(RAC_DEV_SWITCHES) (rac_dev_switches = !is_official_build), so release binaries carry none of it and the old harness body compiles to two empty hooks.

  • --rac-frame-log=PATH writes one JSON line per presented frame plus the sidebar and page bounds, off the UI thread.
  • record DIR MS [SCALE] copies every frame to PNGs with a frames.jsonl; PNG encoding moved off the UI thread, cutting burst frame gaps from ~110 ms to ~16 ms. It starts only once the folder exists, stops with an error if the folder can't be made, clears earlier recordings, and counts frames whose PNG write fails as skipped.
  • Window selectors (#N, first, normal, app[:ID], incognito, profile:NAME, pip, onboarding, name:WIDGET) plus windows, capture-window, target, and profile-window verbs. profile-window marks onboarding finished only for a profile it creates.
  • space-theme, space-new, open-urls, and pointer/pointer-path verbs; the pointer is held via +[NSEvent mouseLocation] so the real cursor never moves. pointer off and new pointer commands cancel a running pointer-path; space-theme validates all arguments before applying any.
  • --rac-force-color-mode=light|dark and --rac-force-increased-contrast affect this process only.
  • Non-browser-window captures (such as the welcome tour) paint each WebView's surface in; any screenshot run keeps occluded windows painting.
  • Adds devutils/rac/motion_trace.py (reads a frame log), devutils/rac/frames_to_video.swift (turns burst/record PNGs into an mp4), and devutils/rac/harness_selftest.py with its unit tests, which check the fixes above against a running build.

Bug Fixes

  • dev_names.py records file mtimes on unsub and restores them on files the passes rewrite byte-identical, so builds don't reprocess ~2,700 unchanged string files after every sync.