Experiment 103: Native nested transaction depth control

Date: 2026-04-25

Status: Rejected

Archive:archive/exp-103

Benchmark Run: None

Problem

Experiment 101 accepted cached prepared statements for the top-level

transaction controls:

It intentionally left nested transaction controls alone because their SQL is

depth-dependent:

Experiment 102 tried caching the Dart-side native strings for those savepoint

statements, but the standard benchmark suite had no nested-transaction workload,

so there was no attributable signal.

This follow-up adds a focused nested-transaction benchmark and tests whether

moving depth-based transaction control into C is worth the complexity.

Hypothesis

The savepoint path pays fixed per-boundary costs in Dart:

A C helper can build the SQL into a stack buffer and execute it directly:

 resqlite_tx_depth_control(db, RESQLITE_TX_SAVEPOINT, depth); resqlite_tx_depth_control(db, RESQLITE_TX_RELEASE, depth); resqlite_tx_depth_control(db, RESQLITE_TX_ROLLBACK_TO, depth); 

That should remove Dart allocation/encoding work while preserving the existing

SQLite semantics.

Approach

Two passes were tested.

First pass:

sqlite3_exec(...)

_handleRollback

Second pass:

nested writes

Focused benchmark:

 dart run benchmark/experiments/nested_tx_control.dart \ --repeats=15 \ --cycles=5000 

The benchmark measures four cases:

Results

Validation:

Both passed in an external validation worktree with a fresh native build.

The second-pass snprintf(...) version had some encouraging intermediate

results:

CaseBaselineCandidateResult
empty commit28.196 ms21.930 ms22.2% faster
empty rollback36.974 ms31.082 ms15.9% faster
write commit42.291 ms34.488 ms18.5% faster
write rollback51.191 ms46.075 ms10.0% faster

But larger-cycle and reversed-order runs weakened the signal:

CaseBaselineCandidateResult
empty commit62.713 ms59.227 ms5.6% faster
empty rollback85.830 ms80.592 ms6.1% faster
write commit97.826 ms97.838 msflat
write rollback137.154 ms124.730 ms9.1% faster

The final manual-SQL-builder pass did not improve things:

CaseBaselineCandidateResult
empty commit67.752 ms65.081 ms3.9% faster
empty rollback89.822 ms91.261 ms1.6% slower
write commit108.312 ms109.071 ms0.7% slower
write rollback134.351 ms137.511 ms2.4% slower

Decision

Rejected.

The first measurable signal came from a new focused nested-transaction benchmark,

which was useful, but the optimization itself does not clear the bar.

The best-case result is a small savepoint-only improvement. Once the benchmark

includes realistic nested work, the effect drops into noise or regresses. The

manual SQL builder also made the native code more specialized without producing

a stable win.

The added complexity is not justified:

The implementation is archived at archive/exp-103 in case a future workload

shows deeply nested transactions as a real bottleneck. For now, exp 101 remains

the right boundary: cache the hot top-level transaction controls, leave

depth-dependent savepoints on the simpler existing path.