From f74d54dc66b68d8199c86ef27b0912d203b61cfe Mon Sep 17 00:00:00 2001 From: MPCoreDeveloper Date: Fri, 4 Sep 2026 06:47:20 +0200 Subject: [PATCH] perf: whole-file range read for DELETE tombstone markers TombstoneRecords now reads the whole (<=32MB) file once for batches of >=64 markers and resolves each record length from the buffer instead of one pread per offset; the per-marker pread dominated the DELETE commit in profiling (~50ms/10K). Per-offset reads remain the fallback for larger files/small batches. Measured (Release, comparative DELETE 10K of ~100K rows, median of 3): SQL ~53K -> ~57K ops/s, Direct ~69K -> ~87K ops/s. --- src/SharpCoreDB/Services/Storage.Append.cs | 71 ++++++++++++++++++---- 1 file changed, 58 insertions(+), 13 deletions(-) diff --git a/src/SharpCoreDB/Services/Storage.Append.cs b/src/SharpCoreDB/Services/Storage.Append.cs index 576cea8f..49c31a4f 100644 --- a/src/SharpCoreDB/Services/Storage.Append.cs +++ b/src/SharpCoreDB/Services/Storage.Append.cs @@ -35,6 +35,9 @@ public partial class Storage /// Maximum accepted record/ciphertext length (1 GB). private const int MaxRecordSize = 1_000_000_000; + /// Upper bound for the whole-file tombstone-marker range read (DELETE commit path). + private const long RangeMarkerReadLimitBytes = 32 * 1024 * 1024; + /// AES-GCM overhead = nonce(12) + tag(16). private const int GcmOverhead = CryptoConstants.GCM_NONCE_SIZE + CryptoConstants.GCM_TAG_SIZE; @@ -681,28 +684,70 @@ public void TombstoneRecords(string path, long[] offsets) Span lengthBuffer = stackalloc byte[4]; Span marker = stackalloc byte[4]; - foreach (var offset in offsets) + // Range fast path: for a large batch on a bounded file, read the whole file ONCE and + // resolve every record length from the buffer instead of one pread per offset (the + // per-marker pread dominated the DELETE commit in profiling: ~50ms/10K markers). + long fileLength = 0; + byte[]? wholeFile = null; + if (offsets.Length >= 64) { - if (offset < 0) + fileLength = File.Exists(path) ? new FileInfo(path).Length : 0; + if (fileLength > 0 && fileLength <= RangeMarkerReadLimitBytes && fileLength <= int.MaxValue) { - continue; + wholeFile = new byte[(int)fileLength]; + if (RandomAccess.Read(readHandle, wholeFile, 0) != fileLength) + { + wholeFile = null; + } } + } - if (RandomAccess.Read(readHandle, lengthBuffer, offset) != 4) + if (wholeFile is not null) + { + foreach (var offset in offsets) { - continue; // offset at/beyond EOF — not a physical record - } + if (offset < 0 || offset + 4 > wholeFile.Length) + { + continue; + } - int currentLength = BinaryPrimitives.ReadInt32LittleEndian(lengthBuffer); - if (currentLength <= 0) - { - continue; // already tombstoned or invalid + int currentLength = BinaryPrimitives.ReadInt32LittleEndian(wholeFile.AsSpan((int)offset, 4)); + if (currentLength <= 0) + { + continue; // already tombstoned or invalid + } + + BinaryPrimitives.WriteInt32LittleEndian(marker, -(4 + currentLength)); + WriteRecordInPlace(path, offset, marker, ReadOnlySpan.Empty); + + pagesToEvict?.Add(ComputePageId(path, offset)); } + } + else + { + foreach (var offset in offsets) + { + if (offset < 0) + { + continue; + } - BinaryPrimitives.WriteInt32LittleEndian(marker, -(4 + currentLength)); - WriteRecordInPlace(path, offset, marker, ReadOnlySpan.Empty); + if (RandomAccess.Read(readHandle, lengthBuffer, offset) != 4) + { + continue; // offset at/beyond EOF — not a physical record + } + + int currentLength = BinaryPrimitives.ReadInt32LittleEndian(lengthBuffer); + if (currentLength <= 0) + { + continue; // already tombstoned or invalid + } + + BinaryPrimitives.WriteInt32LittleEndian(marker, -(4 + currentLength)); + WriteRecordInPlace(path, offset, marker, ReadOnlySpan.Empty); - pagesToEvict?.Add(ComputePageId(path, offset)); + pagesToEvict?.Add(ComputePageId(path, offset)); + } } } catch (IOException)