summaryrefslogtreecommitdiffstats
path: root/ext/wasm/batch-runner-sahpool.js
diff options
context:
space:
mode:
Diffstat (limited to 'ext/wasm/batch-runner-sahpool.js')
-rw-r--r--ext/wasm/batch-runner-sahpool.js341
1 files changed, 341 insertions, 0 deletions
diff --git a/ext/wasm/batch-runner-sahpool.js b/ext/wasm/batch-runner-sahpool.js
new file mode 100644
index 0000000..dfa5044
--- /dev/null
+++ b/ext/wasm/batch-runner-sahpool.js
@@ -0,0 +1,341 @@
+/*
+ 2023-11-30
+
+ The author disclaims copyright to this source code. In place of a
+ legal notice, here is a blessing:
+
+ * May you do good and not evil.
+ * May you find forgiveness for yourself and forgive others.
+ * May you share freely, never taking more than you give.
+
+ ***********************************************************************
+
+ A basic batch SQL runner for the SAHPool VFS. This file must be run in
+ a worker thread. This is not a full-featured app, just a way to get some
+ measurements for batch execution of SQL for the OPFS SAH Pool VFS.
+*/
+'use strict';
+
+const wMsg = function(msgType,...args){
+ postMessage({
+ type: msgType,
+ data: args
+ });
+};
+const toss = function(...args){throw new Error(args.join(' '))};
+const warn = (...args)=>{ wMsg('warn',...args); };
+const error = (...args)=>{ wMsg('error',...args); };
+const log = (...args)=>{ wMsg('stdout',...args); }
+let sqlite3;
+const urlParams = new URL(globalThis.location.href).searchParams;
+const cacheSize = (()=>{
+ if(urlParams.has('cachesize')) return +urlParams.get('cachesize');
+ return 200;
+})();
+
+
+/** Throws if the given sqlite3 result code is not 0. */
+const checkSqliteRc = (dbh,rc)=>{
+ if(rc) toss("Prepare failed:",sqlite3.capi.sqlite3_errmsg(dbh));
+};
+
+const sqlToDrop = [
+ "SELECT type,name FROM sqlite_schema ",
+ "WHERE name NOT LIKE 'sqlite\\_%' escape '\\' ",
+ "AND name NOT LIKE '\\_%' escape '\\'"
+].join('');
+
+const clearDbSqlite = function(db){
+ // This would be SO much easier with the oo1 API, but we specifically want to
+ // inject metrics we can't get via that API, and we cannot reliably (OPFS)
+ // open the same DB twice to clear it using that API, so...
+ const rc = sqlite3.wasm.exports.sqlite3_wasm_db_reset(db.handle);
+ log("reset db rc =",rc,db.id, db.filename);
+};
+
+const App = {
+ db: undefined,
+ cache:Object.create(null),
+ log: log,
+ warn: warn,
+ error: error,
+ metrics: {
+ fileCount: 0,
+ runTimeMs: 0,
+ prepareTimeMs: 0,
+ stepTimeMs: 0,
+ stmtCount: 0,
+ strcpyMs: 0,
+ sqlBytes: 0
+ },
+ fileList: undefined,
+ execSql: async function(name,sql){
+ const db = this.db;
+ const banner = "========================================";
+ this.log(banner,
+ "Running",name,'('+sql.length,'bytes)');
+ const capi = this.sqlite3.capi, wasm = this.sqlite3.wasm;
+ let pStmt = 0, pSqlBegin;
+ const metrics = db.metrics = Object.create(null);
+ metrics.prepTotal = metrics.stepTotal = 0;
+ metrics.stmtCount = 0;
+ metrics.malloc = 0;
+ metrics.strcpy = 0;
+ if(this.gotErr){
+ this.error("Cannot run SQL: error cleanup is pending.");
+ return;
+ }
+ // Run this async so that the UI can be updated for the above header...
+ const endRun = ()=>{
+ metrics.evalSqlEnd = performance.now();
+ metrics.evalTimeTotal = (metrics.evalSqlEnd - metrics.evalSqlStart);
+ this.log("metrics:",JSON.stringify(metrics, undefined, ' '));
+ this.log("prepare() count:",metrics.stmtCount);
+ this.log("Time in prepare_v2():",metrics.prepTotal,"ms",
+ "("+(metrics.prepTotal / metrics.stmtCount),"ms per prepare())");
+ this.log("Time in step():",metrics.stepTotal,"ms",
+ "("+(metrics.stepTotal / metrics.stmtCount),"ms per step())");
+ this.log("Total runtime:",metrics.evalTimeTotal,"ms");
+ this.log("Overhead (time - prep - step):",
+ (metrics.evalTimeTotal - metrics.prepTotal - metrics.stepTotal)+"ms");
+ this.log(banner,"End of",name);
+ this.metrics.prepareTimeMs += metrics.prepTotal;
+ this.metrics.stepTimeMs += metrics.stepTotal;
+ this.metrics.stmtCount += metrics.stmtCount;
+ this.metrics.strcpyMs += metrics.strcpy;
+ this.metrics.sqlBytes += sql.length;
+ };
+
+ const runner = function(resolve, reject){
+ ++this.metrics.fileCount;
+ metrics.evalSqlStart = performance.now();
+ const stack = wasm.scopedAllocPush();
+ try {
+ let t, rc;
+ let sqlByteLen = sql.byteLength;
+ const [ppStmt, pzTail] = wasm.scopedAllocPtr(2);
+ t = performance.now();
+ pSqlBegin = wasm.scopedAlloc( sqlByteLen + 1/*SQL + NUL*/) || toss("alloc(",sqlByteLen,") failed");
+ metrics.malloc = performance.now() - t;
+ metrics.byteLength = sqlByteLen;
+ let pSql = pSqlBegin;
+ const pSqlEnd = pSqlBegin + sqlByteLen;
+ t = performance.now();
+ wasm.heap8().set(sql, pSql);
+ wasm.poke(pSql + sqlByteLen, 0);
+ //log("SQL:",wasm.cstrToJs(pSql));
+ metrics.strcpy = performance.now() - t;
+ let breaker = 0;
+ while(pSql && wasm.peek8(pSql)){
+ wasm.pokePtr(ppStmt, 0);
+ wasm.pokePtr(pzTail, 0);
+ t = performance.now();
+ rc = capi.sqlite3_prepare_v2(
+ db.handle, pSql, sqlByteLen, ppStmt, pzTail
+ );
+ metrics.prepTotal += performance.now() - t;
+ checkSqliteRc(db.handle, rc);
+ pStmt = wasm.peekPtr(ppStmt);
+ pSql = wasm.peekPtr(pzTail);
+ sqlByteLen = pSqlEnd - pSql;
+ if(!pStmt) continue/*empty statement*/;
+ ++metrics.stmtCount;
+ t = performance.now();
+ rc = capi.sqlite3_step(pStmt);
+ capi.sqlite3_finalize(pStmt);
+ pStmt = 0;
+ metrics.stepTotal += performance.now() - t;
+ switch(rc){
+ case capi.SQLITE_ROW:
+ case capi.SQLITE_DONE: break;
+ default: checkSqliteRc(db.handle, rc); toss("Not reached.");
+ }
+ }
+ resolve(this);
+ }catch(e){
+ if(pStmt) capi.sqlite3_finalize(pStmt);
+ this.gotErr = e;
+ reject(e);
+ }finally{
+ capi.sqlite3_exec(db.handle,"rollback;",0,0,0);
+ wasm.scopedAllocPop(stack);
+ }
+ }.bind(this);
+ const p = new Promise(runner);
+ return p.catch(
+ (e)=>this.error("Error via execSql("+name+",...):",e.message)
+ ).finally(()=>{
+ endRun();
+ });
+ },
+
+ /**
+ Loads batch-runner.list and populates the selection list from
+ it. Returns a promise which resolves to nothing in particular
+ when it completes. Only intended to be run once at the start
+ of the app.
+ */
+ loadSqlList: async function(){
+ const infile = 'batch-runner.list';
+ this.log("Loading list of SQL files:", infile);
+ let txt;
+ try{
+ const r = await fetch(infile);
+ if(404 === r.status){
+ toss("Missing file '"+infile+"'.");
+ }
+ if(!r.ok) toss("Loading",infile,"failed:",r.statusText);
+ txt = await r.text();
+ }catch(e){
+ this.error(e.message);
+ throw e;
+ }
+ App.fileList = txt.split(/\n+/).filter(x=>!!x);
+ this.log("Loaded",infile);
+ },
+
+ /** Fetch ./fn and return its contents as a Uint8Array. */
+ fetchFile: async function(fn, cacheIt=false){
+ if(cacheIt && this.cache[fn]) return this.cache[fn];
+ this.log("Fetching",fn,"...");
+ let sql;
+ try {
+ const r = await fetch(fn);
+ if(!r.ok) toss("Fetch failed:",r.statusText);
+ sql = new Uint8Array(await r.arrayBuffer());
+ }catch(e){
+ this.error(e.message);
+ throw e;
+ }
+ this.log("Fetched",sql.length,"bytes from",fn);
+ if(cacheIt) this.cache[fn] = sql;
+ return sql;
+ }/*fetchFile()*/,
+
+ /**
+ Converts this.metrics() to a form which is suitable for easy conversion to
+ CSV. It returns an array of arrays. The first sub-array is the column names.
+ The 2nd and subsequent are the values, one per test file (only the most recent
+ metrics are kept for any given file).
+ */
+ metricsToArrays: function(){
+ const rc = [];
+ Object.keys(this.dbs).sort().forEach((k)=>{
+ const d = this.dbs[k];
+ const m = d.metrics;
+ delete m.evalSqlStart;
+ delete m.evalSqlEnd;
+ const mk = Object.keys(m).sort();
+ if(!rc.length){
+ rc.push(['db', ...mk]);
+ }
+ const row = [k.split('/').pop()/*remove dir prefix from filename*/];
+ rc.push(row);
+ row.push(...mk.map((kk)=>m[kk]));
+ });
+ return rc;
+ },
+
+ metricsToBlob: function(colSeparator='\t'){
+ const ar = [], ma = this.metricsToArrays();
+ if(!ma.length){
+ this.error("Metrics are empty. Run something.");
+ return;
+ }
+ ma.forEach(function(row){
+ ar.push(row.join(colSeparator),'\n');
+ });
+ return new Blob(ar);
+ },
+
+ /**
+ Fetch file fn and eval it as an SQL blob. This is an async
+ operation and returns a Promise which resolves to this
+ object on success.
+ */
+ evalFile: async function(fn){
+ const sql = await this.fetchFile(fn);
+ return this.execSql(fn,sql);
+ }/*evalFile()*/,
+
+ /**
+ Fetches the handle of the db associated with
+ this.e.selImpl.value, opening it if needed.
+ */
+ initDb: function(){
+ const capi = this.sqlite3.capi, wasm = this.sqlite3.wasm;
+ const stack = wasm.scopedAllocPush();
+ let pDb = 0;
+ const d = Object.create(null);
+ d.filename = "/batch.db";
+ try{
+ const oFlags = capi.SQLITE_OPEN_CREATE | capi.SQLITE_OPEN_READWRITE;
+ const ppDb = wasm.scopedAllocPtr();
+ const rc = capi.sqlite3_open_v2(d.filename, ppDb, oFlags, this.PoolUtil.vfsName);
+ pDb = wasm.peekPtr(ppDb)
+ if(rc) toss("sqlite3_open_v2() failed with code",rc);
+ capi.sqlite3_exec(pDb, "PRAGMA cache_size="+cacheSize, 0, 0, 0);
+ this.log("cache_size =",cacheSize);
+ }catch(e){
+ if(pDb) capi.sqlite3_close_v2(pDb);
+ throw e;
+ }finally{
+ wasm.scopedAllocPop(stack);
+ }
+ d.handle = pDb;
+ this.log("Opened db:",d.filename,'@',d.handle);
+ return d;
+ },
+
+ closeDb: function(){
+ if(this.db.handle){
+ this.sqlite3.capi.sqlite3_close_v2(this.db.handle);
+ this.db.handle = undefined;
+ }
+ },
+
+ run: async function(sqlite3){
+ delete this.run;
+ this.sqlite3 = sqlite3;
+ const capi = sqlite3.capi, wasm = sqlite3.wasm;
+ this.log("Loaded module:",capi.sqlite3_libversion(), capi.sqlite3_sourceid());
+ this.log("WASM heap size =",wasm.heap8().length);
+ let timeStart;
+ sqlite3.installOpfsSAHPoolVfs({
+ clearOnInit: true, initialCapacity: 4,
+ name: 'batch-sahpool',
+ verbosity: 2
+ }).then(PoolUtil=>{
+ App.PoolUtil = PoolUtil;
+ App.db = App.initDb();
+ })
+ .then(async ()=>this.loadSqlList())
+ .then(async ()=>{
+ timeStart = performance.now();
+ for(let i = 0; i < App.fileList.length; ++i){
+ const fn = App.fileList[i];
+ await App.evalFile(fn);
+ if(App.gotErr) throw App.gotErr;
+ }
+ })
+ .then(()=>{
+ App.metrics.runTimeMs = performance.now() - timeStart;
+ App.log("total metrics:",JSON.stringify(App.metrics, undefined, ' '));
+ App.log("Reload the page to run this again.");
+ App.closeDb();
+ App.PoolUtil.removeVfs();
+ })
+ .catch(e=>this.error("ERROR:",e));
+ }/*run()*/
+}/*App*/;
+
+let sqlite3Js = 'sqlite3.js';
+if(urlParams.has('sqlite3.dir')){
+ sqlite3Js = urlParams.get('sqlite3.dir') + '/' + sqlite3Js;
+}
+importScripts(sqlite3Js);
+globalThis.sqlite3InitModule().then(async function(sqlite3_){
+ log("Done initializing. Running batch runner...");
+ sqlite3 = sqlite3_;
+ App.run(sqlite3_);
+});