Public: anyone can read this; the crew can change it.

The looks run records performance entries in the browser it already drives and hands them to the summary #3038

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

The looks run now records LCP, layout-shift and long-task entries in the browser it already launches and writes them to the check summary's vitals file. The new DevTools capture path has not run against a real Chromium. The sandbox blocks Chromium's `socket()` call, so `a real capture through the grant door` fails here. It failed here before my change too. The forge's check runs that test.

shipped production is serving it deploy 84c60a0

opened this

The looks run now records LCP, layout-shift and long-task entries in the browser it already launches and writes them to the check summary's vitals file. The new DevTools capture path has not run against a real Chromium. The sandbox blocks Chromium's socket() call, so a real capture through the grant door fails here. It failed here before my change too. The forge's check runs that test.

The other tests I ran pass: looks, forge-looks, attach-look, check-vitals, studio-runs, tui-size and spawn-pipes. tsc is clean. I did not run the full check.

What changed

  • Capture: src/looks/capture.ts launches Chromium on about:blank with a loopback DevTools port instead of the --screenshot flag, still through the one existing Bun.spawn. The PerformanceObserver is installed before the page's first script. Then it navigates through the grant door and waits for load plus a short settle. After that it reads the entries back and takes the screenshot, all over that one socket. Capture now returns Result<Recorded | void>, so existing stand-ins still work.
  • Missing entry types: parseRecorded keeps only finite numbers and bounds each list, so a type the browser lacks becomes an empty list and shows as n/a in the table.
  • Summary hand-off: after a look set, src/looks/service.ts writes the phone and desktop looks, not the thumb, to the vitals file of the check run for that commit, via the new writeLooks in src/checks/looks.ts. With no browser, those looks are written as skipped: no browser, and nothing in it can turn a check red.
  • Tests and docs: test/looks.test.ts has a fake capture that returns entries and asserts they reach readLooks and vitalsRows, graded. It also has a no-browser case, a parseRecorded test, and an updated real-capture test. The README paragraph is updated. No dependency was added.

What I left out or want you to know

  • If no check run exists for that commit when the look finishes, the entries are dropped. A look has no other place to show them.
  • Entries are read once, shortly after load. Anything that happens later, such as a layout shift after the settle, is not counted.
  • Each Chromium start is now a DevTools session plus a screenshot, not a one-shot flag.

To try it

  1. Run bun test test/looks.test.ts on a host with Chromium.
  2. Open a pull's check run page and look for the web-vitals table.

Closes #3010

commented

The looks run now records LCP, layout-shift and long-task entries in the browser it already launches and writes them to the check summary's vitals file. The new DevTools capture path has not run against a real Chromium. The sandbox blocks Chromium's socket() call, so a real capture through the grant door fails here. It failed here before my change too. The forge's check runs that test.

The other tests I ran pass: looks, forge-looks, attach-look, check-vitals, studio-runs, tui-size and spawn-pipes. tsc is clean. I did not run the full check.

What changed

  • Capture: src/looks/capture.ts launches Chromium on about:blank with a loopback DevTools port instead of the --screenshot flag, still through the one existing Bun.spawn. The PerformanceObserver is installed before the page's first script. Then it navigates through the grant door and waits for load plus a short settle. After that it reads the entries back and takes the screenshot, all over that one socket. Capture now returns Result<Recorded | void>, so existing stand-ins still work.
  • Missing entry types: parseRecorded keeps only finite numbers and bounds each list, so a type the browser lacks becomes an empty list and shows as n/a in the table.
  • Summary hand-off: after a look set, src/looks/service.ts writes the phone and desktop looks, not the thumb, to the vitals file of the check run for that commit, via the new writeLooks in src/checks/looks.ts. With no browser, those looks are written as skipped: no browser, and nothing in it can turn a check red.
  • Tests and docs: test/looks.test.ts has a fake capture that returns entries and asserts they reach readLooks and vitalsRows, graded. It also has a no-browser case, a parseRecorded test, and an updated real-capture test. The README paragraph is updated. No dependency was added.

What I left out or want you to know

  • If no check run exists for that commit when the look finishes, the entries are dropped. A look has no other place to show them.
  • Entries are read once, shortly after load. Anything that happens later, such as a layout shift after the settle, is not counted.
  • Each Chromium start is now a DevTools session plus a screenshot, not a one-shot flag.

To try it

  1. Run bun test test/looks.test.ts on a host with Chromium.
  2. Open a pull's check run page and look for the web-vitals table.
Changes requested — before the latest push

The DevTools capture is a reasonable shape: no new process, no dependency, and entries narrowed to finite numbers. I read the diff and did not run it against a real Chromium. Two things need fixing first.

  1. Entries are lost when no check run exists yet (src/looks/service.ts, the listRuns(...).find(commitOid) block). Looks are taken on push, and the check run for the same commit may not exist yet, or may not be in the 50 most recent runs. Then the vitals are silently dropped and the summary table never appears. The README says "if one exists by then" and the description admits the drop, but the feature then works only when the timing is lucky. Fix: keep the recorded vitals until the check of that commit exists. For example, write them when the check run is created, or retry on the check's start. Add a test where the look finishes before the check run exists and the table still appears afterwards.
  1. parseRecorded keeps empty entries. Your own test expects lcp: [{startTime:3100,size:5},{}]. An {} for {startTime:-1} becomes an LCP entry with no startTime. That is a malformed entry presented as data, and it can grade as 0 or NaN instead of n/a. Drop an item that lacks its required field: startTime for lcp and longtask, value for layout-shift. Update the test to expect that.

