diff options
Diffstat (limited to 'ext/wasm/tests/opfs/concurrency')
-rw-r--r-- | ext/wasm/tests/opfs/concurrency/index.html | 34 | ||||
-rw-r--r-- | ext/wasm/tests/opfs/concurrency/test.js | 97 | ||||
-rw-r--r-- | ext/wasm/tests/opfs/concurrency/worker.js | 95 |
3 files changed, 226 insertions, 0 deletions
diff --git a/ext/wasm/tests/opfs/concurrency/index.html b/ext/wasm/tests/opfs/concurrency/index.html new file mode 100644 index 000000000..79a46692c --- /dev/null +++ b/ext/wasm/tests/opfs/concurrency/index.html @@ -0,0 +1,34 @@ +<!doctype html> +<html lang="en-us"> + <head> + <meta charset="utf-8"> + <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> + <link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon"> + <link rel="stylesheet" href="../../../common/testing.css"/> + <title>sqlite3 OPFS Worker concurrency tester</title> + <style> + body { display: revert; } + body > * {} + #test-output { + font-family: monospace; + } + </style> + </head> + <body> + <h1></h1> + <p> + OPFS concurrency tester using multiple independent Workers. + This app is incomplete. + </p> + <div class='input-wrapper'> + <input type='checkbox' id='cb-log-reverse'> + <label for='cb-log-reverse'>Reverse log order?</label> + </div> + <div id='test-output'></div> + <script>(function(){ + document.querySelector('h1').innerHTML = + document.querySelector('title').innerHTML; + })();</script> + <script src="test.js?sqlite3.dir=../../../jswasm"></script> + </body> +</html> diff --git a/ext/wasm/tests/opfs/concurrency/test.js b/ext/wasm/tests/opfs/concurrency/test.js new file mode 100644 index 000000000..d045f3271 --- /dev/null +++ b/ext/wasm/tests/opfs/concurrency/test.js @@ -0,0 +1,97 @@ +(async function(self){ + + const logClass = (function(){ + const mapToString = (v)=>{ + switch(typeof v){ + case 'number': case 'string': case 'boolean': + case 'undefined': case 'bigint': + return ''+v; + default: break; + } + if(null===v) return 'null'; + if(v instanceof Error){ + v = { + message: v.message, + stack: v.stack, + errorClass: v.name + }; + } + return JSON.stringify(v,undefined,2); + }; + const normalizeArgs = (args)=>args.map(mapToString); + const logTarget = document.querySelector('#test-output'); + const logClass = function(cssClass,...args){ + const ln = document.createElement('div'); + if(cssClass){ + for(const c of (Array.isArray(cssClass) ? cssClass : [cssClass])){ + ln.classList.add(c); + } + } + ln.append(document.createTextNode(normalizeArgs(args).join(' '))); + logTarget.append(ln); + }; + const cbReverse = document.querySelector('#cb-log-reverse'); + const cbReverseKey = 'tester1:cb-log-reverse'; + const cbReverseIt = ()=>{ + logTarget.classList[cbReverse.checked ? 'add' : 'remove']('reverse'); + localStorage.setItem(cbReverseKey, cbReverse.checked ? 1 : 0); + }; + cbReverse.addEventListener('change', cbReverseIt, true); + if(localStorage.getItem(cbReverseKey)){ + cbReverse.checked = !!(+localStorage.getItem(cbReverseKey)); + } + cbReverseIt(); + return logClass; + })(); + const stdout = (...args)=>logClass('',...args); + const stderr = (...args)=>logClass('error',...args); + + const wait = async (ms)=>{ + return new Promise((resolve)=>setTimeout(resolve,ms)); + }; + + const urlArgsJs = new URL(document.currentScript.src).searchParams; + const urlArgsHtml = new URL(self.location.href).searchParams; + const options = Object.create(null); + options.sqlite3Dir = urlArgsJs.get('sqlite3.dir'); + options.workerCount = ( + urlArgsHtml.has('workers') ? +urlArgsHtml.get('workers') : 3 + ) || 3; + const workers = []; + workers.post = (type,...args)=>{ + for(const w of workers) w.postMessage({type, payload:args}); + }; + workers.loadedCount = 0; + workers.onmessage = function(msg){ + msg = msg.data; + const wName = msg.worker; + const prefix = 'Worker ['+wName+']:'; + switch(msg.type){ + case 'stdout': stdout(prefix,...msg.payload); break; + case 'stderr': stderr(prefix,...msg.payload); break; + case 'error': stderr(prefix,"ERROR:",...msg.payload); break; + case 'loaded': + stdout(prefix,"loaded"); + if(++workers.loadedCount === workers.length){ + stdout("All workers loaded. Telling them to run..."); + workers.post('run'); + } + break; + default: logClass('error',"Unhandled message type:",msg); break; + } + }; + + stdout("Launching",options.workerCount,"workers..."); + workers.uri = ( + 'worker.js?' + + 'sqlite3.dir='+options.sqlite3Dir + + '&opfs-verbose=2' + ); + for(let i = 0; i < options.workerCount; ++i){ + stdout("Launching worker..."); + workers.push(new Worker(workers.uri+(i ? '' : '&unlink-db'))); + } + // Have to delay onmessage assignment until after the loop + // to avoid that early workers get an undue head start. + workers.forEach((w)=>w.onmessage = workers.onmessage); +})(self); diff --git a/ext/wasm/tests/opfs/concurrency/worker.js b/ext/wasm/tests/opfs/concurrency/worker.js new file mode 100644 index 000000000..7ba15bf8c --- /dev/null +++ b/ext/wasm/tests/opfs/concurrency/worker.js @@ -0,0 +1,95 @@ +importScripts( + (new URL(self.location.href).searchParams).get('sqlite3.dir') + '/sqlite3.js' +); +self.sqlite3InitModule().then(async function(sqlite3){ + const wName = Math.round(Math.random()*10000); + const wPost = (type,...payload)=>{ + postMessage({type, worker: wName, payload}); + }; + const stdout = (...args)=>wPost('stdout',...args); + const stderr = (...args)=>wPost('stderr',...args); + const postErr = (...args)=>wPost('error',...args); + if(!sqlite3.opfs){ + stderr("OPFS support not detected. Aborting."); + return; + } + + const wait = async (ms)=>{ + return new Promise((resolve)=>setTimeout(resolve,ms)); + }; + + const dbName = 'concurrency-tester.db'; + if((new URL(self.location.href).searchParams).has('unlink-db')){ + await sqlite3.opfs.unlink(dbName); + stdout("Unlinked",dbName); + } + wPost('loaded'); + + const run = async function(){ + const db = new sqlite3.opfs.OpfsDb(dbName); + //sqlite3.capi.sqlite3_busy_timeout(db.pointer, 2000); + db.transaction((db)=>{ + db.exec([ + "create table if not exists t1(w TEXT UNIQUE ON CONFLICT REPLACE,v);", + "create table if not exists t2(w TEXT UNIQUE ON CONFLICT REPLACE,v);" + ]); + }); + + const maxIterations = 10; + const interval = Object.assign(Object.create(null),{ + delay: 300, + handle: undefined, + count: 0 + }); + stdout("Starting interval-based db updates with delay of",interval.delay,"ms."); + const doWork = async ()=>{ + const tm = new Date().getTime(); + ++interval.count; + const prefix = "v(#"+interval.count+")"; + stdout("Setting",prefix,"=",tm); + try{ + db.exec({ + sql:"INSERT OR REPLACE INTO t1(w,v) VALUES(?,?)", + bind: [wName, new Date().getTime()] + }); + //stdout("Set",prefix); + }catch(e){ + interval.error = e; + } + }; + const finish = ()=>{ + if(interval.error) stderr("Ending work due to error:",e.message); + else stdout("Ending work after",interval.count,"interval(s)"); + db.close(); + }; + if(1){/*use setInterval()*/ + interval.handle = setInterval(async ()=>{ + await doWork(); + if(interval.error || maxIterations === interval.count){ + clearInterval(interval.handle); + finish(); + } + }, interval.delay); + }else{ + /*This approach provides no concurrency whatsoever: each worker + is run to completion before any others can work.*/ + let i; + for(i = 0; i < maxIterations; ++i){ + await doWork(); + if(interval.error) break; + await wait(interval.ms); + } + finish(); + } + }/*run()*/; + + self.onmessage = function({data}){ + switch(data.type){ + case 'run': run().catch((e)=>postErr(e.message)); + break; + default: + stderr("Unhandled message type '"+data.type+"'."); + break; + } + }; +}); |