1/* 2 2022-08-29 3 4 The author disclaims copyright to this source code. In place of a 5 legal notice, here is a blessing: 6 7 * May you do good and not evil. 8 * May you find forgiveness for yourself and forgive others. 9 * May you share freely, never taking more than you give. 10 11 *********************************************************************** 12 13 A basic batch SQL runner for sqlite3-api.js. This file must be run in 14 main JS thread and sqlite3.js must have been loaded before it. 15*/ 16'use strict'; 17(function(){ 18 const toss = function(...args){throw new Error(args.join(' '))}; 19 const warn = console.warn.bind(console); 20 21 const App = { 22 e: { 23 output: document.querySelector('#test-output'), 24 selSql: document.querySelector('#sql-select'), 25 btnRun: document.querySelector('#sql-run'), 26 btnRunNext: document.querySelector('#sql-run-next'), 27 btnRunRemaining: document.querySelector('#sql-run-remaining'), 28 btnExportMetrics: document.querySelector('#export-metrics'), 29 btnClear: document.querySelector('#output-clear'), 30 btnReset: document.querySelector('#db-reset'), 31 cbReverseLog: document.querySelector('#cb-reverse-log-order') 32 }, 33 cache:{}, 34 metrics:{ 35 /** 36 Map of sql-file to timing metrics. We currently only store 37 the most recent run of each file, but we really should store 38 all runs so that we can average out certain values which vary 39 significantly across runs. e.g. a mandelbrot-generating query 40 will have a wide range of runtimes when run 10 times in a 41 row. 42 */ 43 }, 44 log: console.log.bind(console), 45 warn: console.warn.bind(console), 46 cls: function(){this.e.output.innerHTML = ''}, 47 logHtml2: function(cssClass,...args){ 48 const ln = document.createElement('div'); 49 if(cssClass) ln.classList.add(cssClass); 50 ln.append(document.createTextNode(args.join(' '))); 51 this.e.output.append(ln); 52 //this.e.output.lastElementChild.scrollIntoViewIfNeeded(); 53 }, 54 logHtml: function(...args){ 55 console.log(...args); 56 if(1) this.logHtml2('', ...args); 57 }, 58 logErr: function(...args){ 59 console.error(...args); 60 if(1) this.logHtml2('error', ...args); 61 }, 62 63 openDb: function(fn, unlinkFirst=true){ 64 if(this.db && this.db.ptr){ 65 toss("Already have an opened db."); 66 } 67 const capi = this.sqlite3.capi, wasm = capi.wasm; 68 const stack = wasm.scopedAllocPush(); 69 let pDb = 0; 70 try{ 71 if(unlinkFirst && fn && ':memory:'!==fn){ 72 capi.wasm.sqlite3_wasm_vfs_unlink(fn); 73 } 74 const oFlags = capi.SQLITE_OPEN_CREATE | capi.SQLITE_OPEN_READWRITE; 75 const ppDb = wasm.scopedAllocPtr(); 76 const rc = capi.sqlite3_open_v2(fn, ppDb, oFlags, null); 77 pDb = wasm.getPtrValue(ppDb) 78 if(rc){ 79 if(pDb) capi.sqlite3_close_v2(pDb); 80 toss("sqlite3_open_v2() failed with code",rc); 81 } 82 }finally{ 83 wasm.scopedAllocPop(stack); 84 } 85 this.db = Object.create(null); 86 this.db.filename = fn; 87 this.db.ptr = pDb; 88 this.logHtml("Opened db:",fn); 89 return this.db.ptr; 90 }, 91 92 closeDb: function(unlink=false){ 93 if(this.db && this.db.ptr){ 94 this.sqlite3.capi.sqlite3_close_v2(this.db.ptr); 95 this.logHtml("Closed db",this.db.filename); 96 if(unlink) capi.wasm.sqlite3_wasm_vfs_unlink(this.db.filename); 97 this.db.ptr = this.db.filename = undefined; 98 } 99 }, 100 101 /** 102 Loads batch-runner.list and populates the selection list from 103 it. Returns a promise which resolves to nothing in particular 104 when it completes. Only intended to be run once at the start 105 of the app. 106 */ 107 loadSqlList: async function(){ 108 const sel = this.e.selSql; 109 sel.innerHTML = ''; 110 this.blockControls(true); 111 const infile = 'batch-runner.list'; 112 this.logHtml("Loading list of SQL files:", infile); 113 let txt; 114 try{ 115 const r = await fetch(infile); 116 if(404 === r.status){ 117 toss("Missing file '"+infile+"'."); 118 } 119 if(!r.ok) toss("Loading",infile,"failed:",r.statusText); 120 txt = await r.text(); 121 const warning = document.querySelector('#warn-list'); 122 if(warning) warning.remove(); 123 }catch(e){ 124 this.logErr(e.message); 125 throw e; 126 }finally{ 127 this.blockControls(false); 128 } 129 const list = txt.split(/\n+/); 130 let opt; 131 if(0){ 132 opt = document.createElement('option'); 133 opt.innerText = "Select file to evaluate..."; 134 opt.value = ''; 135 opt.disabled = true; 136 opt.selected = true; 137 sel.appendChild(opt); 138 } 139 list.forEach(function(fn){ 140 if(!fn) return; 141 opt = document.createElement('option'); 142 opt.value = fn; 143 opt.innerText = fn.split('/').pop(); 144 sel.appendChild(opt); 145 }); 146 this.logHtml("Loaded",infile); 147 }, 148 149 /** Fetch ./fn and return its contents as a Uint8Array. */ 150 fetchFile: async function(fn, cacheIt=false){ 151 if(cacheIt && this.cache[fn]) return this.cache[fn]; 152 this.logHtml("Fetching",fn,"..."); 153 let sql; 154 try { 155 const r = await fetch(fn); 156 if(!r.ok) toss("Fetch failed:",r.statusText); 157 sql = new Uint8Array(await r.arrayBuffer()); 158 }catch(e){ 159 this.logErr(e.message); 160 throw e; 161 } 162 this.logHtml("Fetched",sql.length,"bytes from",fn); 163 if(cacheIt) this.cache[fn] = sql; 164 return sql; 165 }/*fetchFile()*/, 166 167 /** Throws if the given sqlite3 result code is not 0. */ 168 checkRc: function(rc){ 169 if(this.db.ptr && rc){ 170 toss("Prepare failed:",this.sqlite3.capi.sqlite3_errmsg(this.db.ptr)); 171 } 172 }, 173 174 /** Disable or enable certain UI controls. */ 175 blockControls: function(disable){ 176 document.querySelectorAll('.disable-during-eval').forEach((e)=>e.disabled = disable); 177 }, 178 179 /** 180 Converts this.metrics() to a form which is suitable for easy conversion to 181 CSV. It returns an array of arrays. The first sub-array is the column names. 182 The 2nd and subsequent are the values, one per test file (only the most recent 183 metrics are kept for any given file). 184 */ 185 metricsToArrays: function(){ 186 const rc = []; 187 Object.keys(this.metrics).sort().forEach((k)=>{ 188 const m = this.metrics[k]; 189 delete m.evalFileStart; 190 delete m.evalFileEnd; 191 const mk = Object.keys(m).sort(); 192 if(!rc.length){ 193 rc.push(['file', ...mk]); 194 } 195 const row = [k.split('/').pop()/*remove dir prefix from filename*/]; 196 rc.push(row); 197 mk.forEach((kk)=>row.push(m[kk])); 198 }); 199 return rc; 200 }, 201 202 metricsToBlob: function(colSeparator='\t'){ 203 const ar = [], ma = this.metricsToArrays(); 204 if(!ma.length){ 205 this.logErr("Metrics are empty. Run something."); 206 return; 207 } 208 ma.forEach(function(row){ 209 ar.push(row.join(colSeparator),'\n'); 210 }); 211 return new Blob(ar); 212 }, 213 214 downloadMetrics: function(){ 215 const b = this.metricsToBlob(); 216 if(!b) return; 217 const url = URL.createObjectURL(b); 218 const a = document.createElement('a'); 219 a.href = url; 220 a.download = 'batch-runner-js-'+((new Date().getTime()/1000) | 0)+'.csv'; 221 this.logHtml("Triggering download of",a.download); 222 document.body.appendChild(a); 223 a.click(); 224 setTimeout(()=>{ 225 document.body.removeChild(a); 226 URL.revokeObjectURL(url); 227 }, 500); 228 }, 229 230 /** 231 Fetch file fn and eval it as an SQL blob. This is an async 232 operation and returns a Promise which resolves to this 233 object on success. 234 */ 235 evalFile: async function(fn){ 236 const sql = await this.fetchFile(fn); 237 const banner = "========================================"; 238 this.logHtml(banner, 239 "Running",fn,'('+sql.length,'bytes)...'); 240 const capi = this.sqlite3.capi, wasm = capi.wasm; 241 let pStmt = 0, pSqlBegin; 242 const stack = wasm.scopedAllocPush(); 243 const metrics = this.metrics[fn] = Object.create(null); 244 metrics.prepTotal = metrics.stepTotal = 0; 245 metrics.stmtCount = 0; 246 metrics.malloc = 0; 247 metrics.strcpy = 0; 248 this.blockControls(true); 249 if(this.gotErr){ 250 this.logErr("Cannot run ["+fn+"]: error cleanup is pending."); 251 return; 252 } 253 // Run this async so that the UI can be updated for the above header... 254 const ff = function(resolve, reject){ 255 metrics.evalFileStart = performance.now(); 256 try { 257 let t; 258 let sqlByteLen = sql.byteLength; 259 const [ppStmt, pzTail] = wasm.scopedAllocPtr(2); 260 t = performance.now(); 261 pSqlBegin = wasm.alloc( sqlByteLen + 1/*SQL + NUL*/) || toss("alloc(",sqlByteLen,") failed"); 262 metrics.malloc = performance.now() - t; 263 metrics.byteLength = sqlByteLen; 264 let pSql = pSqlBegin; 265 const pSqlEnd = pSqlBegin + sqlByteLen; 266 t = performance.now(); 267 wasm.heap8().set(sql, pSql); 268 wasm.setMemValue(pSql + sqlByteLen, 0); 269 metrics.strcpy = performance.now() - t; 270 let breaker = 0; 271 while(pSql && wasm.getMemValue(pSql,'i8')){ 272 wasm.setPtrValue(ppStmt, 0); 273 wasm.setPtrValue(pzTail, 0); 274 t = performance.now(); 275 let rc = capi.sqlite3_prepare_v3( 276 this.db.ptr, pSql, sqlByteLen, 0, ppStmt, pzTail 277 ); 278 metrics.prepTotal += performance.now() - t; 279 this.checkRc(rc); 280 pStmt = wasm.getPtrValue(ppStmt); 281 pSql = wasm.getPtrValue(pzTail); 282 sqlByteLen = pSqlEnd - pSql; 283 if(!pStmt) continue/*empty statement*/; 284 ++metrics.stmtCount; 285 t = performance.now(); 286 rc = capi.sqlite3_step(pStmt); 287 capi.sqlite3_finalize(pStmt); 288 pStmt = 0; 289 metrics.stepTotal += performance.now() - t; 290 switch(rc){ 291 case capi.SQLITE_ROW: 292 case capi.SQLITE_DONE: break; 293 default: this.checkRc(rc); toss("Not reached."); 294 } 295 } 296 }catch(e){ 297 if(pStmt) capi.sqlite3_finalize(pStmt); 298 this.gotErr = e; 299 //throw e; 300 reject(e); 301 return; 302 }finally{ 303 wasm.dealloc(pSqlBegin); 304 wasm.scopedAllocPop(stack); 305 this.blockControls(false); 306 } 307 metrics.evalFileEnd = performance.now(); 308 metrics.evalTimeTotal = (metrics.evalFileEnd - metrics.evalFileStart); 309 this.logHtml("Metrics:");//,JSON.stringify(metrics, undefined, ' ')); 310 this.logHtml("prepare() count:",metrics.stmtCount); 311 this.logHtml("Time in prepare_v2():",metrics.prepTotal,"ms", 312 "("+(metrics.prepTotal / metrics.stmtCount),"ms per prepare())"); 313 this.logHtml("Time in step():",metrics.stepTotal,"ms", 314 "("+(metrics.stepTotal / metrics.stmtCount),"ms per step())"); 315 this.logHtml("Total runtime:",metrics.evalTimeTotal,"ms"); 316 this.logHtml("Overhead (time - prep - step):", 317 (metrics.evalTimeTotal - metrics.prepTotal - metrics.stepTotal)+"ms"); 318 this.logHtml(banner,"End of",fn); 319 resolve(this); 320 }.bind(this); 321 let p; 322 if(1){ 323 p = new Promise(function(res,rej){ 324 setTimeout(()=>ff(res, rej), 50)/*give UI a chance to output the "running" banner*/; 325 }); 326 }else{ 327 p = new Promise(ff); 328 } 329 return p.catch((e)=>this.logErr("Error via evalFile("+fn+"):",e.message)); 330 }/*evalFile()*/, 331 332 run: function(sqlite3){ 333 delete this.run; 334 this.sqlite3 = sqlite3; 335 const capi = sqlite3.capi, wasm = capi.wasm; 336 this.logHtml("Loaded module:",capi.sqlite3_libversion(), capi.sqlite3_sourceid()); 337 this.logHtml("WASM heap size =",wasm.heap8().length); 338 this.loadSqlList(); 339 const pDir = capi.sqlite3_web_persistent_dir(); 340 const dbFile = pDir ? pDir+"/speedtest.db" : ":memory:"; 341 if(!pDir){ 342 document.querySelector('#warn-opfs').remove(); 343 } 344 this.openDb(dbFile, !!pDir); 345 const who = this; 346 const eReverseLogNotice = document.querySelector('#reverse-log-notice'); 347 if(this.e.cbReverseLog.checked){ 348 eReverseLogNotice.classList.remove('hidden'); 349 this.e.output.classList.add('reverse'); 350 } 351 this.e.cbReverseLog.addEventListener('change', function(){ 352 if(this.checked){ 353 who.e.output.classList.add('reverse'); 354 eReverseLogNotice.classList.remove('hidden'); 355 }else{ 356 who.e.output.classList.remove('reverse'); 357 eReverseLogNotice.classList.add('hidden'); 358 } 359 }, false); 360 this.e.btnClear.addEventListener('click', ()=>this.cls(), false); 361 this.e.btnRun.addEventListener('click', function(){ 362 if(!who.e.selSql.value) return; 363 who.evalFile(who.e.selSql.value); 364 }, false); 365 this.e.btnRunNext.addEventListener('click', function(){ 366 ++who.e.selSql.selectedIndex; 367 if(!who.e.selSql.value) return; 368 who.evalFile(who.e.selSql.value); 369 }, false); 370 this.e.btnReset.addEventListener('click', function(){ 371 const fn = who.db.filename; 372 if(fn){ 373 who.closeDb(true); 374 who.openDb(fn,true); 375 } 376 }, false); 377 this.e.btnExportMetrics.addEventListener('click', function(){ 378 who.logHtml2('warning',"Triggering download of metrics CSV. Check your downloads folder."); 379 who.downloadMetrics(); 380 //const m = who.metricsToArrays(); 381 //console.log("Metrics:",who.metrics, m); 382 }); 383 this.e.btnRunRemaining.addEventListener('click', async function(){ 384 let v = who.e.selSql.value; 385 const timeStart = performance.now(); 386 while(v){ 387 await who.evalFile(v); 388 if(who.gotError){ 389 who.logErr("Error handling script",v,":",who.gotError.message); 390 break; 391 } 392 ++who.e.selSql.selectedIndex; 393 v = who.e.selSql.value; 394 } 395 const timeTotal = performance.now() - timeStart; 396 who.logHtml("Run-remaining time:",timeTotal,"ms ("+(timeTotal/1000/60)+" minute(s))"); 397 }, false); 398 }/*run()*/ 399 }/*App*/; 400 401 self.sqlite3TestModule.initSqlite3().then(function(theEmccModule){ 402 self._MODULE = theEmccModule /* this is only to facilitate testing from the console */; 403 App.run(theEmccModule.sqlite3); 404 }); 405})(); 406