Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

AccessHandlePoolVFS with multiple connections#157

rhashimoto started this conversation inIdeas
Discussion options

Update: the VFS described below has been renamed to OPFSCoopSyncVFS. The AccessHandlePoolVFS name is being kept for the original implementation.

The main drawback with AccessHandlePoolVFS is that it doesn't support multiple connections. Thereimplementation from scratch in the dev branch does have some support for multiple connections, but currently only with Chrome's "readwrite-unsafe" access handles and with the application handling the locking. I think I have a way to make things work without those caveats.

My approach revisitsRetryVFS where the VFS returns an error when it needs to perform an asynchronous operation. I implemented this at the time and it worked but it had these issues:

  1. It had problems with opening temporary files.
  2. It required the application to handle the returned error.
  3. It was slow.
  4. It didn't support multiple database access.

The starting point for the new approach would be the new AccessHandlePoolVFS, which establishes a separate directory for each connection to contain temporary files . This solves issue (1).

The internals of the API callstep() can be augmented to retry automatically on SQLITE_BUSY when a VFS provides a Promise that resolves the problem, which is done by taking a lock and acquiring the access handles. This mostly solves issue (2). The application will need to allow message handlers to run by yielding the task occasionally - this may occur naturally if the application itself is communicating with messages.

The newOPFSAdaptiveVFS supports multiple connections when "readwrite-unsafe" is not available by allowing only the connection with the exclusive lock to hold an open access handle. Opening and closing access handles is expensive, so OPFSAdaptiveVFS does this lazily, only when informed over BroadcastChannel that another connection is waiting for the lock. This pays the open/close penalties only when contention actually occurs. Applying this idea will reduce the impact of issue (3).

In theory, the basic idea should work with multiple main databases with another error and retry for each additional asynchronous operation so I think it's possible to solve issue (4). But I won't be that ambitious for a first attempt. This won't support accessing multiple databases in the same transaction.

I hope to modify the dev branch AccessHandlePoolVFS to try this out. If successful, the resulting VFS should have these properties:

  • Advantages
    • Runs on the synchronous build for better size/performance
    • Filesystem transparency for easy import/export
    • Allows multiple connections
  • Disadvantages
    • Only one database in a transaction (for now).
    • Likely much slower with contention.
    • Not compatible with the original AccessHandlePoolVFS.
You must be logged in to vote

Replies: 1 comment 1 reply

Comment options

rhashimoto
Feb 17, 2024
Maintainer Author

I hope to modify the dev branch AccessHandlePoolVFS to try this out.

The implementation was a bit tricky, but there is a shiny new AccessHandlePoolVFS (update: now a new VFS named OPFSCoopSyncVFS) in the dev branch. You can try it online atthis link on any current browser and with multiple tabs. The design sketch in the top post mostly works, except that open_v2(), prepare(), and step() all must allow retrying, and in at least one case SQLite will convert SQLITE_BUSY returned by the VFS to SQLITE_ERROR. The tricky parts were mostly in lazy resource release, i.e. only with multiple connection contention so things are still fast without contention. Isn't it ironic how being lazy creates the most work?

This VFS is really fast at read transactions with low contention. Here are results of doing as many read transactions as possible in one second with 1 database connection:

[12:16:57.195] build: default[12:16:57.195] config: [OPFSCoopSyncVFS][12:16:57.195] nWriters: 0[12:16:57.195] nReaders: 1[12:16:57.195] nSeconds: 1[12:18:21.155] launch workers[12:18:21.248] start[12:18:22.248] worker 0 reader 60892 iterations[12:18:22.248] complete

That's 60K read transactions per second. Note that this isn't a throughput test - SQLite is reading the database file on every transactions, but only enough to confirm that its cache is still valid. It should also be great with throughput, but this is more of a transaction overhead and latency test with minimal data volume.

Here's the same measurement with two connections:

