Skip to content

Attribute event-loop stalls to the work that blocked them - #1437

Merged
SawyerHood merged 2 commits into
mainfrom
bb/event-loop-stall-attribution
Aug 13, 2026
Merged

Attribute event-loop stalls to the work that blocked them#1437
SawyerHood merged 2 commits into
mainfrom
bb/event-loop-stall-attribution

Conversation

@SawyerHood

@SawyerHood SawyerHood commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator

Why

The packaged server logs at info. Slow-query instrumentation used debug, so 82,590 production log lines covering 2,609 event-loop stall windows had zero slow-query lines. The stall report also named no unit of work, so a stall could not be attributed to a route, sweep, or plugin handler.

This is layer 1 of the event-loop stall stack. Later layers (pragmas, timeline, outline cache) need these lines so their effect can be measured rather than argued.

What

  • Log Slow DB query at info (threshold stays 100 ms).
  • Track a labeled work stack on the server event loop.
  • Include currentWork, lastWork, and lastWorkMs on each Event loop stalled line.
  • Label HTTP /api/v1 and /internal requests, periodic sweeps, daemon websocket messages, timeline/outline builds, and plugin handler invocations.

currentWork is sampled after the stall: the interval cannot run while the loop is blocked, so the in-flight label is the unit still on the stack. lastWork is the unit that just finished.

Test plan

  • @bb/db typecheck + tests
  • @bb/server stall-monitor, periodic-sweep, timeline, and daemon-message tests

Prerequisite: none (targets main).

AGENT GENERATED: by Grok 4.6

@SawyerHood

Copy link
Copy Markdown
Collaborator Author

🚨 SLOP COP 🚨 · review

I am SlopCop, and I am reviewing this pull request now. I will check security, code quality, performance, architecture, and practical behavior.

),
resolutionMs: DEFAULT_EVENT_LOOP_STALL_MONITOR_RESOLUTION_MS,
thresholdMs: DEFAULT_EVENT_LOOP_STALL_LOG_THRESHOLD_MS,
...getEventLoopWorkSnapshot(),

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚨 slopcop/review — High: keep the blocking work linked to the maximum delay

histogram.max covers the full five-second window, but this snapshot reads only the later work state. A worker reproduced a 650 ms block followed by a short heartbeat. The report named the heartbeat and showed lastWorkMs: 0. Record time-stamped candidates for the sample window, or log slow synchronous work directly. Please test a real block with later work before the report.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed on 3bf1a86. The stall tick now snapshots slowestWork from every unit in the histogram window before it resets that window. A later heartbeat still shows as lastWork; the long block remains slowestWork. Added a test that records a 650 ms sweep then a 1 ms heartbeat.

}

function leaveEventLoopWork(): void {
const frame = workStack.pop();

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚨 slopcop/review — High: remove the correct frame when concurrent work ends

runEventLoopWork() remains active across await, but this pop always removes the newest global frame. Request A can finish before request B and remove B's frame. A direct probe then reported B as complete and A as active. Track each frame by identity, and store parent links for each asynchronous task. Please add a test where sibling tasks finish out of order.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed on 3bf1a86. Frames are now keyed by id and stored in AsyncLocalStorage, so one request cannot pop another request's frame. Added a test where sibling requests finish out of order.

@SawyerHood SawyerHood left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚨 SLOP COP 🚨 · review

ELI5: This change gives each server job a name tag. When the server freezes, the log should show the responsible job. Two timing errors can show the wrong job instead.

I found two high-confidence correctness problems. I added inline comments for both.

  • The global stack breaks during normal request overlap. One request can remove another request's frame and reverse their labels.
  • The five-second maximum delay does not remain linked to its cause. Later quick work can replace the recorded label.

I found no medium-or-higher security issue. SQL logs omit bound values, limit SQL text, and use structured fields.

The architecture scan found related gaps. Manual labels omit plugin background services and startup recovery. The host daemon has a similar monitor, but server and daemon policy should stay separate. An identity-based frame registry and time-stamped samples would reduce incorrect labels and manual gaps. Some test variables still use old debug-log names, but that issue has no runtime effect.

Validation passed:

  • All @bb/server and @bb/db tests passed. The server suite ran 1,461 tests.
  • Both package type checks passed.
  • The app loaded in a browser without page errors. The projects API returned HTTP 200.
  • A direct concurrency probe reproduced reversed labels.
  • A real-stall probe reproduced the unrelated heartbeat label.
  • All required GitHub checks passed.

The checks do not cover the two core timing failures. I recommend fixing both inline findings before the team relies on these logs.

Promote slow-query logs to info so packaged servers record them,
and label stall reports with the in-flight route, sweep, or plugin
handler so later pragma and query changes can be measured.
Concurrent requests kept a shared LIFO stack, so one finish could
pop another request's frame. Keep frames by id in AsyncLocalStorage
and report the slowest unit in the histogram window so a later
heartbeat cannot hide the block.
@SawyerHood
SawyerHood force-pushed the bb/event-loop-stall-attribution branch from 86a3efc to 3bf1a86 Compare August 13, 2026 01:11
@SawyerHood
SawyerHood merged commit 433fdcc into main Aug 13, 2026
10 checks passed
@SawyerHood
SawyerHood deleted the bb/event-loop-stall-attribution branch August 13, 2026 01:17
SawyerHood added a commit that referenced this pull request Aug 13, 2026
## Why

`better-sqlite3` is synchronous and every query runs on the server event
loop. The live database is ~1.9 GB with a 2 MiB page cache, no mmap, and
`synchronous = FULL` (fsync on every commit). Cold page reads and
per-commit fsync are the main source of sub-second stalls, not a missing
index.

This is layer 2 of the event-loop stall stack. It depends on layer 1
(#1437) so the new pragmas can be confirmed from stall and slow-query
logs.

## What

`createConnection` now sets:

| pragma | value | reason |
| --- | --- | --- |
| `cache_size` | `-262144` (256 MiB) | replace the 2 MiB default in
front of a multi-GB file |
| `synchronous` | `NORMAL` | WAL-standard; no fsync per commit. Power
loss can drop the last transactions; it cannot corrupt the file |
| `mmap_size` | `1073741824` (1 GiB) | avoid a `pread` + copy on every
page |
| `busy_timeout` | `5000` | wait for a lock instead of failing
immediately |
| `temp_store` | `MEMORY` | keep temp sorts off disk |

`wal_autocheckpoint` is left unchanged. A 38 MB WAL is a
reader-starvation problem, not an autocheckpoint setting problem.

## Test plan

- [x] File-backed pragma test asserts the live values
- [x] `@bb/db` typecheck + full test suite
- [x] Server database-maintenance sweep tests (busy_timeout
save/restore)

Prerequisite: #1437.

> AGENT GENERATED: by Grok 4.6
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant