| /* |
| 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_); |
| }); |