mirror of
https://github.com/sqlite/sqlite.git
synced 2025-07-29 08:01:23 +03:00
opfs-sahpool VFS now seems to work, in that it runs fine (and blazingly fast) in speedtest1. Add sqlite3.config options for the high-level configurable parts of opfs-sahpool.
FossilOrigin-Name: 5d92d5f4d8ac4cfa37ba473e5cc861628b783bbf1ae4d138bcae8b9d6cc6e798
This commit is contained in:
@ -10,12 +10,9 @@
|
||||
|
||||
***********************************************************************
|
||||
|
||||
INCOMPLETE! WORK IN PROGRESS!
|
||||
|
||||
This file holds an experimental sqlite3_vfs backed by OPFS storage
|
||||
which uses a different implementation strategy than the "opfs"
|
||||
VFS. This one is a port of Roy Hashimoto's OPFS SyncAccessHandle
|
||||
pool:
|
||||
This file holds a sqlite3_vfs backed by OPFS storage which uses a
|
||||
different implementation strategy than the "opfs" VFS. This one is a
|
||||
port of Roy Hashimoto's OPFS SyncAccessHandle pool:
|
||||
|
||||
https://github.com/rhashimoto/wa-sqlite/blob/master/src/examples/AccessHandlePoolVFS.js
|
||||
|
||||
@ -28,16 +25,17 @@
|
||||
|
||||
https://sqlite.org/forum/forumpost/e140d84e71
|
||||
|
||||
Primary differences from the original "opfs" VFS include:
|
||||
Primary differences from the "opfs" VFS include:
|
||||
|
||||
- This one avoids the need for a sub-worker to synchronize
|
||||
communication between the synchronous C API and the only-partly
|
||||
synchronous OPFS API.
|
||||
communication between the synchronous C API and the
|
||||
only-partly-synchronous OPFS API.
|
||||
|
||||
- It does so by opening a fixed number of OPFS files at
|
||||
library-level initialization time, obtaining SyncAccessHandles to
|
||||
each, and manipulating those handles via the synchronous sqlite3_vfs
|
||||
interface.
|
||||
interface. If it cannot open them (e.g. they are already opened by
|
||||
another tab) then the VFS will not be installed.
|
||||
|
||||
- Because of that, this one lacks all library-level concurrency
|
||||
support.
|
||||
@ -45,20 +43,13 @@
|
||||
- Also because of that, it does not require the SharedArrayBuffer,
|
||||
so can function without the COOP/COEP HTTP response headers.
|
||||
|
||||
- It can hypothetically support Safari 16.4+, whereas the "opfs"
|
||||
VFS requires v17 due to a bug in 16.x which makes it incompatible
|
||||
with that VFS.
|
||||
- It can hypothetically support Safari 16.4+, whereas the "opfs" VFS
|
||||
requires v17 due to a subworker/storage bug in 16.x which makes it
|
||||
incompatible with that VFS.
|
||||
|
||||
- This VFS requires the "semi-fully-sync" FileSystemSyncAccessHandle
|
||||
(hereafter "SAH") APIs released with Chrome v108. There is
|
||||
unfortunately no known programmatic way to determine whether a given
|
||||
API is from that release or newer without actually calling it and
|
||||
checking whether one of the "fully-sync" functions returns a Promise
|
||||
(in which case it's the older version). (Reminder to self: when
|
||||
opening up the initial pool of files, we can close() the first one
|
||||
we open and see if close() returns a Promise. If it does, it's the
|
||||
older version so fail VFS initialization. If it doesn't, re-open it.)
|
||||
|
||||
(hereafter "SAH") APIs released with Chrome v108. If that API
|
||||
is not detected, the VFS is not registered.
|
||||
*/
|
||||
'use strict';
|
||||
globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
|
||||
@ -71,7 +62,7 @@ const installOpfsVfs = async function(sqlite3){
|
||||
return Promise.reject(new Error("Missing required OPFS APIs."));
|
||||
}
|
||||
return new Promise(async function(promiseResolve, promiseReject_){
|
||||
const verbosity = 3;
|
||||
const verbosity = 2;
|
||||
const loggers = [
|
||||
sqlite3.config.error,
|
||||
sqlite3.config.warn,
|
||||
@ -103,7 +94,8 @@ const installOpfsVfs = async function(sqlite3){
|
||||
const HEADER_OFFSET_FLAGS = HEADER_MAX_PATH_SIZE;
|
||||
const HEADER_OFFSET_DIGEST = HEADER_CORPUS_SIZE;
|
||||
const HEADER_OFFSET_DATA = SECTOR_SIZE;
|
||||
const DEFAULT_CAPACITY = 6;
|
||||
const DEFAULT_CAPACITY =
|
||||
sqlite3.config['opfs-sahpool.defaultCapacity'] || 6;
|
||||
/* Bitmask of file types which may persist across sessions.
|
||||
SQLITE_OPEN_xyz types not listed here may be inadvertently
|
||||
left in OPFS but are treated as transient by this VFS and
|
||||
@ -114,7 +106,9 @@ const installOpfsVfs = async function(sqlite3){
|
||||
capi.SQLITE_OPEN_SUPER_JOURNAL |
|
||||
capi.SQLITE_OPEN_WAL /* noting that WAL support is
|
||||
unavailable in the WASM build.*/;
|
||||
const pDVfs = capi.sqlite3_vfs_find(null)/*default VFS*/;
|
||||
/* We fetch the default VFS so that we can inherit some
|
||||
methods from it. */
|
||||
const pDVfs = capi.sqlite3_vfs_find(null);
|
||||
const dVfs = pDVfs
|
||||
? new capi.sqlite3_vfs(pDVfs)
|
||||
: null /* dVfs will be null when sqlite3 is built with
|
||||
@ -122,68 +116,91 @@ const installOpfsVfs = async function(sqlite3){
|
||||
opfsVfs.$iVersion = 2/*yes, two*/;
|
||||
opfsVfs.$szOsFile = capi.sqlite3_file.structInfo.sizeof;
|
||||
opfsVfs.$mxPathname = HEADER_MAX_PATH_SIZE;
|
||||
opfsVfs.$zName = wasm.allocCString("opfs-sahpool");
|
||||
log('opfsVfs.$zName =',opfsVfs.$zName);
|
||||
opfsVfs.addOnDispose(
|
||||
'$zName', opfsVfs.$zName,
|
||||
'cleanup default VFS wrapper', ()=>(dVfs ? dVfs.dispose() : null)
|
||||
opfsVfs.$zName = wasm.allocCString("opfs-sahpool"),
|
||||
()=>(dVfs ? dVfs.dispose() : null)
|
||||
);
|
||||
|
||||
const getFilename = false
|
||||
? (ndx)=>'sahpool-'+('00'+ndx).substr(-3)
|
||||
: ()=>Math.random().toString(36).slice(2)
|
||||
/**
|
||||
Returns short a string of random alphanumeric characters
|
||||
suitable for use as a random filename.
|
||||
*/
|
||||
const getRandomName = ()=>Math.random().toString(36).slice(2);
|
||||
|
||||
/**
|
||||
All state for the VFS.
|
||||
*/
|
||||
const SAHPool = Object.assign(Object.create(null),{
|
||||
/* OPFS dir in which VFS metadata is stored. */
|
||||
vfsDir: ".sqlite3-opfs-sahpool",
|
||||
vfsDir: sqlite3.config['vfs:opfs-sahpool:dir']
|
||||
|| ".sqlite3-opfs-sahpool",
|
||||
/* Directory handle to this.vfsDir. */
|
||||
dirHandle: undefined,
|
||||
/* Maps OPFS access handles to their opaque file names. */
|
||||
mapAH2Name: new Map(),
|
||||
mapPath2AH: new Map(),
|
||||
availableAH: new Set(),
|
||||
mapId2File: new Map(),
|
||||
getCapacity: function(){return this.mapAH2Name.size},
|
||||
getFileCount: function(){return this.mapPath2AH.size},
|
||||
/* Maps SAHs to their opaque file names. */
|
||||
mapSAHToName: new Map(),
|
||||
/* Maps client-side file names to SAHs. */
|
||||
mapPathToSAH: new Map(),
|
||||
/* Set of currently-unused SAHs. */
|
||||
availableSAH: new Set(),
|
||||
/* Maps (sqlite3_file*) to xOpen's file objects. */
|
||||
mapIdToFile: new Map(),
|
||||
/* Current pool capacity. */
|
||||
getCapacity: function(){return this.mapSAHToName.size},
|
||||
/* Current number of in-use files from pool. */
|
||||
getFileCount: function(){return this.mapPathToSAH.size},
|
||||
/**
|
||||
Adds n files to the pool's capacity. This change is
|
||||
persistent across settings. Returns a Promise which resolves
|
||||
to the new capacity.
|
||||
*/
|
||||
addCapacity: async function(n){
|
||||
const cap = this.getCapacity();
|
||||
for(let i = cap; i < cap+n; ++i){
|
||||
const name = getFilename(i)
|
||||
/* Reminder: because of how removeCapacity() works, we
|
||||
really want random names. At this point in the dev
|
||||
process that fills up the OPFS with randomly-named files
|
||||
each time the page is reloaded, so delay the return to
|
||||
random names until we've gotten far enough to eliminate
|
||||
that problem. */;
|
||||
const name = getRandomName();
|
||||
const h = await this.dirHandle.getFileHandle(name, {create:true});
|
||||
const ah = await h.createSyncAccessHandle();
|
||||
this.mapAH2Name.set(ah,name);
|
||||
this.mapSAHToName.set(ah,name);
|
||||
this.setAssociatedPath(ah, '', 0);
|
||||
}
|
||||
return i;
|
||||
},
|
||||
/**
|
||||
Removes n entries from the pool's current capacity
|
||||
if possible. It can only remove currently-unallocated
|
||||
files. Returns a Promise resolving to the number of
|
||||
removed files.
|
||||
*/
|
||||
removeCapacity: async function(n){
|
||||
let nRm = 0;
|
||||
for(const ah of Array.from(this.availableAH)){
|
||||
for(const ah of Array.from(this.availableSAH)){
|
||||
if(nRm === n || this.getFileCount() === this.getCapacity()){
|
||||
break;
|
||||
}
|
||||
const name = this.mapAH2Name.get(ah);
|
||||
const name = this.mapSAHToName.get(ah);
|
||||
ah.close();
|
||||
await this.dirHandle.removeEntry(name);
|
||||
this.mapAH2Name.delete(ah);
|
||||
this.availableAH.delete(ah);
|
||||
this.mapSAHToName.delete(ah);
|
||||
this.availableSAH.delete(ah);
|
||||
++nRm;
|
||||
}
|
||||
return nRm;
|
||||
},
|
||||
/**
|
||||
Releases all currently-opened SAHs.
|
||||
*/
|
||||
releaseAccessHandles: function(){
|
||||
for(const ah of this.mapAH2Name.keys()) ah.close();
|
||||
this.mapAH2Name.clear();
|
||||
this.mapPath2AH.clear();
|
||||
this.availableAH.clear();
|
||||
for(const ah of this.mapSAHToName.keys()) ah.close();
|
||||
this.mapSAHToName.clear();
|
||||
this.mapPathToSAH.clear();
|
||||
this.availableSAH.clear();
|
||||
},
|
||||
/**
|
||||
Opens all files under this.vfsDir/this.dirHandle and acquires
|
||||
a SAH for each. returns a Promise which resolves to no value
|
||||
but completes once all SAHs are acquired. If acquiring an SAH
|
||||
throws, SAHPool.$error will contain the corresponding
|
||||
exception.
|
||||
*/
|
||||
acquireAccessHandles: async function(){
|
||||
const files = [];
|
||||
for await (const [name,h] of this.dirHandle){
|
||||
@ -192,21 +209,35 @@ const installOpfsVfs = async function(sqlite3){
|
||||
}
|
||||
}
|
||||
await Promise.all(files.map(async ([name,h])=>{
|
||||
const ah = await h.createSyncAccessHandle()
|
||||
try{
|
||||
const ah = await h.createSyncAccessHandle()
|
||||
/*TODO: clean up and fail vfs init on error*/;
|
||||
this.mapAH2Name.set(ah, name);
|
||||
const path = this.getAssociatedPath(ah);
|
||||
if(path){
|
||||
this.mapPath2AH.set(path, ah);
|
||||
}else{
|
||||
this.availableAH.add(ah);
|
||||
this.mapSAHToName.set(ah, name);
|
||||
const path = this.getAssociatedPath(ah);
|
||||
if(path){
|
||||
this.mapPathToSAH.set(path, ah);
|
||||
}else{
|
||||
this.availableSAH.add(ah);
|
||||
}
|
||||
}catch(e){
|
||||
SAHPool.storeErr(e);
|
||||
throw e;
|
||||
}
|
||||
}));
|
||||
},
|
||||
gapBody: new Uint8Array(HEADER_CORPUS_SIZE),
|
||||
/** Buffer used by [sg]etAssociatedPath(). */
|
||||
apBody: new Uint8Array(HEADER_CORPUS_SIZE),
|
||||
textDecoder: new TextDecoder(),
|
||||
textEncoder: new TextEncoder(),
|
||||
/**
|
||||
Given an SAH, returns the client-specified name of
|
||||
that file by extracting it from the SAH's header.
|
||||
|
||||
On error, it disassociates SAH from the pool and
|
||||
returns an empty string.
|
||||
*/
|
||||
getAssociatedPath: function(sah){
|
||||
const body = this.gapBody;
|
||||
const body = this.apBody;
|
||||
sah.read(body, {at: 0});
|
||||
// Delete any unexpected files left over by previous
|
||||
// untimely errors...
|
||||
@ -227,8 +258,8 @@ const installOpfsVfs = async function(sqlite3){
|
||||
// Valid digest
|
||||
const pathBytes = body.findIndex((v)=>0===v);
|
||||
if(0===pathBytes){
|
||||
// This file is unassociated, so ensure that it's empty
|
||||
// to avoid leaving stale db data laying around.
|
||||
// This file is unassociated, so truncate it to avoid
|
||||
// leaving stale db data laying around.
|
||||
sah.truncate(HEADER_OFFSET_DATA);
|
||||
}
|
||||
return this.textDecoder.decode(body.subarray(0,pathBytes));
|
||||
@ -239,9 +270,12 @@ const installOpfsVfs = async function(sqlite3){
|
||||
return '';
|
||||
}
|
||||
},
|
||||
textEncoder: new TextEncoder(),
|
||||
/**
|
||||
Stores the given client-defined path and SQLITE_OPEN_xyz
|
||||
flags into the given SAH.
|
||||
*/
|
||||
setAssociatedPath: function(sah, path, flags){
|
||||
const body = this.gapBody;
|
||||
const body = this.apBody;
|
||||
const enc = this.textEncoder.encodeInto(path, body);
|
||||
if(HEADER_MAX_PATH_SIZE <= enc.written){
|
||||
toss("Path too long:",path);
|
||||
@ -256,15 +290,19 @@ const installOpfsVfs = async function(sqlite3){
|
||||
sah.flush();
|
||||
|
||||
if(path){
|
||||
this.mapPath2AH.set(path, sah);
|
||||
this.availableAH.delete(sah);
|
||||
this.mapPathToSAH.set(path, sah);
|
||||
this.availableSAH.delete(sah);
|
||||
}else{
|
||||
// This is not a persistent file, so eliminate the contents.
|
||||
sah.truncate(HEADER_OFFSET_DATA);
|
||||
this.mapPath2AH.delete(path);
|
||||
this.availableAH.add(sah);
|
||||
this.mapPathToSAH.delete(path);
|
||||
this.availableSAH.add(sah);
|
||||
}
|
||||
},
|
||||
/**
|
||||
Computes a digest for the given byte array and
|
||||
returns it as a two-element Uint32Array.
|
||||
*/
|
||||
computeDigest: function(byteArray){
|
||||
if(!byteArray[0]){
|
||||
// Deleted file
|
||||
@ -290,41 +328,71 @@ const installOpfsVfs = async function(sqlite3){
|
||||
this.releaseAccessHandles();
|
||||
await this.acquireAccessHandles();
|
||||
},
|
||||
/**
|
||||
Returns the pathname part of the given argument,
|
||||
which may be any of:
|
||||
|
||||
- a URL object
|
||||
- A JS string representing a file name
|
||||
- Wasm C-string representing a file name
|
||||
*/
|
||||
getPath: function(arg) {
|
||||
if(wasm.isPtr(arg)) arg = wasm.cstrToJs(arg);
|
||||
return ((arg instanceof URL)
|
||||
? arg
|
||||
: new URL(arg, 'file://localhost/')).pathname;
|
||||
},
|
||||
/**
|
||||
Removes the association of the given client-specified file
|
||||
name (JS string) from the pool.
|
||||
*/
|
||||
deletePath: function(path) {
|
||||
const sah = this.mapPath2AH.get(path);
|
||||
const sah = this.mapPathToSAH.get(path);
|
||||
if(sah) {
|
||||
// Un-associate the SQLite path from the OPFS file.
|
||||
this.setAssociatedPath(sah, '', 0);
|
||||
}
|
||||
},
|
||||
/**
|
||||
Sets e as this object's current error. Pass a falsy
|
||||
(or no) value to clear it.
|
||||
*/
|
||||
storeErr: function(e){
|
||||
return this.$error = e;
|
||||
},
|
||||
/**
|
||||
Pops this object's Error object and returns
|
||||
it (a falsy value if no error is set).
|
||||
*/
|
||||
popErr: function(e){
|
||||
const rc = this.$error;
|
||||
this.$error = undefined;
|
||||
return rc;
|
||||
}
|
||||
})/*SAHPool*/;
|
||||
sqlite3.SAHPool = SAHPool/*only for testing*/;
|
||||
// Much, much more TODO...
|
||||
/**
|
||||
Impls for the sqlite3_io_methods methods. Maintenance reminder:
|
||||
members are in alphabetical order to simplify finding them.
|
||||
*/
|
||||
const ioSyncWrappers = {
|
||||
xCheckReservedLock: function(pFile,pOut){
|
||||
SAHPool.storeErr();
|
||||
return 0;
|
||||
},
|
||||
xClose: function(pFile){
|
||||
const file = SAHPool.mapId2File.get(pFile);
|
||||
SAHPool.storeErr();
|
||||
const file = SAHPool.mapIdToFile.get(pFile);
|
||||
if(file) {
|
||||
try{
|
||||
log(`xClose ${file.path}`);
|
||||
file.sah.flush();
|
||||
SAHPool.mapId2File.delete(pFIle);
|
||||
SAHPool.mapIdToFile.delete(pFIle);
|
||||
if(file.flags & capi.SQLITE_OPEN_DELETEONCLOSE){
|
||||
SAHPool.deletePath(file.path);
|
||||
}
|
||||
}catch(e){
|
||||
SAHPool.storeErr(e);
|
||||
error("xClose() failed:",e.message);
|
||||
return capi.SQLITE_IOERR;
|
||||
}
|
||||
@ -338,18 +406,20 @@ const installOpfsVfs = async function(sqlite3){
|
||||
return capi.SQLITE_NOTFOUND;
|
||||
},
|
||||
xFileSize: function(pFile,pSz64){
|
||||
const file = SAHPool.mapId2File(pFile);
|
||||
const file = SAHPool.mapIdToFile(pFile);
|
||||
const size = file.sah.getSize() - HEADER_OFFSET_DATA;
|
||||
//log(`xFileSize ${file.path} ${size}`);
|
||||
wasm.poke64(pSz64, BigInt(size));
|
||||
return 0;
|
||||
},
|
||||
xLock: function(pFile,lockType){
|
||||
SAHPool.storeErr();
|
||||
let rc = capi.SQLITE_IOERR;
|
||||
return rc;
|
||||
},
|
||||
xRead: function(pFile,pDest,n,offset64){
|
||||
const file = SAHPool.mapId2File.get(pFile);
|
||||
SAHPool.storeErr();
|
||||
const file = SAHPool.mapIdToFile.get(pFile);
|
||||
log(`xRead ${file.path} ${n} ${offset64}`);
|
||||
try {
|
||||
const nRead = file.sah.read(
|
||||
@ -361,6 +431,7 @@ const installOpfsVfs = async function(sqlite3){
|
||||
}
|
||||
return 0;
|
||||
}catch(e){
|
||||
SAHPool.storeErr(e);
|
||||
error("xRead() failed:",e.message);
|
||||
return capi.SQLITE_IOERR;
|
||||
}
|
||||
@ -369,23 +440,27 @@ const installOpfsVfs = async function(sqlite3){
|
||||
return SECTOR_SIZE;
|
||||
},
|
||||
xSync: function(pFile,flags){
|
||||
const file = SAHPool.mapId2File.get(pFile);
|
||||
SAHPool.storeErr();
|
||||
const file = SAHPool.mapIdToFile.get(pFile);
|
||||
//log(`xSync ${file.path} ${flags}`);
|
||||
try{
|
||||
file.sah.flush();
|
||||
return 0;
|
||||
}catch(e){
|
||||
SAHPool.storeErr(e);
|
||||
error("xSync() failed:",e.message);
|
||||
return capi.SQLITE_IOERR;
|
||||
}
|
||||
},
|
||||
xTruncate: function(pFile,sz64){
|
||||
const file = SAHPool.mapId2File.get(pFile);
|
||||
SAHPool.storeErr();
|
||||
const file = SAHPool.mapIdToFile.get(pFile);
|
||||
//log(`xTruncate ${file.path} ${iSize}`);
|
||||
try{
|
||||
file.sah.truncate(HEADER_OFFSET_DATA + Number(sz64));
|
||||
return 0;
|
||||
}catch(e){
|
||||
SAHPool.storeErr(e);
|
||||
error("xTruncate() failed:",e.message);
|
||||
return capi.SQLITE_IOERR;
|
||||
}
|
||||
@ -394,15 +469,16 @@ const installOpfsVfs = async function(sqlite3){
|
||||
return capi.SQLITE_IOERR;
|
||||
},*/
|
||||
xWrite: function(pFile,pSrc,n,offset64){
|
||||
const file = SAHPool.mapId2File(pFile);
|
||||
SAHPool.storeErr();
|
||||
const file = SAHPool.mapIdToFile(pFile);
|
||||
//log(`xWrite ${file.path} ${n} ${offset64}`);
|
||||
try{
|
||||
const nBytes = file.sah.write(
|
||||
pSrc, { at: HEADER_OFFSET_DATA + Number(offset64) }
|
||||
);
|
||||
return nBytes === n ? 0 : capi.SQLITE_IOERR;
|
||||
return 0;
|
||||
}catch(e){
|
||||
SAHPool.storeErr(e);
|
||||
error("xWrite() failed:",e.message);
|
||||
return capi.SQLITE_IOERR;
|
||||
}
|
||||
@ -415,8 +491,13 @@ const installOpfsVfs = async function(sqlite3){
|
||||
*/
|
||||
const vfsSyncWrappers = {
|
||||
xAccess: function(pVfs,zName,flags,pOut){
|
||||
const name = this.getPath(zName);
|
||||
wasm.poke32(pOut, SAHPool.mapPath2AH.has(name) ? 1 : 0);
|
||||
SAHPool.storeErr();
|
||||
try{
|
||||
const name = this.getPath(zName);
|
||||
wasm.poke32(pOut, SAHPool.mapPathToSAH.has(name) ? 1 : 0);
|
||||
}catch(e){
|
||||
/*ignored*/;
|
||||
}
|
||||
return 0;
|
||||
},
|
||||
xCurrentTime: function(pVfs,pOut){
|
||||
@ -430,10 +511,12 @@ const installOpfsVfs = async function(sqlite3){
|
||||
return 0;
|
||||
},
|
||||
xDelete: function(pVfs, zName, doSyncDir){
|
||||
SAHPool.storeErr();
|
||||
try{
|
||||
SAHPool.deletePath(SAHPool.getPath(zName));
|
||||
return 0;
|
||||
}catch(e){
|
||||
SAHPool.storeErr(e);
|
||||
error("Error xDelete()ing file:",e.message);
|
||||
return capi.SQLITE_IOERR_DELETE;
|
||||
}
|
||||
@ -443,15 +526,53 @@ const installOpfsVfs = async function(sqlite3){
|
||||
return i<nOut ? 0 : capi.SQLITE_CANTOPEN;
|
||||
},
|
||||
xGetLastError: function(pVfs,nOut,pOut){
|
||||
/* TODO: store exception state somewhere and serve
|
||||
it from here. */
|
||||
warn("OPFS xGetLastError() has nothing sensible to return.");
|
||||
const e = SAHPool.popErr();
|
||||
if(e){
|
||||
const scope = wasm.scopedAllocPush();
|
||||
try{
|
||||
const [cMsg, n] = wasm.scopedAllocCString(e.message, true);
|
||||
wasm.cstrncpy(pOut, cMsg, nOut);
|
||||
if(n > nOut) wasm.poke8(pOut + nOut - 1, 0);
|
||||
}catch(e){
|
||||
return capi.SQLITE_NOMEM;
|
||||
}finally{
|
||||
wasm.scopedAllocPop(scope);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
},
|
||||
//xSleep is optionally defined below
|
||||
xOpen: function f(pVfs, zName, pFile, flags, pOutFlags){
|
||||
let rc = capi.SQLITE_ERROR;
|
||||
return rc;
|
||||
try{
|
||||
// First try to open a path that already exists in the file system.
|
||||
const path = (zName && wasm.peek8(zName))
|
||||
? SAHPool.getPath(name)
|
||||
: getRandomName();
|
||||
let ah = SAHPool.mapPathToSAH.get(path);
|
||||
if(!ah && (flags & capi.SQLITE_OPEN_CREATE)) {
|
||||
// File not found so try to create it.
|
||||
if(SAHPool.getFileCount() < SAHPool.getCapacity()) {
|
||||
// Choose an unassociated OPFS file from the pool.
|
||||
ah = SAHPool.availableSAH.keys()[0];
|
||||
SAHPool.setAssociatedPath(ah, path, flags);
|
||||
}else{
|
||||
// File pool is full.
|
||||
toss('SAH pool is full. Cannot create file',path);
|
||||
}
|
||||
}
|
||||
if(!ah){
|
||||
toss('file not found:',path);
|
||||
}
|
||||
// Subsequent methods are only passed the file pointer, so
|
||||
// map the relevant info we need to that pointer.
|
||||
const file = { path, flags, ah };
|
||||
SAHPool.mapIdToFile.set(pFile, file);
|
||||
wasm.poke32(pOutFlags, flags);
|
||||
return 0;
|
||||
}catch(e){
|
||||
SAHPool.storeErr(e);
|
||||
return capi.SQLITE_CANTOPEN;
|
||||
}
|
||||
}/*xOpen()*/
|
||||
}/*vfsSyncWrappers*/;
|
||||
|
||||
@ -481,10 +602,10 @@ const installOpfsVfs = async function(sqlite3){
|
||||
else reject the promise. Returns true on success,
|
||||
else false.
|
||||
*/
|
||||
const affirmHasSyncAPI = async function(){
|
||||
if(!(async ()=>{
|
||||
try {
|
||||
const dh = await navigator.storage.getDirectory();
|
||||
const fn = '.opfs-sahpool-sync-check-'+Math.random().toString(36).slice(2);
|
||||
const fn = '.opfs-sahpool-sync-check-'+getRandomName();
|
||||
const fh = await dh.getFileHandle(fn, { create: true });
|
||||
const ah = await fh.createSyncAccessHandle();
|
||||
const close = ah.close();
|
||||
@ -499,19 +620,69 @@ const installOpfsVfs = async function(sqlite3){
|
||||
promiseReject(e);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
if(!(await affirmHasSyncAPI())) return;
|
||||
})()){
|
||||
return;
|
||||
}
|
||||
|
||||
SAHPool.isReady = SAHPool.reset().then(async ()=>{
|
||||
if(SAHPool.$error){
|
||||
throw SAHPool.$error;
|
||||
}
|
||||
if(0===SAHPool.getCapacity()){
|
||||
await SAHPool.addCapacity(DEFAULT_CAPACITY);
|
||||
}
|
||||
log("vfs list:",capi.sqlite3_js_vfs_list());
|
||||
//log("vfs list:",capi.sqlite3_js_vfs_list());
|
||||
sqlite3.vfs.installVfs({
|
||||
io: {struct: opfsIoMethods, methods: ioSyncWrappers},
|
||||
vfs: {struct: opfsVfs, methods: vfsSyncWrappers}
|
||||
});
|
||||
log("vfs list:",capi.sqlite3_js_vfs_list());
|
||||
log("opfs-sahpool VFS initialized.");
|
||||
//log("vfs list:",capi.sqlite3_js_vfs_list());
|
||||
if(sqlite3.oo1){
|
||||
const OpfsSAHPoolDb = function(...args){
|
||||
const opt = sqlite3.oo1.DB.dbCtorHelper.normalizeArgs(...args);
|
||||
opt.vfs = opfsVfs.$zName;
|
||||
sqlite3.oo1.DB.dbCtorHelper.call(this, opt);
|
||||
};
|
||||
OpfsSAHPoolDb.prototype = Object.create(sqlite3.oo1.DB.prototype);
|
||||
OpfsSAHPoolDb.addPoolCapacity = async (n)=>SAHPool.addCapacity(n);
|
||||
OpfsSAHPoolDb.removePoolCapacity = async (n)=>SAHPool.removeCapacity(n);
|
||||
OpfsSAHPoolDb.getPoolCapacity = ()=>SAHPool.getCapacity();
|
||||
OpfsSAHPoolDb.getPoolUsage = ()=>SAHPool.getFileCount();
|
||||
sqlite3.oo1.OpfsSAHPoolDb = OpfsSAHPoolDb;
|
||||
sqlite3.oo1.DB.dbCtorHelper.setVfsPostOpenSql(
|
||||
opfsVfs.pointer,
|
||||
function(oo1Db, sqlite3){
|
||||
sqlite3.capi.sqlite3_exec(oo1Db, [
|
||||
/* As of July 2023, the PERSIST journal mode on OPFS is
|
||||
somewhat slower than DELETE or TRUNCATE (it was faster
|
||||
before Chrome version 108 or 109). TRUNCATE and DELETE
|
||||
have very similar performance on OPFS.
|
||||
|
||||
Roy Hashimoto notes that TRUNCATE and PERSIST modes may
|
||||
decrease OPFS concurrency because multiple connections
|
||||
can open the journal file in those modes:
|
||||
|
||||
https://github.com/rhashimoto/wa-sqlite/issues/68
|
||||
|
||||
Given that, and the fact that testing has not revealed
|
||||
any appreciable difference between performance of
|
||||
TRUNCATE and DELETE modes on OPFS, we currently (as of
|
||||
2023-07-13) default to DELETE mode.
|
||||
*/
|
||||
"pragma journal_mode=DELETE;",
|
||||
/*
|
||||
OPFS benefits hugely from cache on moderate/large
|
||||
speedtest1 --size 50 and --size 100 workloads. We
|
||||
currently rely on setting a non-default cache size when
|
||||
building sqlite3.wasm. If that policy changes, the cache
|
||||
can be set here.
|
||||
*/
|
||||
"pragma cache_size=-16384;"
|
||||
], 0, 0, 0);
|
||||
}
|
||||
);
|
||||
}/*extend sqlite3.oo1*/
|
||||
log("VFS initialized.");
|
||||
promiseResolve(sqlite3);
|
||||
}).catch(promiseReject);
|
||||
})/*return Promise*/;
|
||||
@ -520,7 +691,7 @@ const installOpfsVfs = async function(sqlite3){
|
||||
globalThis.sqlite3ApiBootstrap.initializersAsync.push(async (sqlite3)=>{
|
||||
return installOpfsVfs(sqlite3).catch((e)=>{
|
||||
sqlite3.config.warn("Ignoring inability to install opfs-sahpool sqlite3_vfs:",
|
||||
e.message);
|
||||
e.message, e);
|
||||
});
|
||||
}/*sqlite3ApiBootstrap.initializersAsync*/);
|
||||
}/*sqlite3ApiBootstrap.initializers*/);
|
||||
|
Reference in New Issue
Block a user