Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Hardening

- **Default-config benchmark published (P3)** - `docs/benchmarks/default-config-pk.md` records a
median-of-3 fair-PK run where the SharpCoreDB arm uses a PURE default `DatabaseConfig`
(NoEncryptMode=false, no harness flags). Honest result: the default path engages the Columnar
fixed-width fast paths (verified by counters) but UPDATE/DELETE land at ~84K/~93K ops/s
(~3.5x/~4.2x behind SQLite) versus ~245K/~172K with the tuned harness config — the gap is
dominated by the default `WalDurabilityMode.FullSync` per-commit flush. The default durability
stays FullSync (safe); the follow-up is to optimize the synchronous commit flush itself. New
harness mode: `--pk-default`.

- **Upgrade/downgrade policy documented + format-compat regression tests** - new
`docs/manual/upgrade-and-downgrade.md` records the compatibility matrix: reading legacy
(variable-length, pre-marker) databases with the current version is supported; opening a database
Expand Down
39 changes: 39 additions & 0 deletions docs/benchmarks/default-config-pk.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Fair-PK benchmark with DEFAULT DatabaseConfig (P3 hardening)

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

The SharpCoreDB arm uses a **pure default** `DatabaseConfig` (only the engine type is pinned to
`AppendOnly` — `NoEncryptMode` stays at its default `false`, no page-cache/query-cache/durability
harness flags). The table is the fair-PK schema with `id INTEGER PRIMARY KEY`, so the default
record-layout rules apply: a new PK table becomes **Columnar + fixed-width** and the single-pass
contiguous UPDATE/DELETE paths do engage (verified separately by `DefaultEngineSelectionTests`).

## Results (ops/sec, median of 3)

| Database | INSERT | READ | UPDATE | DELETE |
|---|---:|---:|---:|---:|
| SharpCoreDB (default config) | 111,052 | 69,053 | **84,304** | **92,646** |
| SQLite | 188,247 | 107,122 | 291,414 | 389,120 |
| gap vs SQLite | 1.7x | 1.6x | **3.5x** | **4.2x** |

For comparison, the same schema with the tuned benchmark config (NoEncryptMode + Async WAL + larger
batches) measures UPDATE ~245K ops/s and DELETE ~172K ops/s (gaps ~1.2x / ~2.1x).

## Honest conclusion

The out-of-the-box default is **correct and engages the fast paths, but is not yet at the tuned
throughput**: UPDATE/DELETE land ~3.5-4.2x behind SQLite (vs ~1.2-2.1x tuned). The dominant
difference is the **default `WalDurabilityMode.FullSync`** (per-commit flush) versus the benchmark
arm's `Async`; the per-batch commit cost explains most of the gap. Fast-path counters prove the
contiguous code is running — the throughput is limited by durability flushing, not by resolution.

## Recommendation / follow-up

1. Do **not** silently weaken the durability default (`FullSync` is the safe choice for
production). Instead, optimize the **FullSync commit flush** path (fewer/single flush per
commit, group-commit of the commit markers + overwrites that already batch per page) so a
synchronous commit costs a few ms instead of tens of ms.
2. Re-run this `--pk-default` harness after that change and require the default-config UPDATE/DELETE
gap to move from ~3.5-4.2x toward ~2x before the release-cut.
3. Keep this file updated with the latest median-of-3 numbers.
67 changes: 65 additions & 2 deletions tests/benchmarks/SharpCoreDB.Benchmarks.Comparative/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,20 @@ static async Task Main(string[] args)
return;
}

// Optional: --pk-default → fair PK comparison using PURE default DatabaseConfig
// (NoEncryptMode stays at its default false, no harness-only flags). Proves that the
// out-of-the-box default path engages the fixed-width fast paths vs SQLite.
if (args.Any(a => a.Equals("--pk-default", StringComparison.OrdinalIgnoreCase)))
{
var engineArgDefault = args.FirstOrDefault(a => a.StartsWith("--engine=", StringComparison.OrdinalIgnoreCase));
var engineTypeDefault = engineArgDefault is not null
&& engineArgDefault.Substring("--engine=".Length).Equals("pagebased", StringComparison.OrdinalIgnoreCase)
? SharpCoreDB.Interfaces.StorageEngineType.PageBased
: SharpCoreDB.Interfaces.StorageEngineType.AppendOnly;
RunPkDefaultComparison(engineTypeDefault);
return;
}

// Optional: --engine=appendonly (default) | --engine=pagebased
// PageBased is the v2.0 in-place-update engine (WP10-WP13 storage engine roadmap).
var engineArg = args.FirstOrDefault(a => a.StartsWith("--engine=", StringComparison.OrdinalIgnoreCase));
Expand Down Expand Up @@ -845,7 +859,10 @@ data TEXT
/// <c>ExecuteBatchSQL</c> (single transaction). This exercises the PK B-tree fast paths and the
/// recommended usage; the no-PK harness scenario above under-measures the engine on DML.
/// </summary>
static BenchmarkResult RunSharpCoreDBPk(SharpCoreDB.Interfaces.StorageEngineType engineType, bool fixedWidth = false)
static BenchmarkResult RunSharpCoreDBPk(
SharpCoreDB.Interfaces.StorageEngineType engineType,
bool fixedWidth = false,
bool useDefaultConfig = false)
{
var dbPath = Path.Combine(Path.GetTempPath(), $"bench-sharpcoredb-pk-{Guid.NewGuid()}");
var result = new BenchmarkResult();
Expand All @@ -857,7 +874,9 @@ static BenchmarkResult RunSharpCoreDBPk(SharpCoreDB.Interfaces.StorageEngineType
var sp = services.BuildServiceProvider();

var factory = sp.GetRequiredService<DatabaseFactory>();
var config = BuildConfig(engineType, fixedWidth);
var config = useDefaultConfig
? new DatabaseConfig { StorageEngineType = engineType }
: BuildConfig(engineType, fixedWidth);

using var db = (SharpCoreDB.Database)factory.Create(
dbPath: dbPath,
Expand Down Expand Up @@ -1034,6 +1053,50 @@ static void RunPkComparison(SharpCoreDB.Interfaces.StorageEngineType engineType)
Console.WriteLine($"\nResults saved to: {path}");
}

/// <summary>
/// P3 hardening: fair PK comparison where the SharpCoreDB arm uses a PURE default
/// <see cref="DatabaseConfig"/> (no NoEncryptMode / page-cache / query-cache harness flags) —
/// only the engine type is pinned to the runner's engine. Proves the out-of-the-box default
/// path (Columnar + fixed-width PK tables) engages the fast paths against SQLite.
/// </summary>
static void RunPkDefaultComparison(SharpCoreDB.Interfaces.StorageEngineType engineType)
{
var engineLabel = engineType == SharpCoreDB.Interfaces.StorageEngineType.PageBased ? "PageBased" : "AppendOnly";
Console.WriteLine("╔══════════════════════════════════════════════════════════╗");
Console.WriteLine("║ Fair PK with DEFAULT DatabaseConfig vs SQLite ║");
Console.WriteLine("║ (NoEncryptMode=false; no harness-only performance flags)║");
Console.WriteLine("╚══════════════════════════════════════════════════════════╝");
Console.WriteLine();
Console.WriteLine($"Engine: {engineLabel}");
Console.WriteLine("(median of 3 per phase)");

Console.WriteLine("━━━ SharpCoreDB (SQL, PK, default config = Columnar fixed-width) ━━━");
var scdb = RunPkMedian(() => RunSharpCoreDBPk(engineType, useDefaultConfig: true));
Console.WriteLine();

Console.WriteLine("━━━ SQLite (reference) ━━━");
var sqlite = RunPkMedian(() => RunSQLite());
Console.WriteLine();

Console.WriteLine("║ Database │ INSERT │ READ │ UPDATE │ DELETE ║");
Console.WriteLine($"║ SharpCoreDB │ {scdb.InsertOpsPerSec,10:N0} │ {scdb.ReadOpsPerSec,8:N0} │ {scdb.UpdateOpsPerSec,8:N0} │ {scdb.DeleteOpsPerSec,8:N0} ║");
Console.WriteLine($"║ SQLite │ {sqlite.InsertOpsPerSec,10:N0} │ {sqlite.ReadOpsPerSec,8:N0} │ {sqlite.UpdateOpsPerSec,8:N0} │ {sqlite.DeleteOpsPerSec,8:N0} ║");
Console.WriteLine($"\n UPDATE gap: SQLite vs default-config {sqlite.UpdateOpsPerSec / (double)scdb.UpdateOpsPerSec:F1}x");
Console.WriteLine($" DELETE gap: SQLite vs default-config {sqlite.DeleteOpsPerSec / (double)scdb.DeleteOpsPerSec:F1}x");

var results = new Dictionary<string, BenchmarkResult>
{
["SharpCoreDB (SQL, PK, default config)"] = scdb,
["SQLite"] = sqlite,
};

var dir = "results";
Directory.CreateDirectory(dir);
var path = Path.Combine(dir, $"pk_default_{DateTime.UtcNow:yyyyMMdd_HHmmss}.json");
File.WriteAllText(path, JsonSerializer.Serialize(results, new JsonSerializerOptions { WriteIndented = true }));
Console.WriteLine($"\nResults saved to: {path}");
}

// ══════════════════════════════════════
// LiteDB
// ══════════════════════════════════════
Expand Down
Loading