feat(dev): frame log, recording, and window and state verbs for the screenshot harness
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:
burstwas 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.
| Patch | Adds |
|---|---|
rac/core/dev-switches-buildflag (Core) | dev_switches.gni and //chrome/browser/rac/dev:buildflags |
rac/dev/gate-dev-switches | Wraps 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-record | record 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-windows | windows 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-state | space-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
burstshots. The composite now runs only for widgets that aren't browser windows, which covers the tour. Before and after:05-card-corners-before-fix.pngand06-card-corners-after-fix.png. - Thread safety. The
CopyFromSurfaceresult now goes throughbase::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_ptrdangles untilDeleteSoon. - 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.visibilityStateisvisibleand the page paints.burst 10 16request gaps are 16 ± 6 ms under a load average of about 100.record 500 0.5across Cmd+S produced 29 frames with 0 skipped and an 18 ms median gap.pointer 2 400reveals the hidden sidebar over the page.capture-window onboardingmatches a CDP capture of the tour.
Scorecard (17 pass, 0 fail, 1 unknown of 18)
- Compiles: pass.
- Series valid: pass.
- Gate covers every switch: pass on the dev side. All five new switch names are in the dev
libchrome_dll.dylib. - Frame log: pass. Every line is valid JSON. A static page idle for 21.4 s logs 0 frames.
record: pass. 40 to 43 frames per 700 ms, 0 skipped, 16.6 ms median gap.burstgaps went from 110 ms to 15.7 ms.- Forced contrast: pass. The page sees
prefers-contrast: more, and the UI draws the outlined omnibox. - Forced light/dark: pass. Browser windows switch live, and so does an installed web-app window.
windows,capture-windowandtarget: pass. A targetedcommand 34014landed in the incognito window.- 8b. Onboarding capture: pass.
space-themeandspace-new: pass.open-urls: pass. Tabs open in the existing window.pointerverbs: pass.- Sidebar reveal: reveals after the 100 ms dwell, hides after the grace period. A sub-dwell path never reveals.
- Real cursor: never moved.
- Existing verbs: pass. As on 154,
key escapedoesn't close the omnibox popup (nskey escapedoes).wheelchanged nothing visible. - 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 G6stringscheck on the release framework is the real test. - 156 motion baselines recorded: pass.
- PWA re-check: pass.
- 0 upstream hunks: pass.
profile-windowon 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.mp4and04-record-cmds-hide-4x.mp4/Users/knewton/.t3/worktrees/rac/tools-156/.scratch/proof/review/05-card-corners-before-fix.pngand06-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, withsidebar-reveal-in.mp4,sidebar-reveal-in-4x.mp4,reduced-reveal-in.mp4andtopbar-show.mp4in 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
https://github.com/user-attachments/assets/bb137717-d143-48af-bd03-32941711214d
[!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=PATHwrites 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 aframes.jsonl; PNG encoding moved off the UI thread, cuttingburstframe 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) pluswindows,capture-window,target, andprofile-windowverbs.profile-windowmarks onboarding finished only for a profile it creates. space-theme,space-new,open-urls, andpointer/pointer-pathverbs; the pointer is held via+[NSEvent mouseLocation]so the real cursor never moves.pointer offand new pointer commands cancel a runningpointer-path;space-themevalidates all arguments before applying any.--rac-force-color-mode=light|darkand--rac-force-increased-contrastaffect 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(turnsburst/recordPNGs into an mp4), anddevutils/rac/harness_selftest.pywith its unit tests, which check the fixes above against a running build.
Bug Fixes
dev_names.pyrecords file mtimes onunsuband restores them on files the passes rewrite byte-identical, so builds don't reprocess ~2,700 unchanged string files after every sync.