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