Skip to content

Commit 88ebf4e

Browse files
author
MPCoreDeveloper
committed
bench(pk-default): add diagnostic config variants and correct the P3 analysis
Single-knob isolation (SHARPCOREDB_PK_DEFAULT_VARIANT=async|bufferedio|novalidate| noadaptive|hsinsert|plain|tuned) shows the earlier FullSync-dominance claim was wrong: WalDurabilityMode is only honored by GroupCommitWAL (off by default) and the async variant measures the same as default. NoEncryptMode moves default UPDATE ~1.3x (plain ~145K vs tuned ~109K median) and machine drift spans 275-315K on SQLite itself across the same window. docs/benchmarks/default-config-pk.md corrected: no blind 'FullSync commit-flush optimization'; the next step is a same-window interleaved A/B harness mode so default-vs-tuned deltas are attributable before any code change.
1 parent f1289ab commit 88ebf4e

3 files changed

Lines changed: 74 additions & 23 deletions

File tree

docs/CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Hardening
1111

12+
- **Default-config benchmark follow-up: FullSync hypothesis falsified (P3b)** - single-knob
13+
isolation via `SHARPCOREDB_PK_DEFAULT_VARIANT` (`async`, `bufferedio`, `novalidate`,
14+
`noadaptive`, `hsinsert`, `plain`, `tuned`) disproved the earlier “FullSync dominates the
15+
default gap” claim: the durability mode is only honored by GroupCommitWAL (off by default) and
16+
the `async` variant measured the same. `NoEncryptMode` moves default UPDATE ~1.3x (144K vs
17+
109K), machine drift spans ~275-315K on SQLite itself. `docs/benchmarks/default-config-pk.md`
18+
is corrected accordingly; the next step is a same-window interleaved A/B mode before any
19+
further optimization is implemented.
20+
1221
- **Default-config benchmark published (P3)** - `docs/benchmarks/default-config-pk.md` records a
1322
median-of-3 fair-PK run where the SharpCoreDB arm uses a PURE default `DatabaseConfig`
1423
(NoEncryptMode=false, no harness flags). Honest result: the default path engages the Columnar
Lines changed: 30 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Fair-PK benchmark with DEFAULT DatabaseConfig (P3 hardening)
22

33
Run: `dotnet run --project tests/benchmarks/SharpCoreDB.Benchmarks.Comparative -- -c Release -- --pk-default`
4-
Date: 2026-09-04 · Machine: local dev box · median of 3 runs per phase.
4+
Date: 2026-09-04 · Machine: local dev box · median of 3 runs per phase unless noted.
55

66
The SharpCoreDB arm uses a **pure default** `DatabaseConfig` (only the engine type is pinned to
77
`AppendOnly``NoEncryptMode` stays at its default `false`, no page-cache/query-cache/durability
@@ -17,23 +17,35 @@ contiguous UPDATE/DELETE paths do engage (verified separately by `DefaultEngineS
1717
| SQLite | 188,247 | 107,122 | 291,414 | 389,120 |
1818
| gap vs SQLite | 1.7x | 1.6x | **3.5x** | **4.2x** |
1919

20-
For comparison, the same schema with the tuned benchmark config (NoEncryptMode + Async WAL + larger
21-
batches) measures UPDATE ~245K ops/s and DELETE ~172K ops/s (gaps ~1.2x / ~2.1x).
20+
For comparison, the tuned benchmark config measured earlier in the day (UPDATE ~245K ops/s and
21+
DELETE ~172K ops/s, gaps ~1.2x / ~2.1x).
22+
23+
## Knob isolation (diagnostic single-knob variants, single-shot and median)
24+
25+
The harness supports `SHARPCOREDB_PK_DEFAULT_VARIANT=<name>` for the default arm:
26+
`async`, `bufferedio`, `novalidate`, `noadaptive`, `hsinsert`, `plain` (= full tuned knob set with
27+
`NoEncryptMode=true`), `tuned` (= full tuned knob set, `NoEncryptMode=false`).
28+
29+
Observed (2026-09-04 afternoon):
30+
31+
| variant | UPDATE ops/s | notes |
32+
|---|---:|---|
33+
| pure default | ~84K (median) | reference |
34+
| `async` (WalDurabilityMode) | ~74K (single) | **disproves the FullSync hypothesis** — the mode is only honored by GroupCommitWAL (default off) |
35+
| `bufferedio` / `novalidate` | ~74-76K (single) | within noise |
36+
| `tuned` (no NoEncryptMode) | ~109K (median) | knob set alone is not the gap |
37+
| `plain` (= tuned + NoEncryptMode=true) | ~145K (median) | `NoEncryptMode` moves UPDATE ~1.3x; still below the morning's ~212-245K |
38+
39+
Machine drift is significant: SQLite's own UPDATE varied 275K-315K across these runs. Single-knob
40+
deltas below ~1.3x are not reliably attributable outside a same-window interleaved A/B.
2241

2342
## Honest conclusion
2443

25-
The out-of-the-box default is **correct and engages the fast paths, but is not yet at the tuned
26-
throughput**: UPDATE/DELETE land ~3.5-4.2x behind SQLite (vs ~1.2-2.1x tuned). The dominant
27-
difference is the **default `WalDurabilityMode.FullSync`** (per-commit flush) versus the benchmark
28-
arm's `Async`; the per-batch commit cost explains most of the gap. Fast-path counters prove the
29-
contiguous code is running — the throughput is limited by durability flushing, not by resolution.
30-
31-
## Recommendation / follow-up
32-
33-
1. Do **not** silently weaken the durability default (`FullSync` is the safe choice for
34-
production). Instead, optimize the **FullSync commit flush** path (fewer/single flush per
35-
commit, group-commit of the commit markers + overwrites that already batch per page) so a
36-
synchronous commit costs a few ms instead of tens of ms.
37-
2. Re-run this `--pk-default` harness after that change and require the default-config UPDATE/DELETE
38-
gap to move from ~3.5-4.2x toward ~2x before the release-cut.
39-
3. Keep this file updated with the latest median-of-3 numbers.
44+
1. The earlier claim that the default `WalDurabilityMode.FullSync` dominates the gap is **wrong**
45+
(disproven by the `async` variant). Do **not** implement a “FullSync commit-flush optimization”
46+
based on it.
47+
2. The default path is correct and engages the fast paths; part of the remaining gap correlates
48+
with `NoEncryptMode` (record/at-rest toggles and file-format decisions), part is machine drift.
49+
3. Next step: add a **same-window interleaved A/B** mode to this harness (arms round-robin within
50+
one process) so default-vs-tuned deltas are attributable, then re-open the optimization only on
51+
a measured knob.

tests/benchmarks/SharpCoreDB.Benchmarks.Comparative/Program.cs

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -361,11 +361,11 @@ static void RunInsertMicroBenchmark()
361361
Console.WriteLine($" SQL/Direct overhead: {(sqlMedian / directMedian):F2}x");
362362
}
363363

364-
static DatabaseConfig BuildConfig(SharpCoreDB.Interfaces.StorageEngineType engineType, bool fixedWidth = false)
364+
static DatabaseConfig BuildConfig(SharpCoreDB.Interfaces.StorageEngineType engineType, bool fixedWidth = false, bool noEncrypt = true)
365365
{
366366
return new DatabaseConfig
367367
{
368-
NoEncryptMode = true,
368+
NoEncryptMode = noEncrypt,
369369
StorageEngineType = engineType,
370370
// The fair PK comparison intentionally isolates the record-layout variable: the legacy
371371
// arm opts out of the AutoFixedWidthRecords default so it measures true variable-length
@@ -874,9 +874,33 @@ static BenchmarkResult RunSharpCoreDBPk(
874874
var sp = services.BuildServiceProvider();
875875

876876
var factory = sp.GetRequiredService<DatabaseFactory>();
877-
var config = useDefaultConfig
878-
? new DatabaseConfig { StorageEngineType = engineType }
879-
: BuildConfig(engineType, fixedWidth);
877+
DatabaseConfig config;
878+
if (useDefaultConfig)
879+
{
880+
var variant = Environment.GetEnvironmentVariable("SHARPCOREDB_PK_DEFAULT_VARIANT")?.ToLowerInvariant();
881+
config = variant switch
882+
{
883+
"async" => new DatabaseConfig { StorageEngineType = engineType, WalDurabilityMode = SharpCoreDB.Services.DurabilityMode.Async },
884+
"bufferedio" => new DatabaseConfig { StorageEngineType = engineType, UseBufferedIO = true },
885+
"novalidate" => new DatabaseConfig
886+
{
887+
StorageEngineType = engineType,
888+
SqlValidationMode = SharpCoreDB.Services.SqlQueryValidator.ValidationMode.Disabled,
889+
StrictParameterValidation = false,
890+
},
891+
"noadaptive" => new DatabaseConfig { StorageEngineType = engineType, EnableAdaptiveWalBatching = false },
892+
"hsinsert" => new DatabaseConfig { StorageEngineType = engineType, HighSpeedInsertMode = true },
893+
// "plain" == the tuned harness config with NoEncryptMode=true (BuildConfig default);
894+
// "tuned" == the same knob set but NoEncryptMode=false (isolates that flag).
895+
"plain" => BuildConfig(engineType, fixedWidth: true),
896+
"tuned" => BuildConfig(engineType, fixedWidth: true, noEncrypt: false),
897+
_ => new DatabaseConfig { StorageEngineType = engineType },
898+
};
899+
}
900+
else
901+
{
902+
config = BuildConfig(engineType, fixedWidth);
903+
}
880904

881905
using var db = (SharpCoreDB.Database)factory.Create(
882906
dbPath: dbPath,
@@ -979,6 +1003,12 @@ data TEXT
9791003
/// </summary>
9801004
private static BenchmarkResult RunPkMedian(Func<BenchmarkResult> arm, int reps = 3)
9811005
{
1006+
// Allow quick single-shot profiling via SHARPCOREDB_BENCH_REPS (diagnostic only).
1007+
if (int.TryParse(Environment.GetEnvironmentVariable("SHARPCOREDB_BENCH_REPS"), out int envReps) && envReps > 0)
1008+
{
1009+
reps = envReps;
1010+
}
1011+
9821012
var runs = new List<BenchmarkResult>(reps);
9831013
for (int r = 0; r < reps; r++)
9841014
{

0 commit comments

Comments
 (0)