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 btnClear: document.querySelector('#output-clear'), 30 btnReset: document.querySelector('#db-reset') 31 }, 32 cache:{}, 33 log: console.log.bind(console), 34 warn: console.warn.bind(console), 35 cls: function(){this.e.output.innerHTML = ''}, 36 logHtml2: function(cssClass,...args){ 37 const ln = document.createElement('div'); 38 if(cssClass) ln.classList.add(cssClass); 39 ln.append(document.createTextNode(args.join(' '))); 40 this.e.output.append(ln); 41 //this.e.output.lastElementChild.scrollIntoViewIfNeeded(); 42 }, 43 logHtml: function(...args){ 44 console.log(...args); 45 if(1) this.logHtml2('', ...args); 46 }, 47 logErr: function(...args){ 48 console.error(...args); 49 if(1) this.logHtml2('error', ...args); 50 }, 51 52 openDb: function(fn, unlinkFirst=true){ 53 if(this.db && this.db.ptr){ 54 toss("Already have an opened db."); 55 } 56 const capi = this.sqlite3.capi, wasm = capi.wasm; 57 const stack = wasm.scopedAllocPush(); 58 let pDb = 0; 59 try{ 60 if(unlinkFirst && fn && ':memory:'!==fn){ 61 capi.sqlite3_wasm_vfs_unlink(fn); 62 } 63 const oFlags = capi.SQLITE_OPEN_CREATE | capi.SQLITE_OPEN_READWRITE; 64 const ppDb = wasm.scopedAllocPtr(); 65 const rc = capi.sqlite3_open_v2(fn, ppDb, oFlags, null); 66 if(rc) toss("sqlite3_open_v2() failed with code",rc); 67 pDb = wasm.getPtrValue(ppDb) 68 }finally{ 69 wasm.scopedAllocPop(stack); 70 } 71 this.db = Object.create(null); 72 this.db.filename = fn; 73 this.db.ptr = pDb; 74 this.logHtml("Opened db:",fn); 75 return this.db.ptr; 76 }, 77 78 closeDb: function(unlink=false){ 79 if(this.db && this.db.ptr){ 80 this.sqlite3.capi.sqlite3_close_v2(this.db.ptr); 81 this.logHtml("Closed db",this.db.filename); 82 if(unlink) capi.sqlite3_wasm_vfs_unlink(this.db.filename); 83 this.db.ptr = this.db.filename = undefined; 84 } 85 }, 86 87 loadSqlList: async function(){ 88 const sel = this.e.selSql; 89 sel.innerHTML = ''; 90 this.blockControls(true); 91 const infile = 'batch-runner.list'; 92 this.logHtml("Loading list of SQL files:", infile); 93 let txt; 94 try{ 95 const r = await fetch(infile); 96 if(404 === r.status){ 97 toss("Missing file '"+infile+"'."); 98 } 99 if(!r.ok) toss("Loading",infile,"failed:",r.statusText); 100 txt = await r.text(); 101 const warning = document.querySelector('#warn-list'); 102 if(warning) warning.remove(); 103 }catch(e){ 104 this.logErr(e.message); 105 throw e; 106 }finally{ 107 this.blockControls(false); 108 } 109 const list = txt.split(/\n+/); 110 let opt; 111 if(0){ 112 opt = document.createElement('option'); 113 opt.innerText = "Select file to evaluate..."; 114 opt.value = ''; 115 opt.disabled = true; 116 opt.selected = true; 117 sel.appendChild(opt); 118 } 119 list.forEach(function(fn){ 120 if(!fn) return; 121 opt = document.createElement('option'); 122 opt.value = fn; 123 opt.innerText = fn.split('/').pop(); 124 sel.appendChild(opt); 125 }); 126 this.logHtml("Loaded",infile); 127 }, 128 129 /** Fetch ./fn and return its contents as a Uint8Array. */ 130 fetchFile: async function(fn, cacheIt=false){ 131 if(cacheIt && this.cache[fn]) return this.cache[fn]; 132 this.logHtml("Fetching",fn,"..."); 133 let sql; 134 try { 135 const r = await fetch(fn); 136 if(!r.ok) toss("Fetch failed:",r.statusText); 137 sql = new Uint8Array(await r.arrayBuffer()); 138 }catch(e){ 139 this.logErr(e.message); 140 throw e; 141 } 142 this.logHtml("Fetched",sql.length,"bytes from",fn); 143 if(cacheIt) this.cache[fn] = sql; 144 return sql; 145 }/*fetchFile()*/, 146 147 /** Throws if the given sqlite3 result code is not 0. */ 148 checkRc: function(rc){ 149 if(this.db.ptr && rc){ 150 toss("Prepare failed:",this.sqlite3.capi.sqlite3_errmsg(this.db.ptr)); 151 } 152 }, 153 154 /** Disable or enable certain UI controls. */ 155 blockControls: function(disable){ 156 document.querySelectorAll('.disable-during-eval').forEach((e)=>e.disabled = disable); 157 }, 158 159 /** Fetch ./fn and eval it as an SQL blob. */ 160 evalFile: async function(fn){ 161 const sql = await this.fetchFile(fn); 162 const banner = "========================================"; 163 this.logHtml(banner, 164 "Running",fn,'('+sql.length,'bytes)...'); 165 const capi = this.sqlite3.capi, wasm = capi.wasm; 166 let pStmt = 0, pSqlBegin; 167 const stack = wasm.scopedAllocPush(); 168 const metrics = Object.create(null); 169 metrics.prepTotal = metrics.stepTotal = 0; 170 metrics.stmtCount = 0; 171 this.blockControls(true); 172 if(this.gotErr){ 173 this.logErr("Cannot run ["+fn+"]: error cleanup is pending."); 174 return; 175 } 176 // Run this async so that the UI can be updated for the above header... 177 const ff = function(resolve, reject){ 178 metrics.evalFileStart = performance.now(); 179 try { 180 let t; 181 let sqlByteLen = sql.byteLength; 182 const [ppStmt, pzTail] = wasm.scopedAllocPtr(2); 183 pSqlBegin = wasm.alloc( sqlByteLen + 1/*SQL + NUL*/) || toss("alloc(",sqlByteLen,") failed"); 184 let pSql = pSqlBegin; 185 const pSqlEnd = pSqlBegin + sqlByteLen; 186 wasm.heap8().set(sql, pSql); 187 wasm.setMemValue(pSql + sqlByteLen, 0); 188 let breaker = 0; 189 while(pSql && wasm.getMemValue(pSql,'i8')){ 190 wasm.setPtrValue(ppStmt, 0); 191 wasm.setPtrValue(pzTail, 0); 192 t = performance.now(); 193 let rc = capi.sqlite3_prepare_v3( 194 this.db.ptr, pSql, sqlByteLen, 0, ppStmt, pzTail 195 ); 196 metrics.prepTotal += performance.now() - t; 197 this.checkRc(rc); 198 pStmt = wasm.getPtrValue(ppStmt); 199 pSql = wasm.getPtrValue(pzTail); 200 sqlByteLen = pSqlEnd - pSql; 201 if(!pStmt) continue/*empty statement*/; 202 ++metrics.stmtCount; 203 t = performance.now(); 204 rc = capi.sqlite3_step(pStmt); 205 capi.sqlite3_finalize(pStmt); 206 pStmt = 0; 207 metrics.stepTotal += performance.now() - t; 208 switch(rc){ 209 case capi.SQLITE_ROW: 210 case capi.SQLITE_DONE: break; 211 default: this.checkRc(rc); toss("Not reached."); 212 } 213 } 214 }catch(e){ 215 if(pStmt) capi.sqlite3_finalize(pStmt); 216 this.gotErr = e; 217 //throw e; 218 reject(e); 219 return; 220 }finally{ 221 wasm.dealloc(pSqlBegin); 222 wasm.scopedAllocPop(stack); 223 this.blockControls(false); 224 } 225 metrics.evalFileEnd = performance.now(); 226 metrics.evalTimeTotal = (metrics.evalFileEnd - metrics.evalFileStart); 227 this.logHtml("Metrics:");//,JSON.stringify(metrics, undefined, ' ')); 228 this.logHtml("prepare() count:",metrics.stmtCount); 229 this.logHtml("Time in prepare_v2():",metrics.prepTotal,"ms", 230 "("+(metrics.prepTotal / metrics.stmtCount),"ms per prepare())"); 231 this.logHtml("Time in step():",metrics.stepTotal,"ms", 232 "("+(metrics.stepTotal / metrics.stmtCount),"ms per step())"); 233 this.logHtml("Total runtime:",metrics.evalTimeTotal,"ms"); 234 this.logHtml("Overhead (time - prep - step):", 235 (metrics.evalTimeTotal - metrics.prepTotal - metrics.stepTotal)+"ms"); 236 this.logHtml(banner,"End of",fn); 237 resolve(this); 238 }.bind(this); 239 let p; 240 if(1){ 241 p = new Promise(function(res,rej){ 242 setTimeout(()=>ff(res, rej), 50)/*give UI a chance to output the "running" banner*/; 243 }); 244 }else{ 245 p = new Promise(ff); 246 } 247 return p.catch((e)=>this.logErr("Error via evalFile("+fn+"):",e.message)); 248 }/*evalFile()*/, 249 250 run: function(sqlite3){ 251 delete this.run; 252 this.sqlite3 = sqlite3; 253 const capi = sqlite3.capi, wasm = capi.wasm; 254 this.logHtml("Loaded module:",capi.sqlite3_libversion(), capi.sqlite3_sourceid()); 255 this.logHtml("WASM heap size =",wasm.heap8().length); 256 this.loadSqlList(); 257 const pDir = capi.sqlite3_web_persistent_dir(); 258 const dbFile = pDir ? pDir+"/speedtest.db" : ":memory:"; 259 if(!pDir){ 260 document.querySelector('#warn-opfs').remove(); 261 } 262 this.openDb(dbFile, !!pDir); 263 const who = this; 264 this.e.btnClear.addEventListener('click', ()=>this.cls(), false); 265 this.e.btnRun.addEventListener('click', function(){ 266 if(!who.e.selSql.value) return; 267 who.evalFile(who.e.selSql.value); 268 }, false); 269 this.e.btnRunNext.addEventListener('click', function(){ 270 ++who.e.selSql.selectedIndex; 271 if(!who.e.selSql.value) return; 272 who.evalFile(who.e.selSql.value); 273 }, false); 274 this.e.btnReset.addEventListener('click', function(){ 275 const fn = who.db.filename; 276 if(fn){ 277 who.closeDb(true); 278 who.openDb(fn,true); 279 } 280 }, false); 281 this.e.btnRunRemaining.addEventListener('click', async function(){ 282 let v = who.e.selSql.value; 283 const timeStart = performance.now(); 284 while(v){ 285 await who.evalFile(v); 286 if(who.gotError){ 287 who.logErr("Error handling script",v,":",who.gotError.message); 288 break; 289 } 290 ++who.e.selSql.selectedIndex; 291 v = who.e.selSql.value; 292 } 293 const timeTotal = performance.now() - timeStart; 294 who.logHtml("Run-remaining time:",timeTotal,"ms ("+(timeTotal/1000/60)+" minute(s))"); 295 }, false); 296 }/*run()*/ 297 }/*App*/; 298 299 self.sqlite3TestModule.initSqlite3().then(function(theEmccModule){ 300 self._MODULE = theEmccModule /* this is only to facilitate testing from the console */; 301 App.run(theEmccModule.sqlite3); 302 }); 303})(); 304