aboutsummaryrefslogtreecommitdiff
path: root/ext/wasm/api
diff options
context:
space:
mode:
Diffstat (limited to 'ext/wasm/api')
-rw-r--r--ext/wasm/api/extern-post-js.js5
-rw-r--r--ext/wasm/api/sqlite3-api-opfs.js7
-rw-r--r--ext/wasm/api/sqlite3-api-prologue.js93
-rw-r--r--ext/wasm/api/sqlite3-opfs-async-proxy.js84
-rw-r--r--ext/wasm/api/sqlite3-wasm.c3
5 files changed, 145 insertions, 47 deletions
diff --git a/ext/wasm/api/extern-post-js.js b/ext/wasm/api/extern-post-js.js
index 84b99b53a..d933a3626 100644
--- a/ext/wasm/api/extern-post-js.js
+++ b/ext/wasm/api/extern-post-js.js
@@ -15,7 +15,10 @@
impls which Emscripten installs at some point in the file above
this.
*/
- const originalInit = self.sqlite3InitModule;
+ const originalInit =
+ /*Maintenance reminde: DO NOT use `self.` here. It's correct
+ for non-ES6 Module cases but wrong for ES6 modules because those
+ resolve this symbol differently! */ sqlite3InitModule;
if(!originalInit){
throw new Error("Expecting self.sqlite3InitModule to be defined by the Emscripten build.");
}
diff --git a/ext/wasm/api/sqlite3-api-opfs.js b/ext/wasm/api/sqlite3-api-opfs.js
index 86285df1d..da5496f65 100644
--- a/ext/wasm/api/sqlite3-api-opfs.js
+++ b/ext/wasm/api/sqlite3-api-opfs.js
@@ -467,9 +467,11 @@ const installOpfsVfs = function callee(options){
/**
Returns an array of the deserialized state stored by the most
recent serialize() operation (from from this thread or the
- counterpart thread), or null if the serialization buffer is empty.
+ counterpart thread), or null if the serialization buffer is
+ empty. If passed a truthy argument, the serialization buffer
+ is cleared after deserialization.
*/
- state.s11n.deserialize = function(){
+ state.s11n.deserialize = function(clear=false){
++metrics.s11n.deserialize.count;
const t = performance.now();
const argc = viewU8[0];
@@ -494,6 +496,7 @@ const installOpfsVfs = function callee(options){
rc.push(v);
}
}
+ if(clear) viewU8[0] = 0;
//log("deserialize:",argc, rc);
metrics.s11n.deserialize.time += performance.now() - t;
return rc;
diff --git a/ext/wasm/api/sqlite3-api-prologue.js b/ext/wasm/api/sqlite3-api-prologue.js
index cf44f3970..fed1c5666 100644
--- a/ext/wasm/api/sqlite3-api-prologue.js
+++ b/ext/wasm/api/sqlite3-api-prologue.js
@@ -17,22 +17,29 @@
conventions, and build process are very much under construction and
will be (re)documented once they've stopped fluctuating so much.
- Specific goals of this project:
+ Project home page: https://sqlite.org
+
+ Documentation home page: https://sqlite.org/wasm
+
+ Specific goals of this subproject:
- Except where noted in the non-goals, provide a more-or-less
feature-complete wrapper to the sqlite3 C API, insofar as WASM
- feature parity with C allows for. In fact, provide at least 3
+ feature parity with C allows for. In fact, provide at least 4
APIs...
- 1) Bind a low-level sqlite3 API which is as close to the native
- one as feasible in terms of usage.
+ 1) 1-to-1 bindings as exported from WASM, with no automatic
+ type conversions between JS and C.
+
+ 2) A binding of (1) which provides certain JS/C type conversions
+ to greatly simplify its use.
- 2) A higher-level API, more akin to sql.js and node.js-style
+ 3) A higher-level API, more akin to sql.js and node.js-style
implementations. This one speaks directly to the low-level
API. This API must be used from the same thread as the
low-level API.
- 3) A second higher-level API which speaks to the previous APIs via
+ 4) A second higher-level API which speaks to the previous APIs via
worker messages. This one is intended for use in the main
thread, with the lower-level APIs installed in a Worker thread,
and talking to them via Worker messages. Because Workers are
@@ -90,11 +97,13 @@
config object is only honored the first time this is
called. Subsequent calls ignore the argument and return the same
(configured) object which gets initialized by the first call.
+ This function will throw if any of the required config options are
+ missing.
The config object properties include:
- `exports`[^1]: the "exports" object for the current WASM
- environment. In an Emscripten build, this should be set to
+ environment. In an Emscripten-based build, this should be set to
`Module['asm']`.
- `memory`[^1]: optional WebAssembly.Memory object, defaulting to
@@ -104,7 +113,7 @@
WASM-exported memory.
- `bigIntEnabled`: true if BigInt support is enabled. Defaults to
- true if self.BigInt64Array is available, else false. Some APIs
+ true if `self.BigInt64Array` is available, else false. Some APIs
will throw exceptions if called without BigInt support, as BigInt
is required for marshalling C-side int64 into and out of JS.
@@ -116,10 +125,12 @@
the `free(3)`-compatible routine for the WASM
environment. Defaults to `"free"`.
- - `wasmfsOpfsDir`[^1]: if the environment supports persistent storage, this
- directory names the "mount point" for that directory. It must be prefixed
- by `/` and may currently contain only a single directory-name part. Using
- the root directory name is not supported by any current persistent backend.
+ - `wasmfsOpfsDir`[^1]: if the environment supports persistent
+ storage, this directory names the "mount point" for that
+ directory. It must be prefixed by `/` and may contain only a
+ single directory-name part. Using the root directory name is not
+ supported by any current persistent backend. This setting is
+ only used in WASMFS-enabled builds.
[^1] = This property may optionally be a function, in which case this
@@ -388,8 +399,22 @@ self.sqlite3ApiBootstrap = function sqlite3ApiBootstrap(
exceptions.
*/
class WasmAllocError extends Error {
+ /**
+ If called with 2 arguments and the 2nd one is an object, it
+ behaves like the Error constructor, else it concatenates all
+ arguments together with a single space between each to
+ construct an error message string. As a special case, if
+ called with no arguments then it uses a default error
+ message.
+ */
constructor(...args){
- super(...args);
+ if(2===args.length && 'object'===typeof args){
+ super(...args);
+ }else if(args.length){
+ super(args.join(' '));
+ }else{
+ super("Allocation failed.");
+ }
this.name = 'WasmAllocError';
}
};
@@ -699,21 +724,33 @@ self.sqlite3ApiBootstrap = function sqlite3ApiBootstrap(
API NOT throw and must instead return SQLITE_NOMEM (or
equivalent, depending on the context).
- That said, very few cases in the API can result in
+ Very few cases in the sqlite3 JS APIs can result in
client-defined functions propagating exceptions via the C-style
- API. Most notably, this applies ot User-defined SQL Functions
- (UDFs) registered via sqlite3_create_function_v2(). For that
- specific case it is recommended that all UDF creation be
- funneled through a utility function and that a wrapper function
- be added around the UDF which catches any exception and sets
- the error state to OOM. (The overall complexity of registering
- UDFs essentially requires a helper for doing so!)
+ API. Most notably, this applies to WASM-bound JS functions
+ which are created directly by clients and passed on _as WASM
+ function pointers_ to functions such as
+ sqlite3_create_function_v2(). Such bindings created
+ transparently by this API will automatically use wrappers which
+ catch exceptions and convert them to appropriate error codes.
+
+ For cases where non-throwing allocation is required, use
+ sqlite3.wasm.alloc.impl(), which is direct binding of the
+ underlying C-level allocator.
+
+ Design note: this function is not named "malloc" primarily
+ because Emscripten uses that name and we wanted to avoid any
+ confusion early on in this code's development, when it still
+ had close ties to Emscripten's glue code.
*/
alloc: undefined/*installed later*/,
+
/**
The API's one single point of access to the WASM-side memory
deallocator. Works like free(3) (and is likely bound to
free()).
+
+ Design note: this function is not named "free" for the same
+ reason that this.alloc() is not called this.malloc().
*/
dealloc: undefined/*installed later*/
@@ -741,7 +778,9 @@ self.sqlite3ApiBootstrap = function sqlite3ApiBootstrap(
wasm.allocFromTypedArray = function(srcTypedArray){
affirmBindableTypedArray(srcTypedArray);
const pRet = wasm.alloc(srcTypedArray.byteLength || 1);
- wasm.heapForSize(srcTypedArray.constructor).set(srcTypedArray.byteLength ? srcTypedArray : [0], pRet);
+ wasm.heapForSize(srcTypedArray.constructor).set(
+ srcTypedArray.byteLength ? srcTypedArray : [0], pRet
+ );
return pRet;
};
@@ -752,13 +791,13 @@ self.sqlite3ApiBootstrap = function sqlite3ApiBootstrap(
if(!(f instanceof Function)) toss3("Missing required exports[",key,"] function.");
}
- wasm.alloc = function(n){
- const m = wasm.exports[keyAlloc](n);
- if(!m) throw new WasmAllocError("Failed to allocate "+n+" bytes.");
+ wasm.alloc = function f(n){
+ const m = f.impl(n);
+ if(!m) throw new WasmAllocError("Failed to allocate",n," bytes.");
return m;
};
-
- wasm.dealloc = (m)=>wasm.exports[keyDealloc](m);
+ wasm.alloc.impl = wasm.exports[keyAlloc];
+ wasm.dealloc = wasm.exports[keyDealloc];
/**
Reports info about compile-time options using
diff --git a/ext/wasm/api/sqlite3-opfs-async-proxy.js b/ext/wasm/api/sqlite3-opfs-async-proxy.js
index 09c56ff1d..e4657484e 100644
--- a/ext/wasm/api/sqlite3-opfs-async-proxy.js
+++ b/ext/wasm/api/sqlite3-opfs-async-proxy.js
@@ -44,6 +44,7 @@ if(self.window === self){
this API.
*/
const state = Object.create(null);
+
/**
verbose:
@@ -96,13 +97,27 @@ metrics.dump = ()=>{
};
/**
- Map of sqlite3_file pointers (integers) to metadata related to a
- given OPFS file handles. The pointers are, in this side of the
- interface, opaque file handle IDs provided by the synchronous
- part of this constellation. Each value is an object with a structure
- demonstrated in the xOpen() impl.
+ __openFiles is a map of sqlite3_file pointers (integers) to
+ metadata related to a given OPFS file handles. The pointers are, in
+ this side of the interface, opaque file handle IDs provided by the
+ synchronous part of this constellation. Each value is an object
+ with a structure demonstrated in the xOpen() impl.
*/
const __openFiles = Object.create(null);
+/**
+ __autoLocks is a Set of sqlite3_file pointers (integers) which were
+ "auto-locked". i.e. those for which we obtained a sync access
+ handle without an explicit xLock() call. Such locks will be
+ released during db connection idle time, whereas a sync access
+ handle obtained via xLock(), or subsequently xLock()'d after
+ auto-acquisition, will not be released until xUnlock() is called.
+
+ Maintenance reminder: if we relinquish auto-locks at the end of the
+ operation which acquires them, we pay a massive performance
+ penalty: speedtest1 benchmarks take up to 4x as long. By delaying
+ the lock release until idle time, the hit is negligible.
+*/
+const __autoLocks = new Set();
/**
Expects an OPFS file path. It gets resolved, such that ".."
@@ -191,6 +206,10 @@ const getSyncHandle = async (fh)=>{
}
}
log("Got sync handle for",fh.filenameAbs,'in',performance.now() - t,'ms');
+ if(!fh.xLock){
+ __autoLocks.add(fh.fid);
+ log("Auto-locked",fh.fid,fh.filenameAbs);
+ }
}
return fh.syncHandle;
};
@@ -210,11 +229,31 @@ const closeSyncHandle = async (fh)=>{
log("Closing sync handle for",fh.filenameAbs);
const h = fh.syncHandle;
delete fh.syncHandle;
+ delete fh.xLock;
+ __autoLocks.delete(fh.fid);
return h.close();
}
};
/**
+ A proxy for closeSyncHandle() which is guaranteed to not throw.
+
+ This function is part of a lock/unlock step in functions which
+ require a sync access handle but may be called without xLock()
+ having been called first. Such calls need to release that
+ handle to avoid locking the file for all of time. This is an
+ _attempt_ at reducing cross-tab contention but it may prove
+ to be more of a problem than a solution and may need to be
+ removed.
+*/
+const closeSyncHandleNoThrow = async (fh)=>{
+ try{await closeSyncHandle(fh)}
+ catch(e){
+ warn("closeSyncHandleNoThrow() ignoring:",e,fh);
+ }
+};
+
+/**
Stores the given value at state.sabOPView[state.opIds.rc] and then
Atomics.notify()'s it.
*/
@@ -342,9 +381,10 @@ const vfsAsyncImpls = {
xClose: async function(fid/*sqlite3_file pointer*/){
const opName = 'xClose';
mTimeStart(opName);
+ __autoLocks.delete(fid);
const fh = __openFiles[fid];
let rc = 0;
- wTimeStart('xClose');
+ wTimeStart(opName);
if(fh){
delete __openFiles[fid];
await closeSyncHandle(fh);
@@ -422,12 +462,17 @@ const vfsAsyncImpls = {
mTimeStart('xLock');
const fh = __openFiles[fid];
let rc = 0;
+ const oldLockType = fh.xLock;
+ fh.xLock = lockType;
if( !fh.syncHandle ){
wTimeStart('xLock');
- try { await getSyncHandle(fh) }
- catch(e){
+ try {
+ await getSyncHandle(fh);
+ __autoLocks.delete(fid);
+ }catch(e){
state.s11n.storeException(1,e);
rc = state.sq3Codes.SQLITE_IOERR_LOCK;
+ fh.xLock = oldLockType;
}
wTimeEnd();
}
@@ -461,6 +506,7 @@ const vfsAsyncImpls = {
*/
wTimeEnd();
__openFiles[fid] = Object.assign(Object.create(null),{
+ fid: fid,
filenameAbs: filename,
filenamePart: filenamePart,
dirHandle: hDir,
@@ -610,7 +656,7 @@ const initS11n = ()=>{
default: toss("Invalid type ID:",tid);
}
};
- state.s11n.deserialize = function(){
+ state.s11n.deserialize = function(clear=false){
++metrics.s11n.deserialize.count;
const t = performance.now();
const argc = viewU8[0];
@@ -635,6 +681,7 @@ const initS11n = ()=>{
rc.push(v);
}
}
+ if(clear) viewU8[0] = 0;
//log("deserialize:",argc, rc);
metrics.s11n.deserialize.time += performance.now() - t;
return rc;
@@ -701,21 +748,30 @@ const waitLoop = async function f(){
We need to wake up periodically to give the thread a chance
to do other things.
*/
- const waitTime = 1000;
+ const waitTime = 500;
while(!flagAsyncShutdown){
try {
if('timed-out'===Atomics.wait(
state.sabOPView, state.opIds.whichOp, 0, waitTime
)){
+ if(__autoLocks.size){
+ /* Release all auto-locks. */
+ for(const fid of __autoLocks){
+ const fh = __openFiles[fid];
+ await closeSyncHandleNoThrow(fh);
+ log("Auto-unlocked",fid,fh.filenameAbs);
+ }
+ }
continue;
}
const opId = Atomics.load(state.sabOPView, state.opIds.whichOp);
Atomics.store(state.sabOPView, state.opIds.whichOp, 0);
const hnd = opHandlers[opId] ?? toss("No waitLoop handler for whichOp #",opId);
- const args = state.s11n.deserialize() || [];
- state.s11n.serialize(/* clear s11n to keep the caller from
- confusing this with an exception string
- written by the upcoming operation */);
+ const args = state.s11n.deserialize(
+ true /* clear s11n to keep the caller from confusing this with
+ an exception string written by the upcoming
+ operation */
+ ) || [];
//warn("waitLoop() whichOp =",opId, hnd, args);
if(hnd.f) await hnd.f(...args);
else error("Missing callback for opId",opId);
diff --git a/ext/wasm/api/sqlite3-wasm.c b/ext/wasm/api/sqlite3-wasm.c
index 9d04ad129..af5ed6bf7 100644
--- a/ext/wasm/api/sqlite3-wasm.c
+++ b/ext/wasm/api/sqlite3-wasm.c
@@ -1108,9 +1108,6 @@ int sqlite3_wasm_init_wasmfs(const char *zMountPoint){
/** It's not enough to instantiate the backend. We have to create a
mountpoint in the VFS and attach the backend to it. */
if( pOpfs && 0!=access(zMountPoint, F_OK) ){
- /* mkdir() simply hangs when called from fiddle app. Cause is
- not yet determined but the hypothesis is an init-order
- issue. */
/* Note that this check and is not robust but it will
hypothetically suffice for the transient wasm-based virtual
filesystem we're currently running in. */