exp 136: stream completion-side wall audit#111
Conversation
Closes the writer-isolate wall vs SQLite wall split candidate in signals.json#stream-rerun-dispatch.blockedOnMeasurement. Adds three profile-mode counters in the writer isolate (writerHandlerUs, writerSqliteUs, writerHandlerCount), exposes them cross-isolate via Database.snapshotWriterProfileCounters() backed by a new snapshot/reset RPC pair on the writer protocol, and ships a focused audit harness that reports the breakdown on the existing A11c and keyed-PK scenarios. Headline reading: on A11c overlap the writer-isolate handler is only ~22-25% of writer-side burst wall (rest is main-isolate microtask scheduling); within the writer, SQLite step is ~55-58% and Dart dispatch is ~42-45% of writer handler. Structural ceiling for removing all writer-side Dart dispatch on overlap is ~9-11% of total wall — same per-benchmark decision threshold edge as exp 121's invalidation traversal ceiling. Future stream-rerun-dispatch work needs the remaining completion-side scheduling counter before another implementation pass. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- ProfileCounters writerHandlerUs doc references the actual public accessor (Database.snapshotWriterProfileCounters → Writer.snapshotWriterCounters); the original prose pointed at a name that doesn't exist on Writer. - Database.snapshotWriterProfileCounters / resetWriterProfileCounters now run under the writer mutex (writer.locked) so they serialize with execute() / transaction() the same way other public Database write methods do. Without it a snapshot could land between the BEGIN and COMMIT messages of an in-flight transaction and sample partial state. - writer_step_wall_audit.dart clamps dispatch_us at zero. handler_us and sqlite_us come from independent stopwatches, so a marginal handler could in theory round to sqlite_us > handler_us and produce a negative fraction in the rendered markdown. Clamping makes the report robust against that measurement artifact. - test/database_test.dart guards the new writer-counter API surface: one test asserts the snapshot returns the EXP-135 keys, another that reset+snapshot round-trips without throwing. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Closes the completion-side microtask scheduling cost counter entry in signals.json#stream-rerun-dispatch.blockedOnMeasurement. Adds four profile-mode counters on the main isolate (streamCompleteUs, streamCompleteCount, streamEmitUs, streamEmitCount) for the synchronous post-await body of StreamEngine._requery (bookkeeping, hash-changed shortcut, entry.emit, trailing _flushQueue re-entry) and the StreamEntry.emit subscriber for-loop. Both gated behind kProfileMode so release builds tree-shake the recording. Adds benchmark/profile/stream_completion_audit.dart, a focused harness that reuses the audit_workloads.dart A11c / keyed-PK scenarios and reports complete_us / wall, invalidate_us / wall, and the accounted-share fraction alongside the existing exp 135 / 121 counters. Headline reading across 4 passes: on A11c overlap, complete_us is a stable ~29.5-31.7% of writer-burst wall and entry.emit is <0.3% (hash-suppression keeps emissions to ~20-35 per 500-write burst). Combined with exp 121's invalidate_us (~15%), synchronous main-isolate stream-engine code accounts for ~45% of wall; the remaining ~55% lives in framework microtask scheduling and async boundaries the counters cannot reach. A11c disjoint stays at 0% complete (column elision filters at invalidation time before _requery fires). Keyed-PK is ~20-22% complete plus ~14-15% invalidate. Per-handler completion is stable at ~7 µs (A11c overlap) and ~4 µs (keyed-PK). The direction's blockedOnMeasurement list is now empty. Next dispatch implementation experiment must clear a per-handler complete_us target on A11c overlap (~7 µs of ~3,750 handlers per burst) reproducibly across a 5-run release suite. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
CI's `check_experiment_signals` enforces `keyPriors.length <= 6`. With exp 136 added, `stream-rerun-dispatch.keyPriors` and `measurement-system.keyPriors` both hit 7. Curate by moving the oldest entry in each direction (exp 106 for stream-rerun-dispatch; exp 099 for measurement-system) into `archive`. - exp 106's column-level dependency tracking is now context that exps 120 / 121 / 122 build on; the current admission story is better captured by 120 + 122 in keyPriors. - exp 099's "is the path running" lesson is captured in JOURNAL.md and the long-text workload it asked for now lives in exp 110. No behavioral change. Same data; smaller keyPriors slice that the schema requires. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds profile-mode instrumentation and audit harnesses to quantify stream “completion-side” main-isolate wall time (and writer-side handler vs SQLite time) so future stream dispatch experiments can be gated by concrete counters rather than wall-time noise.
Changes:
- Introduces new
ProfileCounterskeys for writer handler/SQLite wall and stream completion/emit wall, plus snapshot/reset plumbing for writer-isolate counters. - Instruments
StreamEngine._requeryandStreamEntry.emitto record completion-side counters (gated behindkProfileMode). - Adds/extends benchmark audit harnesses, experiment writeups, signals metadata, and tests to validate counter snapshot/reset wiring.
Reviewed changes
Copilot reviewed 18 out of 18 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| test/profile_counters_test.dart | Extends snapshot/diff/reset tests to include new writer + stream completion counters. |
| test/database_test.dart | Adds tests ensuring writer counter snapshot/reset APIs round-trip keys without throwing. |
| lib/src/writer/writer.dart | Adds writer-isolate counter snapshot/reset RPC helpers on the main isolate. |
| lib/src/writer/write_worker.dart | Implements writer-isolate snapshot/reset requests and instruments handler + SQLite wall timers. |
| lib/src/stream_engine.dart | Instruments _requery completion path and emit fanout loop for new counters. |
| lib/src/profile_counters.dart | Adds new counter fields/keys and wires them into snapshot/reset. |
| lib/src/database.dart | Exposes writer-isolate counter snapshot/reset APIs at Database level. |
| benchmark/profile/audit_workloads.dart | Resets and merges writer-isolate counters into shared audit scenario results. |
| benchmark/profile/writer_step_wall_audit.dart | New exp-135 harness reporting writer handler vs SQLite wall fractions. |
| benchmark/profile/stream_completion_audit.dart | New exp-136 harness reporting completion/emit wall fractions and derived metrics. |
| benchmark/profile/results/exp-135-writer-step-wall-aggregate.md | Saved aggregate output from exp-135 harness. |
| benchmark/profile/results/exp-136-stream-completion-aggregate.md | Saved aggregate output from exp-136 harness. |
| experiments/135-writer-step-wall-audit.md | New experiment writeup documenting approach/results for exp 135. |
| experiments/136-stream-completion-counter.md | New experiment writeup documenting approach/results for exp 136. |
| experiments/signals.json | Updates direction narrative, clears blockedOnMeasurement, and adds entries for 135/136. |
| experiments/README.md | Adds experiment index entries for 135 and 136. |
| experiments/JOURNAL.md | Adds guidance on cross-isolate counter export via snapshot RPC. |
| docs/experiments/history.json | Regenerates published experiment history to include 135/136. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Fix typo in stream_engine.dart `_requery`: "ocurred" → "occurred" in the post-`await` dirty-re-check comment block. - Guard `Database.snapshotWriterProfileCounters` / `resetWriterProfileCounters` against in-`transaction` calls. The underlying `writer.locked` mutex is non-reentrant, so awaiting it from inside an active transaction body would deadlock the writer pipeline. Both methods now throw `StateError` at the call site if `Transaction.current != null`, matching the explicit surface the Copilot reviewer flagged. Audit harnesses bracket workloads from outside transactions, so existing callers are unaffected. - Add two regression tests confirming the StateError contract. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
Closing this older exp 136 branch as superseded. PR #110 now carries the exp 135 writer-dispatch measurement lane, and PR #113 carries the cleaner current exp 136 completion-side scheduling measurement without the older stacked writer-counter changes. Keeping this open would duplicate the experiment queue rather than add new signal. |
Hypothesis
Exp 135 found the writer-isolate handler is only ~22-25% of A11c overlap burst wall — the remaining ~75% lives on the main isolate. The
completion-side microtask scheduling cost counterentry insignals.json#stream-rerun-dispatch.blockedOnMeasurementis what gates the next dispatch implementation experiment.This measurement experiment ships that counter. The hypothesis: of the ~75% main-isolate share, the synchronous post-
awaitbody ofStreamEngine._requery(bookkeeping, hash-changed shortcut,entry.emit, trailing_flushQueuere-entry) plus theStreamEntry.emitfanout for-loop account for a measurable, structurally bounded share that future dispatch experiments must clear.Approach
streamCompleteUs,streamCompleteCount,streamEmitUs,streamEmitCount. All gated behindkProfileModeso release builds tree-shake the recording.StreamEngine._requery: start the stopwatch right after theselectIfChangedawait resolves; record in the existingfinallyafter_flushQueuere-enters. The cross-isolate round-trip never inflates the wall.StreamEntry.emit: wrap the subscriber for-loop only.streamEmitUsis a strict subset ofstreamCompleteUs.benchmark/profile/stream_completion_audit.dartreuses theaudit_workloads.dartA11c / keyed-PK scenarios exp 119 / 121 / 135 share. Reports raw counters and derived fractions includingaccounted / wall = (invalidate_us + complete_us) / wall_us.test/profile_counters_test.dartextended to round-trip the new keys throughsnapshot/reset.Full writeup: experiments/136-stream-completion-counter.md.
Results
Four repeated passes (a/b/c/d) — d is the saved aggregate at benchmark/profile/results/exp-136-stream-completion-aggregate.md:
Sanity:
dispatcher_parked_total = 0,dispatcher_wake_retry_total = 0,dispatcher_max_parked_concurrent = 0on every workload — exp 120 / 122 still hold.Headline reading:
complete_us~30% of wall (very tight band across 4 passes),emit_us<0.3% (hash-suppression keeps emissions to ~20-35 per 500-write burst). Combined with exp 121'sinvalidate_us(~15%), synchronous main-isolate stream-engine code accounts for ~45% of wall. The remaining ~55% is framework microtask scheduling, async/await chaining, reader-pool internals, and subscriber callback dispatch the counters cannot reach.complete_us = 0(column elision filters every re-query at invalidation time, so_requerynever fires).Outcome
Accept for review — measurement. Closes the last
blockedOnMeasuremententry instream-rerun-dispatchdirection. The next dispatch implementation experiment must clear a per-handlercomplete_ustarget on A11c overlap (~7 µs of ~3,750 handlers per burst) reproducibly across a 5-run release suite. Three speculative follow-ups are now evaluable:_flushQueuere-entry collapse, hash-suppressed completion fast-path, per-completion batching — none committed.Test plan
dart analyze --fatal-infos lib/ benchmark/profile/ test/profile_counters_test.dartdart test test/profile_counters_test.dart test/stream_test.dart test/stream_invalidation_coalescing_test.dart test/stream_dependency_shapes_test.dart test/stream_cache_hit_reliability_test.dart test/stream_overflow_fallback_test.dart test/stream_trigger_cascade_test.dart— all passdart run benchmark/generate_history.dart— parsed 115 experiments, 118 benchmark runsdart run -DRESQLITE_PROFILE=true benchmark/profile/stream_completion_audit.dart --markdown) with stable bands per the table above🤖 Generated with Claude Code