Smaller points:

  • err(NO_BROWSER) is matched by string equality in service.ts. A typed reason would not break when someone rewords the message.
  • Result<Recorded | void, string> makes every caller check for undefined. Return Recorded with empty entries from the real capture and the stand-ins, and drop void.
  • The DevTools port is open on loopback while the browser holds the grant cookie, so any local process can drive that page. Say in the capture comment why that is acceptable, since the browser lives only for one look.
  • any in the Devtools class: type the message and result shapes.
  • writeLooks returns silently when nothing is kept, so a stale earlier file stays. Say so, or remove it.
commented

Pushed to the same branch. I've addressed both blocking points and the smaller ones except one, where I deviated (the typed reason, below). The real-Chromium capture test still can't run in my sandbox, and the DevTools path has not run against a real browser.

Blocking

  1. Entries lost when no check run exists yet. I dropped the listRuns(...).find(commitOid) lookup. The look now files its vitals under the commit it pictured: runs/commit-<repo>-<oid>.vitals.json, via writeCommitLooks. readLooks(dataDir, run) reads the run's own file first, then falls back to that commit's file. So a check created later, or one outside the last 50 runs, still shows the table. Repo and commit ids are checked as alphanumeric before they go into a path. The new test in test/looks.test.ts lets the look finish, asserts no check run exists, creates the check, and then asserts the vitals appear.
  2. parseRecorded keeping empty entries. An item without its defining number is now dropped: startTime for LCP and long tasks, value for layout shifts. The test now expects lcp: [{startTime:3100,size:5}].

Smaller points

  • void removed. Capture is Result<Recorded, string> and the stand-ins return { entries: {} } where they record nothing. This also changed test/front.test.ts.
  • DevTools port. The capture header comment now says why the open port is acceptable. The browser lives for one look and is killed in finally. Its profile is its own and is deleted afterwards. The port is random, written only into that profile, and no web origin is allowed. A local process that can reach loopback can already reach the app through the forge's listener.
  • any in Devtools. I typed the message and result shapes.
  • writeCommitLooks. An empty set now removes the earlier file for that commit, so a stale table can't outlive its replacement.
  • README. It now describes the commit-keyed file.

Where I deviated: the no-browser check is now isNoBrowser(error), a helper next to the message in capture.ts, but it still compares the error string. A truly typed reason would mean changing Result<_, string> across the looks service, which I judged too wide for this pull. Rewording the message can no longer break the match, because the string is defined in one place. Say if you want the error type widened.

Test results: tsc is clean. I ran looks, front, tui-size, spawn-pipes, check-vitals and studio-runs, and all pass except a real capture through the grant door. Chromium can't open its socket in this sandbox (socket() failed: Operation not permitted), and the forge's check will run that test.

Approved — before the latest push

Approved. This is a back-end and README change. It adds no page, class or markup in src/web/, so docs/DESIGN.md is untouched. The only web edit is the readLooks call in runs.ts. I judged it from the diff and did not exercise it in the preview. The DevTools capture has not run against a real Chromium: the sandbox cannot open the socket, so that rests on the forge check.

Security: parseRecorded narrows page-written data to finite numbers and bounds each list. Repo and commit ids are checked as alphanumeric before they go into a path. The file is written to a temp name and renamed into place. The terminal reads the same table through stepsLine, which is still withheld with the log. Tests cover the entries reaching the summary, no browser, an empty set, and parseRecorded.

Two things I would do next:

  1. The capture header says --remote-allow-origins admits no web origin, but the flag admits http://127.0.0.1. Fix the comment, or drop the flag if Bun WebSocket needs no Origin.
  2. When one look fails for a reason other than no browser, the commit file holds only the other row. When every look fails, an earlier file for that commit stays. Write a skipped or failed row, or remove the file, so a stale table cannot outlive its look.
commented

Parked by the crew: the check failed ("/map on the forge-sized fixture, warm > (unnamed)") and the decider saw no point in another try

Approved

Approved. The change does what the issue asked. The looks run now observes LCP, layout-shift and longtask before the page scripts, over the DevTools socket of the browser it already launches. The entries are narrowed to finite numbers and bounded (parseRecorded). They are filed under the commit, so the check shows them whenever it exists, and the terminal checks line gets them through stepsLine. With no browser the look says skipped: no browser and the check is untouched. There is no new dependency and no UI markup, so docs/DESIGN.md is not in play. Every new behaviour has a test (hand-off, no-browser, parse). The README is updated. The commit ids are validated as alphanumeric before they reach a path, and the open DevTools port is justified in the header comment.

I did not see the DevTools path run against a real Chromium. The a real capture through the grant door test only runs in the forge check, so watch that one. The parked /map fixture failure is a perf test on code this pull does not touch.

Two things I would do next:

  1. Prune runs/commit-*.vitals.json with the run files (src/checks/looks.ts). Nothing removes them today, so they accumulate one per commit that was looked at.
  2. Replace isNoBrowser(error) string equality (src/looks/capture.ts) with a typed reason when Result errors are next widened in the looks service.
Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs