summaryrefslogtreecommitdiffstats
path: root/ext/wasm/tests/opfs
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-13 14:07:11 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-13 14:07:11 +0000
commit63847496f14c813a5d80efd5b7de0f1294ffe1e3 (patch)
tree01c7571c7c762ceee70638549a99834fdd7c411b /ext/wasm/tests/opfs
parentInitial commit. (diff)
downloadsqlite3-63847496f14c813a5d80efd5b7de0f1294ffe1e3.tar.xz
sqlite3-63847496f14c813a5d80efd5b7de0f1294ffe1e3.zip
Adding upstream version 3.45.1.upstream/3.45.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'ext/wasm/tests/opfs')
-rw-r--r--ext/wasm/tests/opfs/concurrency/index.html49
-rw-r--r--ext/wasm/tests/opfs/concurrency/test.js136
-rw-r--r--ext/wasm/tests/opfs/concurrency/worker.js113
3 files changed, 298 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 0000000..595ab24
--- /dev/null
+++ b/ext/wasm/tests/opfs/concurrency/index.html
@@ -0,0 +1,49 @@
+<!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.
+ Disclaimer: concurrency in OPFS is currently a pain point!
+ </p>
+ <p>
+ URL flags: pass a number of workers using
+ the <code>workers=N</code> URL flag. Set the time between each
+ workload with <code>interval=N</code> (milliseconds). Set the
+ number of worker iterations with <code>iterations=N</code>.
+ Enable OPFS VFS verbosity with <code>verbose=1-3</code> (output
+ goes to the dev console). Disable/enable "unlock ASAP" mode
+ (higher concurrency, lower speed) with <code>unlock-asap=0-1</code>.
+ </p>
+ <p>Achtung: if it does not start to do anything within a couple of
+ seconds, check the dev console: Chrome sometimes fails to load
+ the wasm module due to "cannot allocate WasmMemory." Closing and
+ re-opening the tab usually resolves it, but sometimes restarting
+ the browser is required.
+ </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 0000000..14cd6f5
--- /dev/null
+++ b/ext/wasm/tests/opfs/concurrency/test.js
@@ -0,0 +1,136 @@
+(async function(self){
+
+ const logCss = (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 logCss = 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 logCss;
+ })();
+ const stdout = (...args)=>logCss('',...args);
+ const stderr = (...args)=>logCss('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
+ ) || 4;
+ options.opfsVerbose = (
+ urlArgsHtml.has('verbose') ? +urlArgsHtml.get('verbose') : 1
+ ) || 1;
+ options.interval = (
+ urlArgsHtml.has('interval') ? +urlArgsHtml.get('interval') : 1000
+ ) || 1000;
+ options.iterations = (
+ urlArgsHtml.has('iterations') ? +urlArgsHtml.get('iterations') : 10
+ ) || 10;
+ options.unlockAsap = (
+ urlArgsHtml.has('unlock-asap') ? +urlArgsHtml.get('unlock-asap') : 0
+ ) || 0;
+ options.noUnlink = !!urlArgsHtml.has('no-unlink');
+ const workers = [];
+ workers.post = (type,...args)=>{
+ for(const w of workers) w.postMessage({type, payload:args});
+ };
+ workers.counts = {loaded: 0, passed: 0, failed: 0};
+ const checkFinished = function(){
+ if(workers.counts.passed + workers.counts.failed !== workers.length){
+ return;
+ }
+ if(workers.counts.failed>0){
+ logCss('tests-fail',"Finished with",workers.counts.failed,"failure(s).");
+ }else{
+ logCss('tests-pass',"All",workers.length,"workers finished.");
+ }
+ };
+ workers.onmessage = function(msg){
+ msg = msg.data;
+ const prefix = 'Worker #'+msg.worker+':';
+ switch(msg.type){
+ case 'loaded':
+ stdout(prefix,"loaded");
+ if(++workers.counts.loaded === workers.length){
+ stdout("All",workers.length,"workers loaded. Telling them to run...");
+ workers.post('run');
+ }
+ break;
+ case 'stdout': stdout(prefix,...msg.payload); break;
+ case 'stderr': stderr(prefix,...msg.payload); break;
+ case 'error': stderr(prefix,"ERROR:",...msg.payload); break;
+ case 'finished':
+ ++workers.counts.passed;
+ logCss('tests-pass',prefix,...msg.payload);
+ checkFinished();
+ break;
+ case 'failed':
+ ++workers.counts.failed;
+ logCss('tests-fail',prefix,"FAILED:",...msg.payload);
+ checkFinished();
+ break;
+ default: logCss('error',"Unhandled message type:",msg); break;
+ }
+ };
+
+ stdout("Launching",options.workerCount,"workers. Options:",options);
+ workers.uri = (
+ 'worker.js?'
+ + 'sqlite3.dir='+options.sqlite3Dir
+ + '&interval='+options.interval
+ + '&iterations='+options.iterations
+ + '&opfs-verbose='+options.opfsVerbose
+ + '&opfs-unlock-asap='+options.unlockAsap
+ );
+ for(let i = 0; i < options.workerCount; ++i){
+ stdout("Launching worker...");
+ workers.push(new Worker(
+ workers.uri+'&workerId='+(i+1)+(
+ (i || options.noUnlink) ? '' : '&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 0000000..5d28bed
--- /dev/null
+++ b/ext/wasm/tests/opfs/concurrency/worker.js
@@ -0,0 +1,113 @@
+importScripts(
+ (new URL(self.location.href).searchParams).get('sqlite3.dir') + '/sqlite3.js'
+);
+self.sqlite3InitModule().then(async function(sqlite3){
+ const urlArgs = new URL(self.location.href).searchParams;
+ const options = {
+ workerName: urlArgs.get('workerId') || Math.round(Math.random()*10000),
+ unlockAsap: urlArgs.get('opfs-unlock-asap') || 0 /*EXPERIMENTAL*/
+ };
+ const wPost = (type,...payload)=>{
+ postMessage({type, worker: options.workerName, payload});
+ };
+ const stdout = (...args)=>wPost('stdout',...args);
+ const stderr = (...args)=>wPost('stderr',...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(urlArgs.has('unlink-db')){
+ await sqlite3.opfs.unlink(dbName);
+ stdout("Unlinked",dbName);
+ }
+ wPost('loaded');
+ let db;
+ const interval = Object.assign(Object.create(null),{
+ delay: urlArgs.has('interval') ? (+urlArgs.get('interval') || 750) : 750,
+ handle: undefined,
+ count: 0
+ });
+ const finish = ()=>{
+ if(db){
+ if(!db.pointer) return;
+ db.close();
+ }
+ if(interval.error){
+ wPost('failed',"Ending work after interval #"+interval.count,
+ "due to error:",interval.error);
+ }else{
+ wPost('finished',"Ending work after",interval.count,"intervals.");
+ }
+ };
+ const run = async function(){
+ db = new sqlite3.oo1.OpfsDb({
+ filename: 'file:'+dbName+'?opfs-unlock-asap='+options.unlockAsap,
+ flags: 'c'
+ });
+ sqlite3.capi.sqlite3_busy_timeout(db.pointer, 5000);
+ 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 =
+ urlArgs.has('iterations') ? (+urlArgs.get('iterations') || 10) : 10;
+ 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: [options.workerName, new Date().getTime()]
+ });
+ //stdout("Set",prefix);
+ }catch(e){
+ interval.error = e;
+ }
+ };
+ if(1){/*use setInterval()*/
+ setTimeout(async function timer(){
+ await doWork();
+ if(interval.error || maxIterations === interval.count){
+ finish();
+ }else{
+ setTimeout(timer, interval.delay);
+ }
+ }, 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)=>{
+ if(!interval.error) interval.error = e;
+ finish();
+ });
+ break;
+ default:
+ stderr("Unhandled message type '"+data.type+"'.");
+ break;
+ }
+ };
+});