Experiment 175: Large selectBytes release coverage

Date: 2026-06-16

Status: In Review

Direction:result-transfer-shape, measurement-system

Problem

Exp 174 changed the large selectBytes() transfer policy: instead of copying

native JSON into a Dart Uint8List and sacrificing the reader isolate for

payloads above 256 KB, the reader now sends a transient view over its reusable

native json_buf. That won strongly in the focused harness, but the curated

release metric still tracked the existing 1K-row JSON-bytes result, which is

below the old sacrifice threshold and therefore mostly shows the small-result

copy path.

The release suite already had standard-row selectBytes() sizes, but the

dashboard/default experiment history did not have a named metric whose purpose

was "this exercises the large native bytes transfer path exp 174 changed."

Hypothesis

Add a cheap, named large-payload resqlite selectBytes() row to the existing

release JSON-bytes suite and curate it into the experiment chart. The row should

stay above 256 KB so it guards the old sacrifice boundary and gives future

runners a visible regression signal for large bytes without running the full

focused exp 174 A/B every time.

This is measurement-support work, not a runtime optimization. It is justified

because it guards a recent accepted runtime path and lets future runners reject

or revisit selectBytes transfer-policy changes against a release-facing metric.

Approach

benchmark/suites/select_bytes.dart now appends a Large payload (~650KB)

subsection after the standard 10 / 100 / 1K / 10K row-count tables. The new row:

256 KB;

stop exercising the large-bytes path;

cross-peer 651 KB jsonEncode multiplication inside every release run.

benchmark/shared/workload_registry.dart now curates

Large payload (~650KB) / resqlite selectBytes() as

selectBytes() large bytes in the read chart.

No production runtime code changed.

Results

Release-suite command:

 dart run benchmark/run_release.dart exp175-large-selectbytes-coverage --repeat=1 --no-auto-compare 

Release artifacts:

New large-payload row from the release run:

RowWall medianWall p90Main medianMain p90
Large payload (~650KB) / resqlite selectBytes()0.323 ms0.942 ms0.000 ms0.003 ms

The standard rows still ran in the same pass. Headline existing

selectBytes() medians:

Row countWall medianMain median
10 rows0.018 ms0.000 ms
100 rows0.051 ms0.000 ms
1,000 rows0.361 ms0.000 ms
10,000 rows3.545 ms0.000 ms

The new row's main-isolate median stays effectively zero, which matches the

contract exp 174 was protecting: bytes are produced off-main and copied across

the isolate boundary, not encoded on the main isolate.

Sensitivity — the lane provably responds to the transfer path

A guard is only worth adding if it moves when the thing it guards changes

(exp 148's lesson: callback counts dropped but measured elapsed did not). This

lane's workload — resqlite selectBytes() on a 651 KB result (2,000 × 300 B) —

is the exact shape exp 174's focused harness measured, where the view-send path

beat the old fromList + sacrifice path by −44 % to −47 % (269 µs vs

483 µs; benchmark/experiments/large_bytes_transfer.dart). A parallel attempt

at this same coverage ran the A/B directly on a release lane of the same band

and independently confirmed it: restoring the pre-174 path moved the row

+13 % median / +55 % p90 (the respawn cost concentrates in the tail) while

adjacent standard lanes stayed neutral.

So a regression to the bytes-transfer path lands on this row rather than passing

silently — which is exactly what the existing lanes can't do: the 1K-row lane

is < 256 KB (below the old sacrifice threshold, so it never exercised the path —

exp 174 calls it neutral), and the 10K-row lane is wide enough that SQLite

stepping + JSON-gen dominate per-query wall and bury the transfer-path delta.

This row sits in the sub-millisecond, transfer-bound band where the delta is

visible.

Decision

Accept for review - measurement support.

The experiment adds durable release coverage for the large selectBytes() path

without widening the public API or changing runtime behavior. This closes exp

174's immediate coverage gap: future benchmark histories now have a named

large-bytes metric instead of relying on the existing 1K row, which is too small

to guard the old sacrifice threshold.

Future Notes

changing bytes transfer policy again.

retention, compare any memory-reclaim threshold against this large-payload row

first so reclaim work does not accidentally reintroduce the respawn cost exp

174 removed.

intentionally keep the sacrifice path because Isolate.exit is a true

object-graph handoff there.