diff --git a/src/Workspaces/Core/Desktop/Workspace/SQLite/Interop/OpenFlags.cs b/src/Workspaces/Core/Desktop/Workspace/SQLite/Interop/OpenFlags.cs index 4504758846d432525486049b6793eb7e943aee87..f0708fe2005f55cc682152339c62665925204240 100644 --- a/src/Workspaces/Core/Desktop/Workspace/SQLite/Interop/OpenFlags.cs +++ b/src/Workspaces/Core/Desktop/Workspace/SQLite/Interop/OpenFlags.cs @@ -22,7 +22,7 @@ internal enum OpenFlags // SQLITE_OPEN_TEMP_JOURNAL = 0x00001000, /* VFS only */ // SQLITE_OPEN_SUBJOURNAL = 0x00002000, /* VFS only */ // SQLITE_OPEN_MASTER_JOURNAL = 0x00004000, /* VFS only */ - // SQLITE_OPEN_NOMUTEX = 0x00008000, /* Ok for sqlite3_open_v2() */ + SQLITE_OPEN_NOMUTEX = 0x00008000, /* Ok for sqlite3_open_v2() */ // SQLITE_OPEN_FULLMUTEX = 0x00010000, /* Ok for sqlite3_open_v2() */ SQLITE_OPEN_SHAREDCACHE = 0x00020000, /* Ok for sqlite3_open_v2() */ // SQLITE_OPEN_PRIVATECACHE = 0x00040000, /* Ok for sqlite3_open_v2() */ diff --git a/src/Workspaces/Core/Desktop/Workspace/SQLite/Interop/Result.cs b/src/Workspaces/Core/Desktop/Workspace/SQLite/Interop/Result.cs index c472f98799f0b3b087e731ac07e662a48c626b20..2b96fcb992748d6b5df743e99fc4fb1cec070b1f 100644 --- a/src/Workspaces/Core/Desktop/Workspace/SQLite/Interop/Result.cs +++ b/src/Workspaces/Core/Desktop/Workspace/SQLite/Interop/Result.cs @@ -16,7 +16,7 @@ internal enum Result // PERM = 3, /* Access permission denied */ // ABORT = 4, /* Callback routine requested an abort */ BUSY = 5, /* The database file is locked */ - // LOCKED = 6, /* A table in the database is locked */ + LOCKED = 6, /* A table in the database is locked */ NOMEM = 7, /* A malloc() failed */ // READONLY = 8, /* Attempt to write a readonly database */ // INTERRUPT = 9, /* Operation terminated by sqlite3_interrupt()*/ diff --git a/src/Workspaces/Core/Desktop/Workspace/SQLite/Interop/SqlConnection.cs b/src/Workspaces/Core/Desktop/Workspace/SQLite/Interop/SqlConnection.cs index 64c952acf640a24d7b28b7a926562bff0ed276da..c60dbf05061ebeb9f9900b45264c75c1dc8a3329 100644 --- a/src/Workspaces/Core/Desktop/Workspace/SQLite/Interop/SqlConnection.cs +++ b/src/Workspaces/Core/Desktop/Workspace/SQLite/Interop/SqlConnection.cs @@ -53,7 +53,7 @@ public static SqlConnection Create(IPersistentStorageFaultInjector faultInjector // Enable shared cache so that multiple connections inside of same process share cache // see https://sqlite.org/threadsafe.html for more detail - var flags = OpenFlags.SQLITE_OPEN_CREATE | OpenFlags.SQLITE_OPEN_READWRITE | OpenFlags.SQLITE_OPEN_SHAREDCACHE; + var flags = OpenFlags.SQLITE_OPEN_CREATE | OpenFlags.SQLITE_OPEN_READWRITE | OpenFlags.SQLITE_OPEN_NOMUTEX | OpenFlags.SQLITE_OPEN_SHAREDCACHE; var result = (Result)raw.sqlite3_open_v2(databasePath, out var handle, (int)flags, vfs: null); if (result != Result.OK) @@ -146,6 +146,7 @@ public void RunInTransaction(Action action) catch (SqlException ex) when (ex.Result == Result.FULL || ex.Result == Result.IOERR || ex.Result == Result.BUSY || + ex.Result == Result.LOCKED || ex.Result == Result.NOMEM) { // See documentation here: https://sqlite.org/lang_transaction.html @@ -156,6 +157,7 @@ public void RunInTransaction(Action action) // SQLITE_FULL: database or disk full // SQLITE_IOERR: disk I/ O error // SQLITE_BUSY: database in use by another process + // SQLITE_LOCKED: database in use by another connection in the same process // SQLITE_NOMEM: out or memory // It is recommended that applications respond to the errors listed above by diff --git a/src/Workspaces/Core/Desktop/Workspace/SQLite/SQLitePersistentStorage.Accessor.cs b/src/Workspaces/Core/Desktop/Workspace/SQLite/SQLitePersistentStorage.Accessor.cs index 4eb1c3e727001afb341dcbe0c03f47fb55ba926f..890d67c0f8b328f2204e44c63f594a120f40bc5c 100644 --- a/src/Workspaces/Core/Desktop/Workspace/SQLite/SQLitePersistentStorage.Accessor.cs +++ b/src/Workspaces/Core/Desktop/Workspace/SQLite/SQLitePersistentStorage.Accessor.cs @@ -60,25 +60,31 @@ public async Task ReadStreamAsync(TKey key, CancellationToken cancellati if (!Storage._shutdownTokenSource.IsCancellationRequested) { + bool haveDataId; + TDatabaseId dataId; using (var pooledConnection = Storage.GetPooledConnection()) { - var connection = pooledConnection.Connection; - if (TryGetDatabaseId(connection, key, out var dataId)) - { - // Ensure all pending document writes to this name are flushed to the DB so that - // we can find them below. - await FlushPendingWritesAsync(connection, key, cancellationToken).ConfigureAwait(false); + haveDataId = TryGetDatabaseId(pooledConnection.Connection, key, out dataId); + } - try + if (haveDataId) + { + // Ensure all pending document writes to this name are flushed to the DB so that + // we can find them below. + await FlushPendingWritesAsync(key, cancellationToken).ConfigureAwait(false); + + try + { + using (var pooledConnection = Storage.GetPooledConnection()) { // Lookup the row from the DocumentData table corresponding to our dataId. - return ReadBlob(connection, dataId); - } - catch (Exception ex) - { - StorageDatabaseLogger.LogException(ex); + return ReadBlob(pooledConnection.Connection, dataId); } } + catch (Exception ex) + { + StorageDatabaseLogger.LogException(ex); + } } } @@ -96,33 +102,36 @@ public async Task ReadStreamAsync(TKey key, CancellationToken cancellati if (!Storage._shutdownTokenSource.IsCancellationRequested) { + bool haveDataId; + TDatabaseId dataId; using (var pooledConnection = Storage.GetPooledConnection()) { // Determine the appropriate data-id to store this stream at. - if (TryGetDatabaseId(pooledConnection.Connection, key, out var dataId)) - { - var (bytes, length, pooled) = GetBytes(stream); + haveDataId = TryGetDatabaseId(pooledConnection.Connection, key, out dataId); + } - await AddWriteTaskAsync(key, con => + if (haveDataId) + { + var (bytes, length, pooled) = GetBytes(stream); + + await AddWriteTaskAsync(key, con => + { + InsertOrReplaceBlob(con, dataId, bytes, length); + if (pooled) { - InsertOrReplaceBlob(con, dataId, bytes, length); - if (pooled) - { - ReturnPooledBytes(bytes); - } - }, cancellationToken).ConfigureAwait(false); - - return true; - } + ReturnPooledBytes(bytes); + } + }, cancellationToken).ConfigureAwait(false); + + return true; } } return false; } - private Task FlushPendingWritesAsync(SqlConnection connection, TKey key, CancellationToken cancellationToken) - => Storage.FlushSpecificWritesAsync( - connection, _writeQueueKeyToWrites, _writeQueueKeyToWriteTask, GetWriteQueueKey(key), cancellationToken); + private Task FlushPendingWritesAsync(TKey key, CancellationToken cancellationToken) + => Storage.FlushSpecificWritesAsync(_writeQueueKeyToWrites, _writeQueueKeyToWriteTask, GetWriteQueueKey(key), cancellationToken); private Task AddWriteTaskAsync(TKey key, Action action, CancellationToken cancellationToken) => Storage.AddWriteTaskAsync(_writeQueueKeyToWrites, GetWriteQueueKey(key), action, cancellationToken); diff --git a/src/Workspaces/Core/Desktop/Workspace/SQLite/SQLitePersistentStorage.cs b/src/Workspaces/Core/Desktop/Workspace/SQLite/SQLitePersistentStorage.cs index 66236c2c777974f475bd4b349c511475b0d10496..94f7c5b592bf0ce8163e0833482860b081db7c22 100644 --- a/src/Workspaces/Core/Desktop/Workspace/SQLite/SQLitePersistentStorage.cs +++ b/src/Workspaces/Core/Desktop/Workspace/SQLite/SQLitePersistentStorage.cs @@ -233,6 +233,14 @@ private void CloseWorker() } } + /// + /// Gets an from the connection pool, or creates one if none are available. + /// + /// + /// Database connections have a large amount of overhead, and should be returned to the pool when they are no + /// longer in use. In particular, make sure to avoid letting a connection lease cross an + /// boundary, as it will prevent code in the asynchronous operation from using the existing connection. + /// private PooledConnection GetPooledConnection() => new PooledConnection(this, GetConnection()); @@ -243,6 +251,19 @@ public override void Initialize(Solution solution) { var connection = pooledConnection.Connection; + // Enable write-ahead logging to increase write performance by reducing amount of disk writes, + // by combining writes at checkpoint, salong with using sequential-only writes to populate the log. + // Also, WAL allows for relaxed ("normal") "synchronous" mode, see below. + connection.ExecuteCommand("pragma journal_mode=wal", throwOnError: false); + + // Set "synchronous" mode to "normal" instead of default "full" to reduce the amount of buffer flushing syscalls, + // significantly reducing both the blocked time and the amount of context switches. + // When coupled with WAL, this (according to https://sqlite.org/pragma.html#pragma_synchronous and + // https://www.sqlite.org/wal.html#performance_considerations) is unlikely to significantly affect durability, + // while significantly increasing performance, because buffer flushing is done for each checkpoint, instead of each + // transaction. While some writes can be lost, they are never reordered, and higher layers will recover from that. + connection.ExecuteCommand("pragma synchronous=normal", throwOnError: false); + // First, create all our tables connection.ExecuteCommand( $@"create table if not exists ""{StringInfoTableName}"" ( diff --git a/src/Workspaces/Core/Desktop/Workspace/SQLite/SQLitePersistentStorage_StringIds.cs b/src/Workspaces/Core/Desktop/Workspace/SQLite/SQLitePersistentStorage_StringIds.cs index e7cb0fe045f7d5fe152ea94134358c01442c8fae..279df4bd7482372fe9609f1382323117b05466a9 100644 --- a/src/Workspaces/Core/Desktop/Workspace/SQLite/SQLitePersistentStorage_StringIds.cs +++ b/src/Workspaces/Core/Desktop/Workspace/SQLite/SQLitePersistentStorage_StringIds.cs @@ -35,7 +35,7 @@ private bool TryFetchStringTable(SqlConnection connection) return true; } - catch (SqlException e) when (e.Result == Result.BUSY) + catch (SqlException e) when (e.Result == Result.BUSY || e.Result == Result.LOCKED) { // Couldn't get access to sql database to fetch the string table. // Try again later. diff --git a/src/Workspaces/Core/Desktop/Workspace/SQLite/SQLitePersistentStorage_WriteBatching.cs b/src/Workspaces/Core/Desktop/Workspace/SQLite/SQLitePersistentStorage_WriteBatching.cs index 1cc457ecd56df1cb326bef65e94df410f232b258..0301a327e4855e1d3b119d7bcd092c7c8dc3778c 100644 --- a/src/Workspaces/Core/Desktop/Workspace/SQLite/SQLitePersistentStorage_WriteBatching.cs +++ b/src/Workspaces/Core/Desktop/Workspace/SQLite/SQLitePersistentStorage_WriteBatching.cs @@ -51,16 +51,15 @@ internal partial class SQLitePersistentStorage } private async Task FlushSpecificWritesAsync( - SqlConnection connection, MultiDictionary> keyToWriteActions, Dictionary keyToWriteTask, - TKey key, CancellationToken cancellationToken) + TKey key, + CancellationToken cancellationToken) { var writesToProcess = ArrayBuilder>.GetInstance(); try { - await FlushSpecificWritesAsync( - connection, keyToWriteActions, keyToWriteTask, key, writesToProcess, cancellationToken).ConfigureAwait(false); + await FlushSpecificWritesAsync(keyToWriteActions, keyToWriteTask, key, writesToProcess, cancellationToken).ConfigureAwait(false); } finally { @@ -69,8 +68,9 @@ internal partial class SQLitePersistentStorage } private async Task FlushSpecificWritesAsync( - SqlConnection connection, MultiDictionary> keyToWriteActions, - Dictionary keyToWriteTask, TKey key, + MultiDictionary> keyToWriteActions, + Dictionary keyToWriteTask, + TKey key, ArrayBuilder> writesToProcess, CancellationToken cancellationToken) { @@ -97,7 +97,10 @@ internal partial class SQLitePersistentStorage // would be losing data. Debug.Assert(taskCompletionSource != null); - ProcessWriteQueue(connection, writesToProcess); + using (var pooledConnection = GetPooledConnection()) + { + ProcessWriteQueue(pooledConnection.Connection, writesToProcess); + } } catch (OperationCanceledException ex) {