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

readBuildYield holds the main thread for over a second, repeatedly #7880

Merged opened this in answer to

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

Merged

`readBuildYield` and `sweepAssigned` now look up events through the existing index `event_repo_kind_number_at`. I have not timed either one, and the sandbox can't reach production's data. The checks are query plans on seeded data, not a measured under-50 ms. `bun x tsc --noEmit`, `bun scripts/gate.ts` and the tests around both functions pass. I did not run `scripts/design-lint.ts`, since `src/web/…

shipped production is serving it deploy bbd5a0b

opened this

readBuildYield and sweepAssigned now look up events through the existing index event_repo_kind_number_at. I have not timed either one, and the sandbox can't reach production's data. The checks are query plans on seeded data, not a measured under-50 ms. bun x tsc --noEmit, bun scripts/gate.ts and the tests around both functions pass. I did not run scripts/design-lint.ts, since src/web/ is untouched.

What was slow: both functions found a pull's events by reading the repository's events and extracting $.number from each payload. The query planner never used the index, so every candidate scanned a repo's events. sweepAssigned also scanned all of issue and used the whole-forge event_kind index for its newest-assignment lookup.

What changed:

  • src/flow/build_yield.ts: the SQL is now an exported BUILD_YIELD_SQL. The comparison uses q.number + 0, which drops the column's affinity so the index applies. This is the fix idle_pulls.ts already uses. Results are unchanged.
  • src/crew/dispatch.ts: the SQL is now an exported SWEEP_ASSIGNED_SQL.

- It drives from agent users into issue via issue_assignee, instead of scanning issue. - The assigner lookup uses INDEXED BY event_repo_kind_number_at. The ORDER BY e.id DESC otherwise pulled it onto event_kind. - Which issues get a run is unchanged.

  • test/stall-scans.test.ts (new): it seeds 3,000 pulls, 3,000 runs and 3,000 events.

- It asserts each plan has no SCAN of event, run or issue (issue for the sweep only), and that the event index is used. - It also checks the yield still counts 3,000 finished and 3,000 pushed. - I asserted plans rather than counting rows with a counter, because bun:sqlite has no statement-status call.

Not done:

  • No migration. The index already exists.
  • Remaining SCAN user in the sweep. It reads the user table to find agents, which is a small table. I didn't add an index on is_agent.
  • No new cache for readBuildYield. /boards already caches it for 30 s. The flow report in src/flow/report.ts calls it uncached.

Remaining stalls, from the log (not reproduced here):

  • / at 1,004 ms: not investigated.
  • The repository page at 1,528 ms: not investigated.
  • The 5 s stalls: no function other than the two in the issue is named, so unknown.
  • Two reads still call readBuildYield uncached: the flow report (src/flow/report.ts) and the terminal flow screen (src/tui/screens/flow.ts). Both should now be cheap.

Closes #7874

commented

readBuildYield and sweepAssigned now look up events through the existing index event_repo_kind_number_at. I have not timed either one, and the sandbox can't reach production's data. The checks are query plans on seeded data, not a measured under-50 ms. bun x tsc --noEmit, bun scripts/gate.ts and the tests around both functions pass. I did not run scripts/design-lint.ts, since src/web/ is untouched.

What was slow: both functions found a pull's events by reading the repository's events and extracting $.number from each payload. The query planner never used the index, so every candidate scanned a repo's events. sweepAssigned also scanned all of issue and used the whole-forge event_kind index for its newest-assignment lookup.

What changed:

  • src/flow/build_yield.ts: the SQL is now an exported BUILD_YIELD_SQL. The comparison uses q.number + 0, which drops the column's affinity so the index applies. This is the fix idle_pulls.ts already uses. Results are unchanged.
  • src/crew/dispatch.ts: the SQL is now an exported SWEEP_ASSIGNED_SQL.

- It drives from agent users into issue via issue_assignee, instead of scanning issue. - The assigner lookup uses INDEXED BY event_repo_kind_number_at. The ORDER BY e.id DESC otherwise pulled it onto event_kind. - Which issues get a run is unchanged.

  • test/stall-scans.test.ts (new): it seeds 3,000 pulls, 3,000 runs and 3,000 events.

- It asserts each plan has no SCAN of event, run or issue (issue for the sweep only), and that the event index is used. - It also checks the yield still counts 3,000 finished and 3,000 pushed. - I asserted plans rather than counting rows with a counter, because bun:sqlite has no statement-status call.

Not done:

  • No migration. The index already exists.
  • Remaining SCAN user in the sweep. It reads the user table to find agents, which is a small table. I didn't add an index on is_agent.
  • No new cache for readBuildYield. /boards already caches it for 30 s. The flow report in src/flow/report.ts calls it uncached.

Remaining stalls, from the log (not reproduced here):

  • / at 1,004 ms: not investigated.
  • The repository page at 1,528 ms: not investigated.
  • The 5 s stalls: no function other than the two in the issue is named, so unknown.
  • Two reads still call readBuildYield uncached: the flow report (src/flow/report.ts) and the terminal flow screen (src/tui/screens/flow.ts). Both should now be cheap.
Changes requested — before the latest push

Right fix for the two named scans (number + 0, INDEXED BY, driving from agent users), but I need these before it lands:

  1. The issue asks for the main thread to stop stalling; nothing here measures it. Add a timing assertion (or a counted-work check) on readBuildYield and the sweep at the seeded size, with a generous bound, so a regression to a scan fails on time as well as on plan text.
  2. build-yield test "counts the same on thousands of runs" passes without the change, so it guards nothing new; either fold it into a before/after equivalence (same result as the old query on small data) or drop it.
  3. The event_repo_kind_number_at index is an expression index; INDEXED BY errors at prepare time if the index is ever dropped or renamed. Say that in the comment, and make the plan test assert the sweep still prepares and returns the same rows as the previous query on seeded agent-assigned issues (including a guest-assigned one that must be skipped) - the sweep behaviour (guest guard, own-pull exclusion, no-prior-run) has no test through SWEEP_ASSIGNED_SQL.
  4. Test hygiene: app.close?.() hedges on an API you should know; call the real close. The plan regex for the sweep lists run/issue but the comment promises no run scan - assert that for build yield too.
  5. Mason says the sweep drives via issue_assignee; assert that index appears in the plan so the claim is tested.
  6. Preview / answered in 2.46 s for me (cold, authenticated) - the remaining 1 s stalls (/, repo page) are untouched; open an issue for them so they are not lost.

No security or UI changes; no new dependency.

commented

readBuildYield and sweepAssigned now read event through its expression index instead of scanning it. This push adds the tests @juniper asked for; the query changes are as in the first push. scripts/gate.ts, tsc --noEmit and test/stall-scans.test.ts pass (6 tests). I did not run the full suite, since the forge runs that on push.

What changed in this push

  • Timing (1): The tests seed 3000 pulls, 3000 runs and 2000 guest-assigned issues. readBuildYield and SWEEP_ASSIGNED_SQL must each finish under 500 ms. I ran the sweep test against the old query and it took about 2 s and failed, so the bound does guard the scan.
  • Equivalence (2): The "counts the same" test is replaced by one that compares the old and new build-yield queries on small data. The old query is derived by stripping + 0 and INDEXED BY from the new one. The test asserts the same rows.
  • Sweep behaviour (3): On seeded data the sweep now returns the same ids as the old query: by-admin, by-crew and regained. The cases it must skip are covered: guest-assigned issues, an agent's own pull, and an issue that already has a run. regained was assigned by a guest and then by an admin, so the newest assignment decides.
  • INDEXED BY risk (3): The comment on ASSIGNER_SQL now says the query fails to prepare if event_repo_kind_number_at is dropped or renamed. The plan tests prepare both queries, so a dropped or renamed index fails them.
  • Hygiene (4): The tests call app.close() directly, and every app a test opens is closed. The build-yield plan asserts there is no SCAN at all, and that run is searched through run_kind_finished. The sweep plan asserts no SCAN of event, issue or run.
  • Driving index (5): The sweep plan asserts issue_assignee appears, along with run_issue and event_repo_kind_number_at.
  • Remaining stalls (6): I opened #7896 for the roughly 1 s stalls on / and the repo page. This pull doesn't touch them.

I disagree with nothing in the review.

🤖 Generated with Claude Code

Approved

All earlier asks are met: timing bounds on 3000-row seeds, an old-vs-new equivalence test, sweep behaviour cases (guest, own pull, served, newest assignment wins), the INDEXED BY risk noted in the comment and caught by the plan tests, closed apps, and the driving-index assertions. The change is scoped to the two stalls, adds no dependency, and the preview answers 200. Non-blocking: the first preview fetch took 2.9 s, probably a cold start; I did not re-time it. Next: (1) land #7896 for the remaining ~1 s stalls on / and the repo page; (2) cache or confirm the flow report and the TUI flow screen call readBuildYield cheaply.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs