Store sqlite writes to an in-memory DB first. Flush that db to disk at periodic intervals.#36970
Conversation
src/Workspaces/Core/Portable/Storage/SQLite/SQLitePersistentStorage.cs
Outdated
Show resolved
Hide resolved
| @@ -1,245 +1,80 @@ | |||
| // Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. | |||
|
|
|||
| using System; | |||
There was a problem hiding this comment.
this file's diff looks awful. the 'old' part is irrelevant. i recommend just looking at the 'new' side.
|
tagging @heejaechang @ericsink @sharwell @jasonmalinowski . @sharwell it would be especially great if we could test the perf out here. I recommend disabling whitespace diffs here. |
src/Workspaces/Core/Portable/Storage/SQLite/SQLitePersistentStorage.Accessor.cs
Outdated
Show resolved
Hide resolved
src/Workspaces/Core/Portable/Storage/SQLite/SQLitePersistentStorage.Accessor.cs
Outdated
Show resolved
Hide resolved
| if (fetchStringTable) | ||
| { | ||
| if (!TryFetchStringTable(connection)) | ||
| if (!TryFetchStringTableFromMainDB(connection)) |
There was a problem hiding this comment.
note: all the string table work is done with the main DB. this keeps us having a single consistent place for the string information. It's also not a place we need to optimize as the string writing is already done in a bulk in a single transactin.
| /// so that all reads for the queue observe any previously completed writes. | ||
| /// </summary> | ||
| private readonly Dictionary<TWriteQueueKey, Task> _writeQueueKeyToWriteTask = | ||
| new Dictionary<TWriteQueueKey, Task>(); |
There was a problem hiding this comment.
no more complex queues of keys-to-writes to maintain. Yaay!
| var (dataBytes, dataLength, dataPooled) = GetBytes(stream); | ||
|
|
||
| await AddWriteTaskAsync(key, con => | ||
| using (var pooledConnection = Storage.GetPooledConnection()) |
There was a problem hiding this comment.
note: we are no longer asynchronous. so we could consider just extending the range of the higher up pooled-connection using to just encompass this.
src/Workspaces/Core/Portable/Storage/SQLite/SQLitePersistentStorage.Accessor.cs
Outdated
Show resolved
Hide resolved
src/Workspaces/Core/Portable/Storage/SQLite/SQLitePersistentStorage.cs
Outdated
Show resolved
Hide resolved
|
|
||
| // Try to bulk populate all the IDs we'll need for strings/projects/documents. | ||
| // Bulk population is much faster than trying to do everything individually. | ||
| BulkPopulateIds(connection, solution, fetchStringTable); |
There was a problem hiding this comment.
these lines are the same from before. all that happened was i extracted out the table-initialization code since we want to initialize the main tables and initialize the tables in the write-cache.
|
|
||
| // Try to bulk populate all the IDs we'll need for strings/projects/documents. | ||
| // Bulk population is much faster than trying to do everything individually. | ||
| BulkPopulateIds(connection, solution, fetchStringTable); |
There was a problem hiding this comment.
note the logic from var fetched to here is the same as before. it's just that the lines above it were extracted out into EnsureTables
| } | ||
|
|
||
| private const int NumThreads = 10; | ||
| private const int NumThreads = 1; |
There was a problem hiding this comment.
local testing will not be merged in.
|
@sharwell have made the reg-key change. |
|
@CyrusNajmabadi I'll review after the next master→dev16.7-preview1 merge completes |
jasonmalinowski
left a comment
There was a problem hiding this comment.
So I was pretty good until I hit the SQLitePersistentStorage_Threading.cs file and then got terrified. It seems like we're doing a locking mechanism atop the transaction system in SQLite and that invalidated any understanding I had of this code. (Regrettably it was the last file I ended up looking at...)
How do we understand all that needs to be surrounded by PerformReadAsync and PerformWriteAsync? For example, why is the flushing not surrounded by a PerformWrite, because otherwise I could imagine it could be involved in a deadlock since it's going to be acquiring multiple read and write locks?
It's been awhile since I've played with SQL transactions but usually either the deadlock victim retries or you have additional tools to specify potential write locks that would be acquired to avoid the cycle. Are those not usable here, or is that not how SQLite chooses to work?
src/VisualStudio/CSharp/Test/PersistentStorage/SQLiteV2PersistentStorageTests.cs
Show resolved
Hide resolved
src/Workspaces/Core/Portable/Storage/SQLite/v2/SQLitePersistentStorageService.cs
Outdated
Show resolved
Hide resolved
| protected override string GetDatabaseFilePath(string workingFolderPath) | ||
| { | ||
| Contract.ThrowIfTrue(string.IsNullOrWhiteSpace(workingFolderPath)); | ||
| return Path.Combine(workingFolderPath, StorageExtension, nameof(v2), PersistentStorageFileName); |
There was a problem hiding this comment.
Does anything clean up the old databases? Or people potentially will have the old DB on their disk forever?
There was a problem hiding this comment.
No. And that's an intentional part of the design. i.e. they're designed to keep working even if you have side-x-side versions of VS (even with the same solution open in all of them).
There was a problem hiding this comment.
Would you imagine that if we delete the v1 we would then clean it up then?
There was a problem hiding this comment.
i mean... it's a bit tough. we could delete v1 and only ship vs in another release of vs. but maybe you have the different releases sxs.
maybe we could check if something hadn't been used in a few months... but i'm wary.
--
Note: with roslyn itself, our full cache size is 60mb. So while i'm not a huge fan of leaving these behind, i also don't view it as a substantial problem that needs pressing resolution.
| _select_star_from_string_table = $@"select * from {StringInfoTableName}"; | ||
| _insert_into_string_table_values_0 = $@"insert into {StringInfoTableName}(""{DataColumnName}"") values (?)"; | ||
| _select_star_from_string_table_where_0_limit_one = $@"select * from {StringInfoTableName} where (""{DataColumnName}"" = ?) limit 1"; |
There was a problem hiding this comment.
So these aren't also involved in the in-memory table?
There was a problem hiding this comment.
correct. the string table is dumped (and read) only from the on-disk db. they cahnge rarely enough (and the bulk is computed right at the beginning) that there's no need for the write cache.
There was a problem hiding this comment.
(we also write them in bulk anyways).
src/Workspaces/Core/Portable/Storage/SQLite/v2/SQLitePersistentStorage_FlushWrites.cs
Outdated
Show resolved
Hide resolved
| // Haven't been shutdown. Actually go and move any outstanding data to the real DB. | ||
| using var _ = storage.GetPooledConnection(out var connection); | ||
| storage.FlushInMemoryDataToDisk_MustRunUnderLock(connection); |
There was a problem hiding this comment.
We're doing the actual flushing under _flushGate? I would have imagined this was kept outside as otherwise any write to the in-memory is still going to block behind this writing. Presumably the SQLite transaction semantics already make it safe (otherwise this has bigger problems), and since this is running after the nulling-out of the task it means if this runs outside the gate there's a possibility of an extra flush job being queued, but that won't do anything so it's moot.
There was a problem hiding this comment.
great find. I think this is actually a pure bug (though not likely to hit). I think we likely should not have this flush gate tbh. we already have a mechanism to ensure exclusive access through the threading mechanism.
src/Workspaces/Core/Portable/Storage/SQLite/v2/SQLitePersistentStorage_Threading.cs
Show resolved
Hide resolved
src/Workspaces/Core/Portable/Storage/SQLite/v2/SQLitePersistentStorage_Threading.cs
Show resolved
Hide resolved
…tStorage.Accessor.cs Co-Authored-By: Jason Malinowski <jason@jason-m.com>
…into sqliteInMem
… reader/writer schedulers.
|
@jasonmalinowski thanks for the feedback. i think i've addressed it all now. fortunately, it likely made things safer and simpler, so that's great :) |
src/Workspaces/Core/Portable/Storage/SQLite/v2/SQLitePersistentStorage.cs
Outdated
Show resolved
Hide resolved
| } | ||
|
|
||
| // Create a connection to the DB and ensure it has tables for the types we care about. | ||
| using var _ = GetPooledConnection(out var connection); |
There was a problem hiding this comment.
this commit should be viewed with whitespe diff off.
src/Workspaces/Core/Portable/Storage/SQLite/v2/SQLitePersistentStorage.cs
Outdated
Show resolved
Hide resolved
jasonmalinowski
left a comment
There was a problem hiding this comment.
The .Wait()s that are spawning work and then immediately blocking during startup/shutdown do worry me a little bit on the perf side of thing, since if the thread pool is slammed during startup/shutdown that might cause some perf issues. If we have a reader writer lock that supports both sync and async lock acquisition though I can't find it, so I don't have a better idea. (And switching to sync acquisition would be worse since the main line stuff wouldn't be async anymore so that's even worse.)
From a conversation in gitter.im with @ericsink he helped me with the following idea: Instead of caching sqlite writes into .net memory, just write them to an in-memory sqlite db, and then periodically flush that in-memory db to disk.
This is similar to our existing approach, but makes things much simpler in terms of implementation.
Backstory:
When we moved to sqlite an early issue I ran into was that sqlite has issues with doing tons of tiny transactions. These transactions have some fixed cost on them, and so if you're hammering the DB with them (which we are), then you can bottleneck and cause lots of backup as things try to get through. All our features are insulated from teh actual underlying DB, and only get a key/value read/write store. So all of them often just keep trying to dump lots of data into the db exacerbating the system.
To alleviate this problem i introduced an in-memory write-cache where we staged a lot of the incoming writes to then write out to disk twice-a-second, under a single transaction. This improved perf roughly 50x, but came with a complexity cost. Specifically, we needed to write the in memory cache, and we had to make sure that 'reads' of data that were in the write cache would see the latest 'writes' (even if they hadn't made it to disk).
This new approach is much simpler: