Experiment 214: Write result direct-read prototype

Date: 2026-07-04T10:09:20Z

Status: Rejected

Direction:stream-rerun-dispatch, measurement-system

Benchmark Run: focused

benchmark/experiments/write_result_direct_read.dart;

see

benchmark/results/2026-07-04T10-09-20Z-exp214-write-result-direct-read.md.

Archive:archive/exp-214

Problem

executeWrite() receives a 16-byte resqlite_write_result struct from native

code on every Database.execute() / Transaction.execute() call. Baseline

code decodes that struct by wrapping the native pointer in a typed list and

then creating a ByteData view:

 final view = ByteData.sublistView( resultBuf.asTypedList(_writeResultSize), ); return WriteResult( view.getInt32(_writeResultOffAffected, Endian.little), view.getInt64(_writeResultOffLastId, Endian.little), ); 

Exp 095 already rejected the larger persistent writer-result-buffer idea:

removing the native calloc / free pair did not produce reliable write-path

wins, and the extra lifetime state was not worth carrying. Exp 211 later

accepted persistent queryBytes() slots only because select_bytes_repeated_calls.dart

gave that reader-side scratch cost a microsecond-precision harness.

This experiment tests a narrower writer-side question that exp 095 did not

isolate: can we remove the per-write Dart view allocation while keeping the

native result buffer lifetime exactly as it is?

Hypothesis

Reading affectedRows and lastInsertId directly from the native pointer

should avoid a small Dart allocation/decode step per write. If that cost is

material, a microsecond writer harness should show same-direction improvement

on the no-op writer floor and simple point-update lanes.

Reject if the deltas do not reproduce across order-flipped passes. The expected

effect is small, so a single candidate-faster lane is not enough.

Approach

The archived prototype changed only the executeWrite() result decode:

 return WriteResult( (resultBuf + _writeResultOffAffected).cast<ffi.Int32>().value, (resultBuf + _writeResultOffLastId).cast<ffi.Int64>().value, ); 

The direct-read shape above records the corrected Dart FFI width for

affected_rows: the C field is a 4-byte int, matching the baseline

ByteData.getInt32() decode.

The native resultBuf allocation and free stayed unchanged, so the prototype

does not add persistent native state and does not revisit exp 095's lifetime

trade-off.

The final branch reverts the runtime change and keeps a focused benchmark:

write_result_direct_read.dart. It repeatedly runs three public writer shapes

and reports median microseconds per Database.execute() call:

Results

Raw numbers are recorded in

benchmark/results/2026-07-04T10-09-20Z-exp214-write-result-direct-read.md.

Negative deltas mean the direct-read candidate was faster.

PairShapeBaseline us/callCandidate us/callDelta
baseline -> candidatenoop update6.7586.842+1.2%
baseline -> candidatepoint update14.82514.286-3.6%
baseline -> candidateparam update14.57814.347-1.6%
candidate -> baselinenoop update6.8859.800+42.3%
candidate -> baselinepoint update14.53116.405+12.9%
candidate -> baselineparam update14.66715.728+7.2%
baseline -> candidate confirmationnoop update6.8796.635-3.5%
baseline -> candidate confirmationpoint update14.57714.539-0.3%
baseline -> candidate confirmationparam update14.43514.639+1.4%

The result does not reproduce. Pair 1 had a small candidate-faster signal on

the two write-shaped lanes but made the no-op floor slower. Pair 2 moved every

lane candidate-slower, including the no-op and parameterized shapes where the

mechanism is identical, which reads as broad run drift or a real regression

rather than a targeted scalar-decode win. Pair 3 returned to mixed/neutral

deltas.

Decision

Rejected. Do not keep the direct pointer-read runtime change.

The prototype removes a small Dart view allocation, but the public writer path

does not show a stable same-direction win. This now closes the narrow

"decode the existing writer result struct differently" idea. It does not

reopen exp 095's persistent native result buffer: that idea adds lifetime state

and was already rejected under focused and release evidence.

The retained benchmark is the useful part. Future writer-result micro-changes

can use write_result_direct_read.dart as a quick floor check, but they still

need same-direction order-flipped deltas before touching runtime code.

Test plan

benchmark/experiments/write_result_direct_read.dart