[12:21:55.250] build: default[12:21:55.250] config: [OPFSCoopSyncVFS][12:21:55.250] nWriters: 0[12:21:55.250] nReaders: 2[12:21:55.250] nSeconds: 1[12:21:57.951] launch workers[12:21:58.047] start[12:21:59.047] worker 0 reader 777 iterations[12:21:59.049] worker 1 reader 776 iterations[12:21:59.049] complete

Now it's only doing about 1.5K read transactions per second with contention. It works with contention, but it's a lot slower.

For comparison, here is IDBBatchAtomicVFS (also reimplemented in the dev branch) with 1 connection:

[12:25:13.118] build: asyncify[12:25:13.118] config: IDBBatchAtomicVFS[12:25:13.118] nWriters: 0[12:25:13.118] nReaders: 1[12:25:13.118] nSeconds: 1[12:25:15.232] launch workers[12:25:15.341] start[12:25:16.341] worker 0 reader 2901 iterations[12:25:16.341] complete

And IDBBatchAtomicVFS with 2 connections:

[12:25:41.649] build: asyncify[12:25:41.649] config: IDBBatchAtomicVFS[12:25:41.649] nWriters: 0[12:25:41.649] nReaders: 2[12:25:41.649] nSeconds: 1[12:25:44.119] launch workers[12:25:44.221] start[12:25:45.221] worker 1 reader 2291 iterations[12:25:45.221] worker 0 reader 2315 iterations[12:25:45.221] complete

IDBBatchAtomicVFS is much slower than OPFSCoopSyncVFS without contention because (1) it doesn't use lazy locking, (2) OPFS access handles are faster than IndexedDB, and (3) synchronous WebAssembly is faster than Asyncify. But IDBBatchAtomicVFS actually speeds up in total transactions per second with more read connections because it allows concurrent reads.

OPFSCoopSyncVFS has a lot of write transaction overhead,as we already knew. Here's 1 writer and 2 writers:

[13:06:11.502] build: default[13:06:11.502] config: [OPFSCoopSyncVFS][13:06:11.502] nWriters: 1[13:06:11.502] nReaders: 0[13:06:11.502] nSeconds: 1[13:06:14.753] launch workers[13:06:14.848] start[13:06:15.850] worker 0 writer 97 iterations[13:06:15.850] complete
[13:07:10.422] build: default[13:07:10.422] config: [OPFSCoopSyncVFS][13:07:10.422] nWriters: 2[13:07:10.422] nReaders: 0[13:07:10.422] nSeconds: 1[13:07:12.270] launch workers[13:07:12.366] start[13:07:13.369] worker 0 writer 45 iterations[13:07:13.381] worker 1 writer 45 iterations[13:07:13.381] complete

Here is IDBBatchAtomicVFS with 1 and 2 writers for comparison:

[13:13:02.943] build: asyncify[13:13:02.943] config: IDBBatchAtomicVFS[13:13:02.943] nWriters: 1[13:13:02.943] nReaders: 0[13:13:02.943] nSeconds: 1[13:13:04.916] launch workers[13:13:05.033] start[13:13:06.033] worker 0 writer 226 iterations[13:13:06.033] complete
[13:13:36.050] build: asyncify[13:13:36.050] config: IDBBatchAtomicVFS[13:13:36.050] nWriters: 2[13:13:36.050] nReaders: 0[13:13:36.050] nSeconds: 1[13:13:38.265] launch workers[13:13:38.400] start[13:13:39.400] worker 0 writer 147 iterations[13:13:39.404] worker 1 writer 66 iterations[13:13:39.404] complete

So IDBBatchAtomicVFS is more than twice as fast, and this is with the default "full" synchronous setting. IDBBatchAtomicVFS also works with the "normal" setting which trades durability for even more performance:

[13:15:18.697] build: asyncify[13:15:18.697] config: IDBBatchAtomicVFS[13:15:18.698] nWriters: 1[13:15:18.698] nReaders: 0[13:15:18.698] nSeconds: 1[13:15:21.293] launch workers[13:15:21.419] start[13:15:22.419] worker 0 writer 610 iterations[13:15:22.419] complete
[13:16:28.455] build: asyncify[13:16:28.455] config: IDBBatchAtomicVFS[13:16:28.455] nWriters: 2[13:16:28.455] nReaders: 0[13:16:28.455] nSeconds: 1[13:16:30.468] launch workers[13:16:30.595] start[13:16:31.595] worker 1 writer 297 iterations[13:16:31.596] worker 0 writer 303 iterations[13:16:31.596] complete

What about contention between readers and writers? Here OPFSCoopSyncVFS with 3 readers and 1 writer:

[14:29:02.956] build: default[14:29:02.956] config: [OPFSCoopSyncVFS][14:29:02.956] nWriters: 1[14:29:02.956] nReaders: 3[14:29:02.956] nSeconds: 1[14:29:05.893] launch workers[14:29:06.010] start[14:29:07.012] worker 0 writer 81 iterations[14:29:07.013] worker 1 reader 79 iterations[14:29:07.015] worker 2 reader 79 iterations[14:29:07.015] worker 3 reader 79 iterations[14:29:07.015] complete

Here's IDBBatchAtomicVFS also with 3 readers and 1 writer (using relaxed durability settings):

[14:30:31.112] build: asyncify[14:30:31.112] config: IDBBatchAtomicVFS[14:30:31.112] nWriters: 1[14:30:31.112] nReaders: 3[14:30:31.112] nSeconds: 1[14:30:38.047] launch workers[14:30:38.189] start[14:30:39.190] worker 2 reader 715 iterations[14:30:39.190] worker 3 reader 707 iterations[14:30:39.190] worker 1 reader 719 iterations[14:30:39.190] worker 0 writer 329 iterations[14:30:39.190] complete

NewcomerFLOOR requires the new POSIX-style access handles only in Chrome for now, and always has relaxed durability, but really shines on mixed contention:

[14:32:11.244] build: asyncify[14:32:11.244] config: FLOOR[14:32:11.244] nWriters: 1[14:32:11.244] nReaders: 3[14:32:11.244] nSeconds: 1[14:32:21.340] launch workers[14:32:21.570] start[14:32:22.570] worker 1 reader 1048 iterations[14:32:22.582] worker 2 reader 1065 iterations[14:32:22.582] worker 3 reader 1060 iterations[14:32:22.583] worker 0 writer 592 iterations[14:32:22.583] complete

The takeaway is that different VFS implementations handle different workloads in different ways, and which one is "best" depends on how you plan to use it. OPFSCoopSyncVFS dazzles on reads with low contention. It has high write transaction overhead, so avoid it if you need optimal performance on large numbers of small write transactions. It's pretty cool that it works with multiple connections at all, but there are likely better options for high contention scenarios.

You must be logged in to vote
1 reply
@rhashimoto
Comment options

rhashimotoFeb 18, 2024
Maintainer Author

Implementation note: If you're running a long section of code in the same JavaScript task - e.g. you're calling a lot of synchronous WebAssembly functions - and you want to yield to allow background events like message delivery,don't do it like this:

awaitnewPromise(resolve=>setTimeout(resolve));

Instead do something like this:

constyieldTask=(function(){const{ port1, port2}=newMessageChannel();port1.start();port2.start();returnfunction(){returnnewPromise(function(resolve){port1.onmessage=resolve;port2.postMessage(null);});}})();...awaityieldTask();

The reason ishere:

As specified in theHTML standard, browsers will enforce a minimum timeout of 4 milliseconds once a nested call to setTimeout has been scheduled 5 times.

TIL When you're trying to time something that happens as fast as 60K times per second (0.017 ms), inserting a 4 ms yield is not helpful.

Sign up for freeto join this conversation on GitHub. Already have an account?Sign in to comment
Category
Ideas
Labels
None yet
1 participant
@rhashimoto

[8]ページ先頭

©2009-2025 Movatter.jp