Experiment 013: FFI isLeaf Annotation

Date: 2026-04-07

Status: Accepted

Commit:af2cfd0

Hypothesis

Adding isLeaf: true to all @ffi.Native bindings will reduce per-call overhead by eliminating safepoint checks and thread state transitions on every Dart-to-C call.

Background

Dart FFI calls without isLeaf perform a full thread state transition on every call:

  1. Transition from Dart mutator to native thread state
  2. Safepoint check (can the GC run?)
  3. Execute the C function
  4. Transition back to Dart mutator state

With isLeaf: true, steps 1-2 and 4 are eliminated. The requirement is that the C function doesn't call back into Dart and doesn't block for extended time. All of resqlite's C functions qualify — they're pure computation against in-process SQLite memory.

Changes

Added isLeaf: true to all 33 @ffi.Native annotations across three files:

Results

Compared against previous run (after-cleanup baseline):

BenchmarkBeforeAfterDelta
Select Maps 100 rows0.32ms0.26ms-19%
Schema Narrow (2 cols)0.17ms0.15ms-12%
Schema Wide (20 cols)1.11ms1.01ms-9%
Parameterized (100 × 500 rows)19.34ms17.77ms-8%
Select Maps 5000 rows2.39ms2.21ms-8%
Batch Insert 10k rows4.83ms4.52ms-6%
Select Bytes 1000 rows0.78ms0.74ms-5%
Schema Text-heavy0.63ms0.60ms-5%

0 regressions. Improvements strongest on small results where FFI overhead is a larger fraction of total time.

Why It Works

resqlite already batches work into relatively few FFI calls (one resqlite_step_row per row instead of ~16 sqlite3_column_* calls). But even with batching, a 1000-row query makes ~1010+ FFI calls (step × rows + column metadata + acquire/release). At ~50ns saved per call, that's ~50μs saved — consistent with the observed 5-8% improvement on 1000-row queries.

The 19% improvement on 100-row queries is larger because the FFI overhead is a bigger fraction of the ~0.3ms total wall time.

Decision

Accepted. Zero complexity cost, zero risk, consistent improvement across all benchmarks.