Skip to content

Commit

Permalink
Merge pull request #183 from rhashimoto/OPFSAnyContextVFS
Browse files Browse the repository at this point in the history
Add OPFSAnyContextVFS
  • Loading branch information
rhashimoto authored Jun 11, 2024
2 parents a05f863 + 2f7b024 commit 8d7eda9
Show file tree
Hide file tree
Showing 9 changed files with 365 additions and 21 deletions.
5 changes: 5 additions & 0 deletions demo/demo-worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ const BUILDS = new Map([
vfsModule: '../src/examples/OPFSAdaptiveVFS.js',
vfsOptions: { lockPolicy: 'shared+hint' }
},
{
name: 'OPFSAnyContextVFS',
vfsModule: '../src/examples/OPFSAnyContextVFS.js',
vfsOptions: { lockPolicy: 'shared+hint' }
},
{
name: 'OPFSCoopSyncVFS',
vfsModule: '../src/examples/OPFSCoopSyncVFS.js',
Expand Down
1 change: 1 addition & 0 deletions demo/hello/hello.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import SQLiteESMFactory from '../../dist/wa-sqlite-async.mjs';
// FileSystemSyncAccessHandle (generally any OPFS VFS) will run only
// in a Worker.
import { IDBBatchAtomicVFS as MyVFS } from '../../src/examples/IDBBatchAtomicVFS.js';
// import { OPFSAnyContextVFS as MyVFS } from '../../src/examples/OPFSAnyContextVFS.js';
// import { AccessHandlePoolVFS as MyVFS } from '../src/examples/AccessHandlePoolVFS.js';
// import { OPFSAdaptiveVFS as MyVFS } from '../src/examples/OPFSAdaptiveVFS.js';
// import { OPFSCoopSyncVFS as MyVFS } from '../src/examples/OPFSCoopSyncVFS.js';
Expand Down
300 changes: 300 additions & 0 deletions src/examples/OPFSAnyContextVFS.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,300 @@
// Copyright 2024 Roy T. Hashimoto. All Rights Reserved.
import { FacadeVFS } from '../FacadeVFS.js';
import * as VFS from '../VFS.js';
import { WebLocksMixin } from '../WebLocksMixin.js';

/**
* @param {string} pathname
* @param {boolean} create
* @returns {Promise<[FileSystemDirectoryHandle, string]>}
*/
async function getPathComponents(pathname, create) {
const [_, directories, filename] = pathname.match(/[/]?(.*)[/](.*)$/);

let directoryHandle = await navigator.storage.getDirectory();
for (const directory of directories.split('/')) {
if (directory) {
directoryHandle = await directoryHandle.getDirectoryHandle(directory, { create });
}
}
return [directoryHandle, filename];
};

class File {
/** @type {string} */ pathname;
/** @type {number} */ flags;
/** @type {FileSystemFileHandle} */ fileHandle;
/** @type {Blob?} */ blob;
/** @type {FileSystemWritableFileStream?} */ writable;

constructor(pathname, flags) {
this.pathname = pathname;
this.flags = flags;
}
}

export class OPFSAnyContextVFS extends WebLocksMixin(FacadeVFS) {
/** @type {Map<number, File>} */ mapIdToFile = new Map();
lastError = null;

log = null;

static async create(name, module, options) {
const vfs = new OPFSAnyContextVFS(name, module, options);
await vfs.isReady();
return vfs;
}

constructor(name, module, options = {}) {
super(name, module, options);
}

getFilename(fileId) {
const pathname = this.mapIdToFile.get(fileId).pathname;
return `OPFS:${pathname}`
}

/**
* @param {string?} zName
* @param {number} fileId
* @param {number} flags
* @param {DataView} pOutFlags
* @returns {Promise<number>}
*/
async jOpen(zName, fileId, flags, pOutFlags) {
try {
const url = new URL(zName || Math.random().toString(36).slice(2), 'file://');
const pathname = url.pathname;

const file = new File(pathname, flags);
this.mapIdToFile.set(fileId, file);

const create = !!(flags & VFS.SQLITE_OPEN_CREATE);
const [directoryHandle, filename] = await getPathComponents(pathname, create);
file.fileHandle = await directoryHandle.getFileHandle(filename, { create });

pOutFlags.setInt32(0, flags, true);
return VFS.SQLITE_OK;
} catch (e) {
this.lastError = e;
return VFS.SQLITE_CANTOPEN;
}
}

/**
* @param {string} zName
* @param {number} syncDir
* @returns {Promise<number>}
*/
async jDelete(zName, syncDir) {
try {
const url = new URL(zName, 'file://');
const pathname = url.pathname;

const [directoryHandle, name] = await getPathComponents(pathname, false);
const result = directoryHandle.removeEntry(name, { recursive: false });
if (syncDir) {
await result;
}
return VFS.SQLITE_OK;
} catch (e) {
return VFS.SQLITE_IOERR_DELETE;
}
}

/**
* @param {string} zName
* @param {number} flags
* @param {DataView} pResOut
* @returns {Promise<number>}
*/
async jAccess(zName, flags, pResOut) {
try {
const url = new URL(zName, 'file://');
const pathname = url.pathname;

const [directoryHandle, dbName] = await getPathComponents(pathname, false);
const fileHandle = await directoryHandle.getFileHandle(dbName, { create: false });
pResOut.setInt32(0, 1, true);
return VFS.SQLITE_OK;
} catch (e) {
if (e.name === 'NotFoundError') {
pResOut.setInt32(0, 0, true);
return VFS.SQLITE_OK;
}
this.lastError = e;
return VFS.SQLITE_IOERR_ACCESS;
}
}

/**
* @param {number} fileId
* @returns {Promise<number>}
*/
async jClose(fileId) {
try {
const file = this.mapIdToFile.get(fileId);
this.mapIdToFile.delete(fileId);

await file.writable?.close();
if (file?.flags & VFS.SQLITE_OPEN_DELETEONCLOSE) {
const [directoryHandle, name] = await getPathComponents(file.pathname, false);
await directoryHandle.removeEntry(name, { recursive: false });
}
return VFS.SQLITE_OK;
} catch (e) {
return VFS.SQLITE_IOERR_DELETE;
}
}

/**
* @param {number} fileId
* @param {Uint8Array} pData
* @param {number} iOffset
* @returns {Promise<number>}
*/
async jRead(fileId, pData, iOffset) {
try {
const file = this.mapIdToFile.get(fileId);

if (file.writable) {
await file.writable.close();
file.writable = null;
file.blob = null;
}
if (!file.blob) {
file.blob = await file.fileHandle.getFile();
}

const bytesRead = await file.blob.slice(iOffset, iOffset + pData.byteLength)
.arrayBuffer()
.then(arrayBuffer => {
pData.set(new Uint8Array(arrayBuffer));
return arrayBuffer.byteLength;
});

if (bytesRead < pData.byteLength) {
pData.fill(0, bytesRead);
return VFS.SQLITE_IOERR_SHORT_READ;
}
return VFS.SQLITE_OK;
} catch (e) {
this.lastError = e;
return VFS.SQLITE_IOERR_READ;
}
}

/**
* @param {number} fileId
* @param {Uint8Array} pData
* @param {number} iOffset
* @returns {Promise<number>}
*/
async jWrite(fileId, pData, iOffset) {
try {
const file = this.mapIdToFile.get(fileId);

if (!file.writable) {
file.writable = await file.fileHandle.createWritable({ keepExistingData: true });
}
await file.writable.seek(iOffset);
await file.writable.write(pData.subarray());
file.blob = null;

return VFS.SQLITE_OK;
} catch (e) {
this.lastError = e;
return VFS.SQLITE_IOERR_WRITE;
}
}

/**
* @param {number} fileId
* @param {number} iSize
* @returns {Promise<number>}
*/
async jTruncate(fileId, iSize) {
try {
const file = this.mapIdToFile.get(fileId);

if (!file.writable) {
file.writable = await file.fileHandle.createWritable({ keepExistingData: true });
}
await file.writable.truncate(iSize);
file.blob = null;
return VFS.SQLITE_OK;
} catch (e) {
this.lastError = e;
return VFS.SQLITE_IOERR_TRUNCATE;
}
}

/**
* @param {number} fileId
* @param {number} flags
* @returns {Promise<number>}
*/
async jSync(fileId, flags) {
try {
const file = this.mapIdToFile.get(fileId);
await file.writable?.close();
file.writable = null;
file.blob = null;
return VFS.SQLITE_OK;
} catch (e) {
this.lastError = e;
return VFS.SQLITE_IOERR_FSYNC;
}
}

/**
* @param {number} fileId
* @param {DataView} pSize64
* @returns {Promise<number>}
*/
async jFileSize(fileId, pSize64) {
try {
const file = this.mapIdToFile.get(fileId);

if (file.writable) {
await file.writable.close();
file.writable = null;
file.blob = null;
}
if (!file.blob) {
file.blob = await file.fileHandle.getFile();
}
pSize64.setBigInt64(0, BigInt(file.blob.size), true);
return VFS.SQLITE_OK;
} catch (e) {
this.lastError = e;
return VFS.SQLITE_IOERR_FSTAT;
}
}

/**
* @param {number} fileId
* @param {number} lockType
* @returns {Promise<number>}
*/
async jLock(fileId, lockType) {
if (lockType === VFS.SQLITE_LOCK_SHARED) {
// Make sure to get a current readable view of the file.
const file = this.mapIdToFile.get(fileId);
file.blob = null;
}

// Call the actual unlock implementation.
return super.jLock(fileId, lockType);
}

jGetLastError(zBuf) {
if (this.lastError) {
console.error(this.lastError);
const outputArray = zBuf.subarray(0, zBuf.byteLength - 1);
const { written } = new TextEncoder().encodeInto(this.lastError.message, outputArray);
zBuf[written] = 0;
}
return VFS.SQLITE_OK
}
}
47 changes: 26 additions & 21 deletions src/examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,15 @@ IDBBatchAtomicVFS can trade durability for performance by setting `PRAGMA synchr

Changing the page size after the database is created is not supported (this is a change from pre-1.0).

### AccessHandlePoolVFS
This is an OPFS VFS that has all synchronous methods, i.e. they don't return Promises. This allows it to be used with a with a synchronous WebAssembly build and that has definite performance advantages.

AccessHandlePoolVFS works by pre-opening a number of access handles and associating them with SQLite open requests as needed. Operation is restricted to a single wa-sqlite instance, so multiple connections are not supported.

The silver lining to not allowing multiple connections is that there is no drawback to using `PRAGMA locking_mode=exclusive`. This in turn allows `PRAGMA journal_mode=wal`, which can significantly reduce write transaction overhead.

This VFS is not filesystem transparent, which means that its database files in OPFS cannot be directly imported and exported.

### OPFSAdaptiveVFS
This VFS is fundamentally a straightforward mapping of OPFS access handles to VFS methods, but adds two different techniques to support multiple connections.

Expand All @@ -25,14 +34,10 @@ A proposed change to OPFS allows there to be multiple open access handles on a f

If multiple open access handles are not supported then only journaling modes "delete" (default), "memory", and "off" are allowed.

### AccessHandlePoolVFS
This is an OPFS VFS that has all synchronous methods, i.e. they don't return Promises. This allows it to be used with a with a synchronous WebAssembly build and that has definite performance advantages.

AccessHandlePoolVFS works by pre-opening a number of access handles and associating them with SQLite open requests as needed. Operation is restricted to a single wa-sqlite instance, so multiple connections are not supported.

The silver lining to not allowing multiple connections is that there is no drawback to using `PRAGMA locking_mode=exclusive`. This in turn allows `PRAGMA journal_mode=wal`, which can significantly reduce write transaction overhead.
### OPFSAnyContextVFS
This VFS uses the slower File and FileSystemWritableFileStream OPFS APIs instead of synchronous access handles. This should allow it to be used on any context, i.e. not just a dedicated Worker.

This VFS is not filesystem transparent, which means that its database files in OPFS cannot be directly imported and exported.
Read performance should be only somewhat slower, and might even be better than messaging overhead to communicate with a Worker. Write performance, however, will be very bad and will be increasingly worse as the file grows. It is recommended to use it only for read-only or nearly read-only databases.

### OPFSCoopSyncVFS
This VFS is a synchronous OPFS VFS (like AccessHandlePoolVFS) that allows multiple connections and is filesystem transparent (unlike AccessHandlePoolVFS).
Expand All @@ -54,21 +59,21 @@ Changing the page size after the database is created is not supported. Not files

## VFS Comparison

||MemoryVFS|MemoryAsyncVFS|IDBBatchAtomicVFS|OPFSAdaptiveVFS|AccessHandlePoolVFS|OPFSCoopSyncVFS|OPFSPermutedVFS|
|-|-|-|-|-|-|-|-|
|Storage|RAM|RAM|IndexedDB|OPFS|OPFS|OPFS|OPFS/IndexedDB|
|Synchronous build||:x:|:x:|:x:|||:x:|
||MemoryVFS|MemoryAsyncVFS|IDBBatchAtomicVFS|OPFSAdaptiveVFS|AccessHandlePoolVFS|OPFSAnyContextVFS|OPFSCoopSyncVFS|OPFSPermutedVFS|
|-|-|-|-|-|-|-|-|-|
|Storage|RAM|RAM|IndexedDB|OPFS|OPFS|OPFS|OPFS|OPFS/IndexedDB|
|Synchronous build||:x:|:x:|:x:||:x:||:x:|
|Asyncify build||||||||
|JSPI build||||||||
|Contexts|All|All|All|Worker|Worker|Worker|Worker|
|Multiple connections|:x:|:x:|||:x:||[^1]|
|Full durability||||||||
|Relaxed durability|:x:|:x:||:x:|:x:|:x:||
|Filesystem transparency|:x:|:x:|:x:||:x:||:x:[^2]|
|Write-ahead logging|:x:|:x:|:x:|:x:|:x:|:x:|[^3]|
|Multi-database transactions||||||:x:||
|Change page size|||:x:||||:x:|
|No COOP/COEP requirements||||||||
|JSPI build|||||||||
|Contexts|All|All|All|Worker|Worker|All|Worker|Worker|
|Multiple connections|:x:|:x:|||:x:|||[^1]|
|Full durability|||||||||
|Relaxed durability|:x:|:x:||:x:|:x:|:x:|:x:||
|Filesystem transparency|:x:|:x:|:x:||:x:|||:x:[^2]|
|Write-ahead logging|:x:|:x:|:x:|:x:|:x:|:x:|:x:|[^3]|
|Multi-database transactions|||||||:x:||
|Change page size|||:x:|||||:x:|
|No COOP/COEP requirements|||||||||

[^1]: Requires FileSystemSyncAccessHandle readwrite-unsafe locking mode support.
[^2]: Only filesystem transparent immediately after VACUUM.
Expand Down
File renamed without changes.
Loading

0 comments on commit 8d7eda9

Please sign in to comment.