Skip to content

Commit 1bfcfd2

Browse files
committed
Update to add benchmarks
1 parent e955b7a commit 1bfcfd2

34 files changed

Lines changed: 1266 additions & 6 deletions
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
using BenchmarkDotNet.Columns;
2+
using BenchmarkDotNet.Configs;
3+
using BenchmarkDotNet.Diagnosers;
4+
using BenchmarkDotNet.Exporters;
5+
using BenchmarkDotNet.Exporters.Csv;
6+
7+
namespace TrimDB.Benchmarks
8+
{
9+
public class TrimBenchConfig : ManualConfig
10+
{
11+
public TrimBenchConfig()
12+
{
13+
AddDiagnoser(MemoryDiagnoser.Default);
14+
AddDiagnoser(new DisassemblyDiagnoser(new DisassemblyDiagnoserConfig(
15+
maxDepth: 2,
16+
syntax: DisassemblySyntax.Masm,
17+
exportGithubMarkdown: true)));
18+
AddExporter(HtmlExporter.Default);
19+
AddExporter(MarkdownExporter.GitHub);
20+
}
21+
}
22+
}
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
using System;
2+
using System.Buffers;
3+
using System.Text;
4+
using System.Threading.Tasks;
5+
using BenchmarkDotNet.Attributes;
6+
using BenchmarkDotNet.Configs;
7+
using BenchmarkDotNet.Diagnosers;
8+
using BenchmarkDotNet.Exporters;
9+
using TrimDB.Core;
10+
using TrimDB.Core.InMemory.SkipList32;
11+
using TrimDB.Core.Storage;
12+
using TrimDB.Core.Storage.Blocks;
13+
using TrimDB.Core.Storage.Blocks.MemoryMappedCache;
14+
15+
namespace TrimDB.Benchmarks
16+
{
17+
[Config(typeof(BlockSearchBenchConfig))]
18+
public class BlockSearchBench
19+
{
20+
private const int EntryCount = 100;
21+
private const int BlockSize = 4096;
22+
23+
// In-memory block data
24+
private byte[] _blockData = null!;
25+
private FixedMemoryOwner _blockOwner = null!;
26+
27+
// Keys for lookup
28+
private byte[] _middleKey = null!;
29+
private byte[] _firstKey = null!;
30+
private byte[] _missingKey = null!;
31+
32+
// File-backed SSTable
33+
private string _tempFolder = null!;
34+
private TableFile _tableFile = null!;
35+
private MMapBlockCache _blockCache = null!;
36+
private byte[] _tableMiddleKey = null!;
37+
38+
[GlobalSetup]
39+
public async Task GlobalSetup()
40+
{
41+
// ---- In-memory block ----
42+
_blockData = new byte[BlockSize];
43+
var builder = new SlottedBlockBuilder(_blockData);
44+
var keys = new byte[EntryCount][];
45+
46+
for (int i = 0; i < EntryCount; i++)
47+
{
48+
keys[i] = Encoding.UTF8.GetBytes($"block_key_{i:D6}");
49+
}
50+
51+
// Sort keys lexicographically (they're already sorted due to formatting, but be safe)
52+
Array.Sort(keys, (a, b) => a.AsSpan().SequenceCompareTo(b));
53+
54+
foreach (var key in keys)
55+
{
56+
var value = Encoding.UTF8.GetBytes($"val_{key.Length}");
57+
if (!builder.TryAdd(key, value, isDeleted: false))
58+
break;
59+
}
60+
builder.Finish();
61+
62+
_blockOwner = new FixedMemoryOwner(_blockData);
63+
_firstKey = keys[0];
64+
_middleKey = keys[EntryCount / 2];
65+
_missingKey = Encoding.UTF8.GetBytes("block_key_zzzzzz");
66+
67+
// ---- File-backed SSTable ----
68+
_tempFolder = System.IO.Path.Combine(System.IO.Path.GetTempPath(), "trimdb_block_bench_" + Guid.NewGuid().ToString("N"));
69+
System.IO.Directory.CreateDirectory(_tempFolder);
70+
71+
var allocator = new ArrayBasedAllocator32(16 * 1024 * 1024, 25);
72+
var skipList = new SkipList32(allocator);
73+
74+
for (int i = 0; i < 1000; i++)
75+
{
76+
var key = Encoding.UTF8.GetBytes($"sst_key_{i:D6}");
77+
var value = Encoding.UTF8.GetBytes($"sst_value_{i:D6}");
78+
skipList.Put(key, value);
79+
}
80+
81+
var fileName = System.IO.Path.Combine(_tempFolder, "Level1_1.trim");
82+
var writer = new TableFileWriter(fileName);
83+
await writer.SaveMemoryTable(skipList);
84+
85+
_blockCache = new MMapBlockCache();
86+
_tableFile = new TableFile(fileName, _blockCache);
87+
await _tableFile.LoadAsync();
88+
89+
_tableMiddleKey = Encoding.UTF8.GetBytes("sst_key_000500");
90+
}
91+
92+
[GlobalCleanup]
93+
public void GlobalCleanup()
94+
{
95+
_blockCache?.Dispose();
96+
try { System.IO.Directory.Delete(_tempFolder, true); } catch { }
97+
}
98+
99+
// --- BlockReader benchmarks (class-based, IDisposable) ---
100+
101+
[Benchmark(Baseline = true)]
102+
public BlockReader.KeySearchResult BlockReader_FindMiddle()
103+
{
104+
using var reader = new BlockReader(new FixedMemoryOwner(_blockData));
105+
return reader.TryFindKey(_middleKey);
106+
}
107+
108+
[Benchmark]
109+
public BlockReader.KeySearchResult BlockReader_FindFirst()
110+
{
111+
using var reader = new BlockReader(new FixedMemoryOwner(_blockData));
112+
return reader.TryFindKey(_firstKey);
113+
}
114+
115+
[Benchmark]
116+
public BlockReader.KeySearchResult BlockReader_FindMissing()
117+
{
118+
using var reader = new BlockReader(new FixedMemoryOwner(_blockData));
119+
return reader.TryFindKey(_missingKey);
120+
}
121+
122+
// --- BlockView benchmarks (ref struct, zero-alloc) ---
123+
124+
[Benchmark]
125+
public BlockReader.KeySearchResult BlockView_FindMiddle()
126+
{
127+
var view = new BlockView(_blockData);
128+
return view.TryFindKey(_middleKey);
129+
}
130+
131+
[Benchmark]
132+
public BlockReader.KeySearchResult BlockView_FindFirst()
133+
{
134+
var view = new BlockView(_blockData);
135+
return view.TryFindKey(_firstKey);
136+
}
137+
138+
[Benchmark]
139+
public int BlockView_IterateAll()
140+
{
141+
var view = new BlockView(_blockData);
142+
int count = 0;
143+
while (view.TryGetNextKey(out _))
144+
{
145+
count++;
146+
}
147+
return count;
148+
}
149+
150+
// --- Full SSTable path ---
151+
152+
[Benchmark]
153+
public async Task<ReadOnlyMemory<byte>> TableFile_FindKey()
154+
{
155+
var hash = new Core.Hashing.MurmurHash3().ComputeHash64(_tableMiddleKey);
156+
var result = await _tableFile.GetAsync(_tableMiddleKey, hash);
157+
return result.Value;
158+
}
159+
}
160+
161+
/// <summary>
162+
/// Wraps a byte[] as IMemoryOwner without disposing the underlying array.
163+
/// Used for benchmarking BlockReader without heap allocation noise from pooling.
164+
/// </summary>
165+
internal sealed class FixedMemoryOwner : IMemoryOwner<byte>
166+
{
167+
private readonly byte[] _data;
168+
169+
public FixedMemoryOwner(byte[] data) => _data = data;
170+
public Memory<byte> Memory => _data;
171+
public void Dispose() { } // intentional no-op
172+
}
173+
174+
/// <summary>
175+
/// Config with deeper disassembly (depth 3) for block binary search hot path.
176+
/// </summary>
177+
public class BlockSearchBenchConfig : ManualConfig
178+
{
179+
public BlockSearchBenchConfig()
180+
{
181+
AddDiagnoser(MemoryDiagnoser.Default);
182+
AddDiagnoser(new DisassemblyDiagnoser(new DisassemblyDiagnoserConfig(
183+
maxDepth: 3,
184+
syntax: DisassemblySyntax.Masm,
185+
exportGithubMarkdown: true)));
186+
AddExporter(HtmlExporter.Default);
187+
AddExporter(MarkdownExporter.GitHub);
188+
}
189+
}
190+
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
using System;
2+
using System.Text;
3+
using System.Threading.Tasks;
4+
using BenchmarkDotNet.Attributes;
5+
using TrimDB.Core;
6+
using TrimDB.Core.InMemory.SkipList32;
7+
using TrimDB.Core.Storage.Blocks.CachePrototype;
8+
9+
namespace TrimDB.Benchmarks
10+
{
11+
[Config(typeof(TrimBenchConfig))]
12+
public class DatabaseReadBench
13+
{
14+
private TrimDatabase _db = null!;
15+
private string _tempFolder = null!;
16+
private byte[] _hitKey = null!;
17+
private byte[] _missKey = null!;
18+
private const int KeyCount = 10_000;
19+
20+
[GlobalSetup]
21+
public async Task GlobalSetup()
22+
{
23+
_tempFolder = System.IO.Path.Combine(System.IO.Path.GetTempPath(), "trimdb_read_bench_" + Guid.NewGuid().ToString("N"));
24+
System.IO.Directory.CreateDirectory(_tempFolder);
25+
26+
var options = new TrimDatabaseOptions
27+
{
28+
DatabaseFolder = _tempFolder,
29+
DisableWAL = true,
30+
DisableMerging = true,
31+
DisableManifest = true,
32+
MemoryTable = () => new SkipList32(new ArrayBasedAllocator32(4 * 1024 * 1024, 25)),
33+
BlockCache = () => new ProtoSharded(200),
34+
};
35+
36+
_db = new TrimDatabase(options);
37+
await _db.LoadAsync();
38+
39+
// Populate keys
40+
for (int i = 0; i < KeyCount; i++)
41+
{
42+
var key = Encoding.UTF8.GetBytes($"key_{i:D8}");
43+
var value = Encoding.UTF8.GetBytes($"value_{i:D8}");
44+
await _db.PutAsync(key, value);
45+
}
46+
47+
// Flush to SSTable
48+
await _db.FlushAsync();
49+
await Task.Delay(200);
50+
51+
_hitKey = Encoding.UTF8.GetBytes($"key_{KeyCount / 2:D8}");
52+
_missKey = Encoding.UTF8.GetBytes("key_zzzzzzzz_not_exist");
53+
}
54+
55+
[GlobalCleanup]
56+
public async Task GlobalCleanup()
57+
{
58+
await _db.DisposeAsync();
59+
try { System.IO.Directory.Delete(_tempFolder, true); } catch { }
60+
}
61+
62+
[Benchmark(Baseline = true)]
63+
public async Task<ReadOnlyMemory<byte>> GetAsyncFound()
64+
{
65+
return await _db.GetAsync(_hitKey);
66+
}
67+
68+
[Benchmark]
69+
public async Task<ReadOnlyMemory<byte>> GetAsyncMiss()
70+
{
71+
return await _db.GetAsync(_missKey);
72+
}
73+
74+
[Benchmark]
75+
public async Task GetWithLeaseAsync()
76+
{
77+
var lease = await _db.GetWithLeaseAsync(_hitKey);
78+
lease.Dispose();
79+
}
80+
81+
[Benchmark]
82+
public async Task<int> ScanAll()
83+
{
84+
int count = 0;
85+
await foreach (var entry in _db.ScanAsync())
86+
{
87+
count++;
88+
}
89+
return count;
90+
}
91+
}
92+
}

0 commit comments

Comments
 (0)