Skip to content

Commit 27504c2

Browse files
author
MPCoreDeveloper
committed
performance improvements
1 parent bbd701a commit 27504c2

File tree

9 files changed

+193
-57
lines changed

9 files changed

+193
-57
lines changed

SharpCoreDB/DataStructures/IndexManager.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,19 @@ private async Task UpdateIndexesAsync()
5858
}
5959
}
6060

61+
/// <summary>
62+
/// Updates indexes asynchronously for a given update.
63+
/// </summary>
64+
/// <param name="update">The index update.</param>
65+
public async Task UpdateIndexesAsync(IndexUpdate update)
66+
{
67+
foreach (var index in update.Indexes)
68+
{
69+
index.Add(update.Row);
70+
}
71+
await Task.CompletedTask; // Placeholder for async
72+
}
73+
6174
/// <summary>
6275
/// Disposes the index manager and completes asynchronous operations.
6376
/// </summary>

SharpCoreDB/DataStructures/Table.cs

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ public Table(IStorage storage, bool isReadOnly = false) : this()
3737
{
3838
this.indexManager = new IndexManager();
3939
}
40+
41+
// PERFORMANCE FIX: Start background task for asynchronous index updates
42+
Task.Run(ProcessIndexUpdatesAsync);
4043
}
4144

4245
/// <inheritdoc />
@@ -84,6 +87,9 @@ public Table(IStorage storage, bool isReadOnly = false) : this()
8487
/// </summary>
8588
private IndexManager? indexManager;
8689

90+
// PERFORMANCE FIX: Asynchronous index updates via channel to avoid blocking operations
91+
private readonly Channel<IndexUpdate> _indexQueue = Channel.CreateUnbounded<IndexUpdate>();
92+
8793
/// <summary>
8894
/// Sets the storage for this table.
8995
/// </summary>
@@ -164,7 +170,8 @@ public void Insert(Dictionary<string, object> row)
164170
// Queue index updates asynchronously
165171
if (this.indexManager != null && this.hashIndexes.Count > 0)
166172
{
167-
this.indexManager.QueueUpdate(new IndexManager.IndexUpdate(row, this.hashIndexes.Values.ToList()));
173+
// PERFORMANCE FIX: Fire-and-forget index update via channel
174+
_ = _indexQueue.Writer.WriteAsync(new IndexUpdate(row, this.hashIndexes.Values.ToList()));
168175
}
169176
}
170177
}
@@ -552,6 +559,24 @@ public void CreateHashIndex(string columnName)
552559
public void Dispose()
553560
{
554561
this.indexManager?.Dispose();
562+
_indexQueue.Writer.Complete();
555563
GC.SuppressFinalize(this);
556564
}
565+
566+
private async Task ProcessIndexUpdatesAsync()
567+
{
568+
await foreach (var update in _indexQueue.Reader.ReadAllAsync())
569+
{
570+
// PERFORMANCE FIX: Process index updates asynchronously in background
571+
foreach (var index in update.Indexes)
572+
{
573+
index.Add(update.Row);
574+
}
575+
}
576+
}
577+
578+
/// <summary>
579+
/// Represents an index update operation.
580+
/// </summary>
581+
private record IndexUpdate(Dictionary<string, object> Row, IEnumerable<HashIndex> Indexes);
557582
}

SharpCoreDB/Database.cs

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ public class Database : IDatabase
3131
private readonly ConcurrentDictionary<string, CachedQueryPlan> _prepared = new();
3232
private readonly WalManager _walManager;
3333

34+
// PERFORMANCE FIX: Cache prepared query plans to avoid repeated parsing
35+
private readonly ConcurrentDictionary<string, CachedQueryPlan> _preparedPlans = new();
36+
3437
/// <summary>
3538
/// Initializes a new instance of the <see cref="Database"/> class.
3639
/// </summary>
@@ -288,6 +291,26 @@ public async Task ExecutePreparedAsync(PreparedStatement stmt, Dictionary<string
288291
await Task.Run(() => this.ExecutePrepared(stmt, parameters), cancellationToken).ConfigureAwait(false);
289292
}
290293

294+
/// <summary>
295+
/// Executes a prepared statement asynchronously with parameters.
296+
/// </summary>
297+
/// <param name="stmt">The prepared statement.</param>
298+
/// <param name="parameters">The parameters to bind.</param>
299+
/// <returns>A ValueTask representing the execution result.</returns>
300+
public async ValueTask<object> ExecutePreparedAsync(PreparedStatement stmt, params object[] parameters)
301+
{
302+
// PERFORMANCE FIX: Use cached plan without re-parsing
303+
var plan = _preparedPlans[stmt.Sql];
304+
// Convert params to dictionary
305+
var paramDict = new Dictionary<string, object?>();
306+
for (int i = 0; i < parameters.Length; i++)
307+
{
308+
paramDict[i.ToString()] = parameters[i];
309+
}
310+
await Task.Run(() => this.ExecutePrepared(stmt, paramDict));
311+
return new object(); // placeholder
312+
}
313+
291314
/// <inheritdoc />
292315
public void CreateUser(string username, string password) => this.userService.CreateUser(username, password);
293316

@@ -311,13 +334,13 @@ public IDatabase Initialize(string dbPath, string masterPassword)
311334
/// <returns>A prepared statement instance.</returns>
312335
public PreparedStatement Prepare(string sql)
313336
{
314-
if (!_prepared.TryGetValue(sql, out var plan))
337+
// PERFORMANCE FIX: Parse and cache query plan once per unique SQL string
338+
if (!_preparedPlans.TryGetValue(sql, out var plan))
315339
{
316340
var parts = sql.Trim().Split(' ', StringSplitOptions.RemoveEmptyEntries);
317341
plan = new CachedQueryPlan { Sql = sql, Parts = parts };
318-
_prepared[sql] = plan;
342+
_preparedPlans[sql] = plan;
319343
}
320-
321344
return new PreparedStatement(sql, plan);
322345
}
323346

SharpCoreDB/Interfaces/ICryptoService.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
namespace SharpCoreDB.Interfaces;
66

7+
using SharpCoreDB.Services;
8+
79
/// <summary>
810
/// Interface for cryptographic operations including password hashing and data encryption/decryption.
911
/// </summary>
@@ -32,4 +34,23 @@ public interface ICryptoService
3234
/// <param name="encryptedData">The encrypted data.</param>
3335
/// <returns>The decrypted data as a byte array.</returns>
3436
byte[] Decrypt(byte[] key, byte[] encryptedData);
37+
38+
/// <summary>
39+
/// Encrypts a page using AES-256-GCM.
40+
/// </summary>
41+
/// <param name="page">The page data to encrypt (modified in place if buffer is large enough).</param>
42+
void EncryptPage(Span<byte> page);
43+
44+
/// <summary>
45+
/// Decrypts a page using AES-256-GCM.
46+
/// </summary>
47+
/// <param name="page">The encrypted page data (modified in place to decrypted data).</param>
48+
void DecryptPage(Span<byte> page);
49+
50+
/// <summary>
51+
/// Gets an AesGcmEncryption instance for the specified key.
52+
/// </summary>
53+
/// <param name="key">The encryption key.</param>
54+
/// <returns>An AesGcmEncryption instance.</returns>
55+
SharpCoreDB.Services.AesGcmEncryption GetAesGcmEncryption(byte[] key);
3556
}

SharpCoreDB/Services/AesGcmEncryption.cs

Lines changed: 53 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,11 @@ namespace SharpCoreDB.Services;
1212
/// </summary>
1313
public class AesGcmEncryption : IDisposable
1414
{
15-
private readonly AesGcm? _aesInstance;
15+
private readonly AesGcm _aes; // PERFORMANCE FIX: Single reusable AesGcm instance to avoid repeated allocations
1616
private readonly bool _disableEncrypt;
1717
private readonly ArrayPool<byte> _pool = ArrayPool<byte>.Shared;
18+
private readonly byte[] _nonceBuffer; // PERFORMANCE FIX: Pre-allocated nonce buffer for reuse
19+
private readonly byte[] _tagBuffer; // PERFORMANCE FIX: Pre-allocated tag buffer for reuse
1820

1921
/// <summary>
2022
/// Initializes a new instance of the AesGcmEncryption class.
@@ -26,7 +28,9 @@ public AesGcmEncryption(byte[] key, bool disableEncrypt = false)
2628
_disableEncrypt = disableEncrypt;
2729
if (!disableEncrypt)
2830
{
29-
_aesInstance = new AesGcm(key, AesGcm.TagByteSizes.MaxSize);
31+
_aes = new AesGcm(key, AesGcm.TagByteSizes.MaxSize); // PERFORMANCE FIX: Create once and reuse
32+
_nonceBuffer = _pool.Rent(AesGcm.NonceByteSizes.MaxSize); // PERFORMANCE FIX: Pre-allocate for reuse
33+
_tagBuffer = _pool.Rent(AesGcm.TagByteSizes.MaxSize); // PERFORMANCE FIX: Pre-allocate for reuse
3034
}
3135
}
3236

@@ -39,23 +43,19 @@ public byte[] Encrypt(byte[] data)
3943
{
4044
if (_disableEncrypt) return data;
4145

42-
var nonce = _pool.Rent(AesGcm.NonceByteSizes.MaxSize);
43-
var tag = _pool.Rent(AesGcm.TagByteSizes.MaxSize);
4446
var cipher = _pool.Rent(data.Length);
4547
try
4648
{
47-
RandomNumberGenerator.Fill(nonce.AsSpan(0, AesGcm.NonceByteSizes.MaxSize));
48-
_aesInstance!.Encrypt(nonce.AsSpan(0, AesGcm.NonceByteSizes.MaxSize), data, cipher.AsSpan(0, data.Length), tag.AsSpan(0, AesGcm.TagByteSizes.MaxSize));
49+
RandomNumberGenerator.Fill(_nonceBuffer.AsSpan(0, AesGcm.NonceByteSizes.MaxSize)); // PERFORMANCE FIX: Use pre-allocated buffer
50+
_aes.Encrypt(_nonceBuffer.AsSpan(0, AesGcm.NonceByteSizes.MaxSize), data, cipher.AsSpan(0, data.Length), _tagBuffer.AsSpan(0, AesGcm.TagByteSizes.MaxSize)); // PERFORMANCE FIX: Reuse AesGcm instance
4951
var result = new byte[AesGcm.NonceByteSizes.MaxSize + data.Length + AesGcm.TagByteSizes.MaxSize];
50-
nonce.AsSpan(0, AesGcm.NonceByteSizes.MaxSize).CopyTo(result.AsSpan(0, AesGcm.NonceByteSizes.MaxSize));
52+
_nonceBuffer.AsSpan(0, AesGcm.NonceByteSizes.MaxSize).CopyTo(result.AsSpan(0, AesGcm.NonceByteSizes.MaxSize));
5153
cipher.AsSpan(0, data.Length).CopyTo(result.AsSpan(AesGcm.NonceByteSizes.MaxSize, data.Length));
52-
tag.AsSpan(0, AesGcm.TagByteSizes.MaxSize).CopyTo(result.AsSpan(AesGcm.NonceByteSizes.MaxSize + data.Length, AesGcm.TagByteSizes.MaxSize));
54+
_tagBuffer.AsSpan(0, AesGcm.TagByteSizes.MaxSize).CopyTo(result.AsSpan(AesGcm.NonceByteSizes.MaxSize + data.Length, AesGcm.TagByteSizes.MaxSize));
5355
return result;
5456
}
5557
finally
5658
{
57-
_pool.Return(nonce);
58-
_pool.Return(tag);
5959
_pool.Return(cipher);
6060
}
6161
}
@@ -76,7 +76,7 @@ public byte[] Decrypt(byte[] encryptedData)
7676
var nonce = encryptedData.AsSpan(0, nonceSize);
7777
var cipher = encryptedData.AsSpan(nonceSize, cipherLength);
7878
var tag = encryptedData.AsSpan(nonceSize + cipherLength, tagSize);
79-
_aesInstance!.Decrypt(nonce, cipher, tag, plain);
79+
_aes.Decrypt(nonce, cipher, tag, plain); // PERFORMANCE FIX: Reuse AesGcm instance
8080
return plain;
8181
}
8282

@@ -99,22 +99,18 @@ public int Encrypt(ReadOnlySpan<byte> data, Span<byte> output)
9999
var totalSize = nonceSize + data.Length + tagSize;
100100
if (output.Length < totalSize) throw new ArgumentException("Output buffer too small");
101101

102-
var nonce = _pool.Rent(nonceSize);
103-
var tag = _pool.Rent(tagSize);
104102
var cipher = _pool.Rent(data.Length);
105103
try
106104
{
107-
RandomNumberGenerator.Fill(nonce.AsSpan(0, nonceSize));
108-
_aesInstance!.Encrypt(nonce.AsSpan(0, nonceSize), data, cipher.AsSpan(0, data.Length), tag.AsSpan(0, tagSize));
109-
nonce.AsSpan(0, nonceSize).CopyTo(output.Slice(0, nonceSize));
105+
RandomNumberGenerator.Fill(_nonceBuffer.AsSpan(0, nonceSize)); // PERFORMANCE FIX: Use pre-allocated buffer
106+
_aes.Encrypt(_nonceBuffer.AsSpan(0, nonceSize), data, cipher.AsSpan(0, data.Length), _tagBuffer.AsSpan(0, tagSize)); // PERFORMANCE FIX: Reuse AesGcm instance
107+
_nonceBuffer.AsSpan(0, nonceSize).CopyTo(output.Slice(0, nonceSize));
110108
cipher.AsSpan(0, data.Length).CopyTo(output.Slice(nonceSize, data.Length));
111-
tag.AsSpan(0, tagSize).CopyTo(output.Slice(nonceSize + data.Length, tagSize));
109+
_tagBuffer.AsSpan(0, tagSize).CopyTo(output.Slice(nonceSize + data.Length, tagSize));
112110
return totalSize;
113111
}
114112
finally
115113
{
116-
_pool.Return(nonce);
117-
_pool.Return(tag);
118114
_pool.Return(cipher);
119115
}
120116
}
@@ -141,15 +137,51 @@ public int Decrypt(ReadOnlySpan<byte> encryptedData, Span<byte> output)
141137
var nonce = encryptedData.Slice(0, nonceSize);
142138
var cipher = encryptedData.Slice(nonceSize, cipherLength);
143139
var tag = encryptedData.Slice(nonceSize + cipherLength, tagSize);
144-
_aesInstance!.Decrypt(nonce, cipher, tag, output.Slice(0, cipherLength));
140+
_aes.Decrypt(nonce, cipher, tag, output.Slice(0, cipherLength)); // PERFORMANCE FIX: Reuse AesGcm instance
145141
return cipherLength;
146142
}
147143

144+
/// <summary>
145+
/// Encrypts a page using AES-256-GCM.
146+
/// </summary>
147+
/// <param name="page">The page data to encrypt (modified in place if buffer is large enough).</param>
148+
public void EncryptPage(Span<byte> page)
149+
{
150+
var encrypted = Encrypt(page.ToArray());
151+
if (page.Length >= encrypted.Length)
152+
{
153+
encrypted.AsSpan().CopyTo(page);
154+
}
155+
else
156+
{
157+
throw new ArgumentException("Page buffer too small for encrypted data");
158+
}
159+
}
160+
161+
/// <summary>
162+
/// Decrypts a page using AES-256-GCM.
163+
/// </summary>
164+
/// <param name="page">The encrypted page data (modified in place to decrypted data).</param>
165+
public void DecryptPage(Span<byte> page)
166+
{
167+
var decrypted = Decrypt(page.ToArray());
168+
if (page.Length >= decrypted.Length)
169+
{
170+
decrypted.AsSpan().CopyTo(page);
171+
}
172+
else
173+
{
174+
throw new ArgumentException("Page buffer too small for decrypted data");
175+
}
176+
}
177+
148178
/// <summary>
149179
/// Disposes the AesGcm instance.
150180
/// </summary>
151181
public void Dispose()
152182
{
153-
_aesInstance?.Dispose();
183+
_aes?.Dispose();
184+
if (_nonceBuffer != null) _pool.Return(_nonceBuffer); // PERFORMANCE FIX: Return pre-allocated buffers
185+
if (_tagBuffer != null) _pool.Return(_tagBuffer);
154186
}
155187
}

SharpCoreDB/Services/CryptoService.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,4 +42,24 @@ public byte[] Decrypt(byte[] key, byte[] encryptedData)
4242
aes.Decrypt(nonce, cipher, tag, plain);
4343
return plain;
4444
}
45+
46+
/// <inheritdoc />
47+
public void EncryptPage(Span<byte> page)
48+
{
49+
// Not implemented in CryptoService, use GetAesGcmEncryption
50+
throw new NotImplementedException();
51+
}
52+
53+
/// <inheritdoc />
54+
public void DecryptPage(Span<byte> page)
55+
{
56+
// Not implemented in CryptoService, use GetAesGcmEncryption
57+
throw new NotImplementedException();
58+
}
59+
60+
/// <inheritdoc />
61+
public SharpCoreDB.Services.AesGcmEncryption GetAesGcmEncryption(byte[] key)
62+
{
63+
return new AesGcmEncryption(key, false);
64+
}
4565
}

SharpCoreDB/Services/WAL.cs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ public class WAL : IWAL, IDisposable
2626
private readonly ArrayPool<byte> pool = ArrayPool<byte>.Shared;
2727
private const int BufferSize = 4 * 1024 * 1024; // 4MB
2828
private readonly SemaphoreSlim semaphore = new(1);
29-
private const int FlushThreshold = 1000;
30-
private List<WalEntry> _pendingEntries = new();
29+
private const int FlushThreshold = 1000; // PERFORMANCE FIX: Batch flush threshold to reduce I/O calls
30+
private List<WalEntry> _pendingEntries = new(1024); // PERFORMANCE FIX: Pre-allocated list for pending entries to batch writes
3131
private readonly WalManager? _walManager;
3232

3333
/// <summary>
@@ -86,10 +86,10 @@ public async Task AppendEntryAsync(WalEntry entry, CancellationToken cancellatio
8686
throw new ObjectDisposedException(nameof(WAL));
8787
}
8888

89-
_pendingEntries.Add(entry);
89+
_pendingEntries.Add(entry); // PERFORMANCE FIX: Add to pending list instead of immediate flush
9090
if (_pendingEntries.Count >= FlushThreshold)
9191
{
92-
await FlushPendingAsync();
92+
await FlushPendingAsync(); // PERFORMANCE FIX: Batch flush when threshold reached
9393
}
9494
}
9595

@@ -108,8 +108,8 @@ private async Task FlushPendingAsync()
108108
operationBytes.AsSpan().CopyTo(this.buffer.AsSpan(this.bufferPosition));
109109
this.bufferPosition += operationBytes.Length;
110110
}
111-
await this.FlushBufferAsync(); // Ensure buffer is flushed after batch
112-
_pendingEntries.Clear();
111+
await this.FlushBufferAsync(); // PERFORMANCE FIX: Single flush after batch write
112+
_pendingEntries.Clear(); // PERFORMANCE FIX: Clear after successful flush
113113
}
114114
catch (Exception ex)
115115
{
@@ -152,9 +152,9 @@ public async Task CommitTransactionAsync(CancellationToken cancellationToken = d
152152
}
153153

154154
// Ensure all buffered data is written before committing to maintain WAL integrity
155-
await FlushPendingAsync();
155+
await FlushPendingAsync(); // PERFORMANCE FIX: Ensure all pending entries are flushed
156156
await this.FlushBufferAsync();
157-
await this.fileStream.FlushAsync(cancellationToken);
157+
// PERFORMANCE FIX: Removed redundant FlushAsync call to avoid double flushing
158158
this.fileStream.Close();
159159

160160
File.Delete(this.logPath);

SharpCoreDB/SharpCoreDB.Tests/SharpCoreDB.Tests.csproj

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22
<PropertyGroup>
33
<TargetFramework>net10.0</TargetFramework>
4+
<LangVersion>14.0</LangVersion>
45
<ImplicitUsings>enable</ImplicitUsings>
56
<Nullable>enable</Nullable>
67
<IsPackable>false</IsPackable>
8+
<GenerateTargetFrameworkAttribute>false</GenerateTargetFrameworkAttribute>
79
</PropertyGroup>
810
<ItemGroup>
911
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
@@ -15,4 +17,4 @@
1517
<ItemGroup>
1618
<ProjectReference Include="..\SharpCoreDB.Core\SharpCoreDB.Core.csproj" />
1719
</ItemGroup>
18-
</Project>
20+
</Project>

0 commit comments

Comments
 (0)