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