Skip to content

test(benchmark): flat-state base point-read benchmark (RocksDB vs sorted-arena vs LMDB) - #12684

Closed
LukaszRozmej wants to merge 1 commit into
masterfrom
perf/flat-base-pointread-benchmark
Closed

test(benchmark): flat-state base point-read benchmark (RocksDB vs sorted-arena vs LMDB)#12684
LukaszRozmej wants to merge 1 commit into
masterfrom
perf/flat-base-pointread-benchmark

Conversation

@LukaszRozmej

Copy link
Copy Markdown
Member

Changes

Phase 0 gate for evaluating an mmap-based (MDBX-like) base tier for flat state: a standalone micro-benchmark comparing cold/warm random point reads across three backends, before committing to any production backend work.

  • FlatBaseBenchmarkDatasetBuilder: deterministic synthetic dataset (keccak(counter) keys, realistic slim-RLP account values, 52-byte split-key slots) built once and reused via marker files. Scale via NETH_FLATBENCH_SCALE: smoke (100k accounts / 500k slots, local-friendly) or full (300M / 1.2B, ≥100 GB, for a Linux box). Root dir via NETH_FLATBENCH_DIR.
  • FlatBasePointReadBenchmark: BenchmarkDotNet, backend × {hit, guaranteed-miss} × threads {1, 8, 32}, account (20B) and slot (52B) reads, zero per-read allocations.
  • Backends (benchmark-scoped only, zero production-code changes):
    • RocksDB with the production DbConfig Flat* option strings and 300/700 MiB HyperClockCaches mirroring FlatRocksDbConfigAdjuster — the honest baseline;
    • ShardedSortedTableArena: 256-shard prototype of the in-repo SortedTable format over production ArenaFile mmap (the candidate base-tier design);
    • LMDB via LightningDB (added to CPM, referenced only by Nethermind.Benchmark) — the reth-configuration control.
  • README documents the full-scale build, the Linux cold-read procedure (drop_caches per pass), the metrics to record, and the go/no-go thresholds: arena ≥1.8× RocksDB on cold hits at ≥100 GB AND ≥0.85× LMDB; miss cost ≤1.2× LMDB.
  • Every dataset build and benchmark setup runs a byte-for-byte validation pass (1000 sampled keys per backend + guaranteed-miss checks).

Indicative warm smoke numbers on Windows (mean ns/read, account hits, 1 thread): RocksDB ~1700, arena ~420, LMDB ~230; at 32 threads: 224 / 43 / 49. The decision numbers require the documented cold full-scale Linux run.

Types of changes

What types of changes does your code introduce?

  • Bugfix (a non-breaking change that fixes an issue)
  • New feature (a non-breaking change that adds functionality)
  • Breaking change (a change that causes existing functionality not to work as expected)
  • Optimization
  • Refactoring
  • Documentation update
  • Build-related changes
  • Other: Benchmark infrastructure (no shipping-code changes)

Testing

Requires testing

  • Yes
  • No

If yes, did you write tests?

  • Yes
  • No

Notes on testing

  • Smoke scale ran end-to-end locally: all 36 benchmark cases completed; built-in correctness validation (byte-for-byte compare + miss checks) passed for all three backends at build time and in every benchmark setup.
  • Benchmarks.slnx release build: 0 warnings.
  • Note: the 7 Sorted/* files are verbatim copies of internal Nethermind.State.Flat sorted-table machinery (header-noted) — copied rather than widening production visibility with InternalsVisibleTo. If reviewers prefer InternalsVisibleTo, happy to switch.

Documentation

Requires documentation update

  • Yes
  • No

Requires explanation in Release Notes

  • Yes
  • No

Remarks

Part of the BAL benchmark performance workstream (#12681, #12682, #12683). Next step: run the full scale cold procedure on a Linux benchmark host; the thresholds above decide whether the sorted-arena base tier (or an LMDB/libmdbx backend behind the same IPersistence seam) proceeds to a production prototype.

🤖 Generated with Claude Code

…ena vs LMDB)

Phase-0 go/no-go gate for the MDBX-like flat persistence workstream: a
standalone micro-benchmark comparing cold/warm uniform-random point reads
(account 20B keys, storage-slot 52B split keys; hits and guaranteed misses;
1/8/32 threads) across three backends holding byte-identical synthetic data:

- production-tuned RocksDB flat columns (default DbConfig Flat* options plus
  the 300/700 MiB Account/Storage HyperClockCaches mirroring
  FlatRocksDbConfigAdjuster);
- a sharded SortedTable arena prototype (256 prefix shards in one mmap'd
  ArenaFile, in-memory shard directory) reusing the persisted-snapshot
  sorted-table format via verbatim copies of the internal Sorted/* files
  (no InternalsVisibleTo for benchmarks);
- LMDB via the LightningDB package (benchmark-only dependency).

The deterministic dataset builder (NETH_FLATBENCH_SCALE=smoke|full,
NETH_FLATBENCH_DIR) bulk-loads all three backends in ascending key order,
is reused across runs via a parameter marker, and every benchmark setup
re-validates sampled keys byte-for-byte on the selected backend.
State/FlatBase/README.md documents the full-scale procedure, cold-run
methodology (drop_caches), metrics to record, and the go/no-go thresholds.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@core-repository-dispatch-app core-repository-dispatch-app Bot added build changes dependencies Pull requests that update a dependency file test labels Aug 3, 2026
@claude

claude Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Claude finished @LukaszRozmej's task in 10m 50s —— View job


Review: flat-base point-read benchmark

Reviewed the full diff against origin/master. This is well-built benchmark infrastructure — genuinely zero production-code changes, LightningDB correctly scoped to Nethermind.Benchmark (which is not in Nethermind.slnx, so it stays out of the main build), and the built-in byte-for-byte validation on every dataset build and every benchmark setup is exactly the right instinct for a harness whose output will drive an architectural decision.

I verified the parts most likely to be silently wrong, and they hold up:

  • Shard/key consistencyShardOf is key[0] at 256 shards, and storage keys are [4B addrHash | …], so a slot lands in the same shard as its account on both the write and read paths. Writing shards 0..255 in order with keys sorted within keeps the arena globally ascending, which is what LMDB's MDB_APPEND bulk load requires. ✅
  • Miss keys are genuinely absent — account misses use counters ≥ AccountCount; slot misses use slotIndex ≥ SlotsPerAccount on live accounts, and since WriteSlotHash XORs the index into the SplitMix64 seed, distinct indices give distinct hashes. ✅
  • Miss return valuesIKeyValueStore.Get(key, output) returns 0 (not -1) on a missing key, so the length != 0 miss checks and the sum accumulator are correct for the RocksDB path. ✅
  • GetSpan sizingIByteBufferWriter.Copy chunks at 256 bytes and BlockBuilder.Finish only ever requests ≤ 256, so the 1 MiB FileByteBufferWriter is never asked for more than it can return, even for the ~1.2 MB storage index block at full scale. ✅
  • ArenaFile lifecyclePersistOnShutdown() is required to stop CleanUp deleting the dataset, and it's called. ✅
  • Sorted/* copies — diffed all 7 against the originals; no behavioural divergence. ✅
  • Cold-run key freshness_cursor advancing monotonically means the measured invocation reads keys the jitting invocation never touched, so the documented --invocationCount 1 cold procedure really is cold. Nice detail. ✅

The findings below are about the benchmark's methodology rather than its correctness — which is the thing that matters most here, since the numbers feed a go/no-go gate.


High

1. The Threads = 32 row does not run 32 concurrent readers. (inline) Parallel.For(0, Threads, …) range-partitions 32 indices across ThreadPool workers, so concurrency is capped at the live pool thread count (ProcessorCount, growing at ~1 thread/sec under starvation). Each worker only does 256 reads ≈ 25 ms, so the injector never fires — and under the single-shot cold procedure the pool has not ramped at all. Since one of the explicit gates is arena ≥ 0.85× LMDB and LMDB's headline property is reader scaling, a silently-throttled concurrency level can flip the decision. It also means the "zero per-read allocations" claim only holds for Threads = 1. Dedicated Threads + a Barrier fixes it.

Medium

2. Warm mode measures a 32 K hot key set, not random access. (inline) _cursor wraps the 32768-entry pool every 4 invocations; BDN's pilot picks ~150 invocations/iteration. At full scale that working set (~130 MB) fits in both the page cache and the 300 MiB Account block cache, so the warm row — the source of the indicative numbers in the PR description — compresses exactly the differences the benchmark exists to expose. Raising PoolSize is cheap.

3. A marker mismatch silently rm -rfs a multi-hour, ~400 GB dataset. (inline) EnsureBuilt deletes unconditionally from inside [GlobalSetup], with no resume and no warning — an interrupted full build or a NETH_FLATBENCH_SCALE typo costs hours. Worth gating behind an explicit NETH_FLATBENCH_REBUILD=1.

4. The 7 copied Sorted/* files can't be kept in sync mechanically. (inline) Answering the question you raised in the description: I'd suggest a third option over both copying and InternalsVisibleTolinked <Compile Include="..\Nethermind.State.Flat\PersistedSnapshots\Sorted\*.cs" Link="…" />. Production visibility is unchanged, drift becomes impossible, and ~700 lines leave the diff. If copies stay, note they already diverge (the <remarks> wire-format docs are stripped from Block.cs/SortedTable.cs/SortedTableBuilder.cs), so diff can no longer detect real drift — a sync-assertion test would be needed.

Low

5. The Setup() pool sanity check only probes the account pool, not the slot pool. (inline)
6. The account ArenaFile leaks if the storage arena fails to open (same shape in LmdbFlatBackend.Open). (inline)
7. Worth documenting how faithful the RocksDB baseline is — TestHardwareInfo's 10 MB is harmless (that branch is State-only) and unlimited max_open_files matches Linux-server production, but the latter pins all index/filter blocks outside the 1 GiB budget the README quotes for RSS. Also, the "" additionalConfig mirror is only correct while FlatDbConfig.Layout defaults to Flat. (inline)
8. README: state that the gate measures a freshly-bulk-loaded/compacted best case for both backends and that compaction cost is out of Phase-0 scope; total full disk is ~400 GB, not ~300 GB. (inline)


Only #1 seems worth blocking on, since it can change the answer the benchmark is meant to produce; #2#4 are cheap and should land before the full run rather than after. Nothing here is a correctness bug in the data path.

Note on verification: dotnet build was not permitted in this environment, so I could not compile. I did check the API surface statically — ColumnsDb<T> ctor, IColumnsDb<T>.CreateSnapshot(), HyperClockCacheWrapper, AdjustedRocksdbConfig, PruningConfig, TestHardwareInfo, ArenaFile, IByteBufferWriter — and all resolve. IRocksDbConfigFactory.GetForDatabase declares string? columnName while CacheInjectingConfigFactory declares string, which is fine because nullable is not enabled in this project (and null falls through the switch correctly at runtime). I also could not verify that LightningDB 0.22.0 resolves from the configured feeds.

· branch perf/flat-base-pointread-benchmark

Comment on lines +144 to +150
int perThread = TotalReads / Threads;
long[] sums = _threadSums;
Parallel.For(0, Threads, t =>
{
using IFlatReadSession session = _backend.BeginSession();
sums[t] = ReadRange(session, start + t * perThread, perThread, slots);
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

High — the Threads = 32 row does not actually run 32 concurrent readers

Parallel.For(0, Threads, …) schedules onto the ThreadPool with a range partitioner: for 32 indices it hands each pool worker a contiguous chunk that the worker runs sequentially. So the achieved concurrency is bounded by the number of live pool threads, which starts at ProcessorCount and only grows via the starvation injector at roughly 1 thread per 0.5–1 s.

Each worker here does TotalReads / Threads = 256 reads. Even at a pessimistic 100 µs cold latency that's ~25 ms of work per worker — the injector never fires. On a 16-core host the Threads = 32 row measures ~16-way concurrency; under the README's single-shot cold procedure (--warmupCount 0 --iterationCount 1 --invocationCount 1) the pool has had no chance to ramp at all, so it is worse.

This matters because concurrent read scaling is exactly the property the go/no-go compares (arena ≥ 0.85× LMDB) — LMDB's headline characteristic is lock-free MVCC reader scaling, and silently capping every backend at ProcessorCount can flip the decision.

Two secondary effects of the same call:

  • [MemoryDiagnoser] will attribute the Parallel.For range partitioner / task / closure allocations to the benchmark, so the "zero per-read allocations" property only holds for the Threads = 1 row.
  • The workers do not start together, so there is no steady-state 32-way phase to measure even if the pool did ramp.

Suggested fix: create Threads dedicated Threads once in [GlobalSetup], park them on a Barrier, and have Run release/join them. A cheaper stopgap is ThreadPool.SetMinThreads(Threads + Environment.ProcessorCount, …) plus new ParallelOptions { MaxDegreeOfParallelism = Threads } — but that still doesn't guarantee 32 simultaneous in-flight reads, so dedicated threads are the honest option. Either way, it would be worth recording the observed peak concurrency so a throttled run is visible in the results rather than silent.

Fix this →

Comment on lines +40 to +42
private const int TotalReads = 8192;
private const int PoolSize = 1 << 15;
private const int PoolMask = PoolSize - 1;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium — the 32 K key pool makes "warm" mode a 32 K-hot-key benchmark, not a random-access one

_cursor advances by TotalReads (8192) per invocation and indexes the pool with & PoolMask (32767), so the pool fully wraps every 4 invocations.

  • Cold mode is fine: with --warmupCount 0 --iterationCount 1 --invocationCount 1 only jitting + one measured invocation run, so the measured 8192 keys are distinct from the jitted ones and never repeat. Good design.
  • Warm mode (the first README command, and the source of the indicative numbers in the PR description) is not. BDN's pilot will pick ~150 invocations/iteration at ~3 ms each, so the same 32768 keys are re-read ~37× per iteration. At full scale that is a working set of roughly 32768 × 4 KiB ≈ 130 MB — it fits entirely in the OS page cache and inside the 300 MiB Account HyperClockCache. So the "warm" row measures a tiny hot set, not warm random access over 300 M accounts, and it compresses exactly the differences the benchmark exists to expose.

PoolSize is cheap to raise: 4 M entries costs ~290 MB for the slot pool and ~80 MB for the account pool, which is nothing next to the documented "tens of GB of RAM headroom". Alternatively derive the key from the global read index instead of a fixed pool.

At minimum the README's Warm section should state that warm numbers reflect a 32 K hot key set regardless of dataset scale, so nobody reads them as a warm-at-scale signal.

Fix this →

Comment on lines +110 to +125
public static FlatBaseDatasetSpec EnsureBuilt()
{
FlatBaseDatasetSpec spec = FlatBaseDatasetSpec.FromEnvironment();
if (File.Exists(spec.MarkerPath) && File.ReadAllText(spec.MarkerPath) == spec.MarkerText)
return spec;

if (Directory.Exists(spec.DatasetDir))
Directory.Delete(spec.DatasetDir, recursive: true);
Directory.CreateDirectory(spec.DatasetDir);

Console.WriteLine($"[FlatBase] Building dataset: {spec.MarkerText} under {spec.DatasetDir}");
Build(spec);
File.WriteAllText(spec.MarkerPath, spec.MarkerText);
Console.WriteLine("[FlatBase] Dataset build complete");
return spec;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium — a marker mismatch silently rm -rfs a multi-hour, ~400 GB dataset from inside [GlobalSetup]

EnsureBuilt is called from Setup(), and any marker mismatch (or a build that was interrupted, leaving no marker) unconditionally does Directory.Delete(spec.DatasetDir, recursive: true) before rebuilding. At full scale the README says the build takes hours and needs ≥ 100 GB per backend, so:

  • Ctrl-C'ing a build, then re-running, deletes everything and starts from zero — there is no resume and no warning.
  • A FormatVersion bump or a stray NETH_FLATBENCH_SCALE typo triggers the same, from what looks like a routine dotnet run benchmark command.

Given the documented cost, this deserves a guard: print what is about to be deleted (path + size) and require an explicit opt-in such as NETH_FLATBENCH_REBUILD=1 before deleting a non-empty existing dataset directory, otherwise throw with the command to run. The smoke path can keep the current behaviour.

Related, smaller: the marker is written only after all three backends are built and validated, so there is no way to distinguish "never built" from "half built" — recording per-backend markers would let an interrupted run skip the backends that already completed.

Fix this →

Comment on lines +80 to +85
// A hit pool read must return data and a miss pool read must not — fail loudly, not measure garbage.
using IFlatReadSession session = _backend.BeginSession();
Span<byte> buffer = stackalloc byte[256];
int length = session.GetAccount(_accountKeys.AsSpan(0, FlatBaseBenchmarkDatasetBuilder.AccountKeyLength), buffer);
if (Miss ? length != 0 : length == 0)
throw new InvalidOperationException($"Key pool sanity check failed: Miss={Miss}, first account read returned {length} bytes");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Low — the pool sanity check only covers the account pool

The comment says "A hit pool read must return data and a miss pool read must not", but only _accountKeys[0] is probed. SlotPointRead is half the benchmark and the slot pool uses a different derivation (miss = live account + slotIndex ≥ SlotsPerAccount), so a regression there would silently make the Miss=false slot row measure all-misses (or vice versa) with no failure. Validate does not close this gap — it recomputes its own keys rather than reading the pool.

Two extra lines mirroring the account probe with session.GetSlot(_slotKeys.AsSpan(0, StorageKeyLength), buffer) would cover it.

Fix this →

Comment on lines +4 to +6
// Verbatim copy of Nethermind.State.Flat/PersistedSnapshots/Sorted/SortedTableReader.cs (internal in
// Nethermind.State.Flat, which has no InternalsVisibleTo for benchmarks). Benchmark-scoped
// prototype code — keep in sync with the original; do not diverge.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium — "verbatim copy … do not diverge" is not mechanically checkable, and the copies already diverge

The PR asks for a reviewer preference here, so: a third option beats both copying and InternalsVisibleTo — link the originals into the benchmark project without duplicating them:

<Compile Include="..\Nethermind.State.Flat\PersistedSnapshots\Sorted\*.cs"
         Exclude="..\Nethermind.State.Flat\PersistedSnapshots\Sorted\SortedTableEnumerator.cs"
         Link="State\FlatBase\Sorted\%(Filename)%(Extension)" />

That keeps production visibility unchanged (the linked types compile as internal into the benchmark assembly, and the referenced assembly's own internals stay invisible), removes the drift risk entirely, and drops ~700 lines from the diff. Worth at least trying before merging 7 copies.

If copies do stay, the "keep in sync; do not diverge" instruction needs to be enforceable, and right now it isn't:

  • The copies strip the <remarks> blocks from Block.cs, SortedTable.cs and SortedTableBuilder.cs (the wire-format documentation — the single most valuable thing to keep in sync), and drop TryReadRecordRange from both block readers. So diff against the originals is already noisy and a real behavioural drift would be indistinguishable from the intentional edits.
  • Nothing fails when the original changes. Consider a small test in Nethermind.State.Flat.Test that asserts the copies match the originals modulo the header/namespace lines, so a future format change to the production SortedTable breaks the build instead of silently invalidating a benchmark that is being used to make an architectural go/no-go call.

Fix this →

Comment on lines +44 to +52
DbConfig dbConfig = new();
RocksDbConfigFactory baseFactory = new(
dbConfig, new PruningConfig(), new TestHardwareInfo(), NullLogManager.Instance);
CacheInjectingConfigFactory factory = new(baseFactory, _accountCache, _storageCache);

_db = new ColumnsDb<FlatDbColumns>(
basePath, new DbSettings("Flat", "flat"), dbConfig, factory, NullLogManager.Instance,
Enum.GetValues<FlatDbColumns>());
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Low — worth documenting how faithful the "production-tuned" baseline actually is

I checked the two places where this could silently diverge from production and both turn out fine, but they are non-obvious enough that a one-line comment would save the next reader the same walk:

  • new TestHardwareInfo() reports AvailableMemoryBytes = 10 MB. RocksDbConfigFactory only consults that inside if (databaseName.StartsWith("State")), and this DB is named "Flat", so the low value has no effect here. ✅
  • MaxOpenFilesLimit is null, so the auto-cap branch is skipped and MaxOpenFiles stays unset (RocksDB -1). That matches production on a typical Linux server (ulimit ≫ 10000 ⇒ "leave unlimited"), so it is right for the target host — but it does mean all SST index and filter blocks are preloaded at DB open. For a ≥ 100 GB dataset that is several GB pinned outside the 1 GiB block cache, and it means a post-drop_caches run still measures RocksDB with warm indexes/filters after open. That is generous to RocksDB, so it is the conservative direction for a arena ≥ 1.8× RocksDB gate — but it should be stated in the README next to "Peak RSS per backend", because RocksDB's real memory in that run will be well above the 1 GiB block-cache budget listed there.
  • Also note FlatDbConfig.Layout defaults to FlatLayout.Flat, so FlatRocksDbConfigAdjuster's additionalConfig is "" in production too — matching the "" passed on line 127. Worth a comment, since a future default flip to FlatInTrie would make this mirror wrong without any compile error.

Minor: constructing the 1 GiB of HyperClockCaches on the bulk-load instance in FlatBaseBenchmarkDatasetBuilder.Build costs a GiB during an already memory-hungry build for no benefit; a separate small-cache write path would be cheap.

Comment on lines +238 to +244
public static SortedArenaBackend OpenRead(string dir) => new(
ShardedArenaReader.Open(
Path.Combine(dir, ShardedSortedTableArena.AccountArenaFile),
Path.Combine(dir, ShardedSortedTableArena.AccountDirFile)),
ShardedArenaReader.Open(
Path.Combine(dir, ShardedSortedTableArena.StorageArenaFile),
Path.Combine(dir, ShardedSortedTableArena.StorageDirFile)));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Low — the account ArenaFile leaks if opening the storage arena throws

If the second ShardedArenaReader.Open fails (missing/truncated storage.dir, a short storage.arena, mmap failure at full scale), the already-constructed account reader is never disposed, so its mmap view and SafeFileHandle stay alive until finalization. Same shape in LmdbFlatBackend.Open if the second tx.OpenDatabase throws, and in FlatBaseBenchmarkDatasetBuilder.Build where a throw partway through the using chain is handled correctly but a throw inside ShardedArenaWriter's constructor after the FileStream is opened is not.

Benchmark-scope, so low impact, but at full scale a leaked 130 GB mmap during a failed setup is not free, and .agents/rules/robustness.md asks for using on all IDisposables. A try { … } catch { accounts.Dispose(); throw; } in OpenRead covers it.

Comment on lines +75 to +82
## Go/no-go thresholds (from planning)

On cold uniform-random reads at the `full` scale (≥ 100 GB per backend):

- **Go** if the arena is ≥ **1.8× RocksDB** on cold random hits **and** ≥ **0.85× LMDB**;
- miss cost must be ≤ **1.2× LMDB** (the arena has no bloom filters — misses still binary-search a
shard; if this fails, Phase 1 adds a per-shard filter before any production work);
- otherwise **no-go**: stop the flat-base workstream and record the numbers.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Low — state the "fresh bulk load" caveat next to the thresholds

Both the arena and RocksDB are measured immediately after a perfect, strictly-ascending bulk load (RocksDB additionally gets Flush → Compact → Flush), so both are in a best-case shape that a live node never sees. That is symmetric and therefore fair as a relative comparison, but the numbers are an upper bound on both backends, and the arena's advantage in particular comes partly from being a single perfectly-packed sorted image per shard — the property a production base tier would have to maintain through compaction.

A sentence under "Go/no-go thresholds" saying the gate measures a freshly-compacted steady state, and that write amplification / compaction cost is explicitly out of scope for Phase 0, would stop the ratio being read as a production speedup.

Two smaller README items:

  • Total disk for the full run is closer to ~400 GB across the three backends (arena alone: ~28 GB accounts + ~102 GB storage), not "≥ 100 GB per backend" read as ~300 GB. Worth being concrete, given LmdbMapSize also ftruncates a ~403 GB sparse file.
  • "Peak RSS per backend (… RocksDB holds a 1 GiB block cache)" understates RocksDB: with max_open_files unlimited it also pins every SST's index and filter blocks outside that budget.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

build changes dependencies Pull requests that update a dependency file test

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant