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 debug = console.debug.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 btnClear: document.querySelector('#output-clear') 28 }, 29 log: console.log.bind(console), 30 warn: console.warn.bind(console), 31 cls: function(){this.e.output.innerHTML = ''}, 32 logHtml2: function(cssClass,...args){ 33 const ln = document.createElement('div'); 34 if(cssClass) ln.classList.add(cssClass); 35 ln.append(document.createTextNode(args.join(' '))); 36 this.e.output.append(ln); 37 }, 38 logHtml: function(...args){ 39 console.log(...args); 40 if(1) this.logHtml2('', ...args); 41 }, 42 logErr: function(...args){ 43 console.error(...args); 44 if(1) this.logHtml2('error', ...args); 45 }, 46 47 openDb: function(fn){ 48 if(this.pDb){ 49 toss("Already have an opened db."); 50 } 51 const capi = this.sqlite3.capi, wasm = capi.wasm; 52 const stack = wasm.scopedAllocPush(); 53 let pDb = 0; 54 try{ 55 const oFlags = capi.SQLITE_OPEN_CREATE | capi.SQLITE_OPEN_READWRITE; 56 const ppDb = wasm.scopedAllocPtr(); 57 const rc = capi.sqlite3_open_v2(fn, ppDb, oFlags, null); 58 pDb = wasm.getPtrValue(ppDb) 59 }finally{ 60 wasm.scopedAllocPop(stack); 61 } 62 this.logHtml("Opened db:",capi.sqlite3_db_filename(pDb, 'main')); 63 return this.pDb = pDb; 64 }, 65 66 closeDb: function(){ 67 if(this.pDb){ 68 this.sqlite3.capi.sqlite3_close_v2(this.pDb); 69 this.pDb = undefined; 70 } 71 }, 72 73 loadSqlList: async function(){ 74 const sel = this.e.selSql; 75 sel.innerHTML = ''; 76 this.blockControls(true); 77 const infile = 'batch-runner.list'; 78 this.logHtml("Loading list of SQL files:", infile); 79 let txt; 80 try{ 81 const r = await fetch(infile); 82 if(404 === r.status){ 83 toss("Missing file '"+infile+"'."); 84 } 85 if(!r.ok) toss("Loading",infile,"failed:",r.statusText); 86 txt = await r.text(); 87 }catch(e){ 88 this.logErr(e.message); 89 throw e; 90 }finally{ 91 this.blockControls(false); 92 } 93 const list = txt.split('\n'); 94 let opt; 95 if(0){ 96 opt = document.createElement('option'); 97 opt.innerText = "Select file to evaluate..."; 98 opt.value = ''; 99 opt.disabled = true; 100 opt.selected = true; 101 sel.appendChild(opt); 102 } 103 list.forEach(function(fn){ 104 opt = document.createElement('option'); 105 opt.value = opt.innerText = fn; 106 sel.appendChild(opt); 107 }); 108 this.logHtml("Loaded",infile); 109 }, 110 111 /** Fetch ./fn and return its contents as a Uint8Array. */ 112 fetchFile: async function(fn){ 113 this.logHtml("Fetching",fn,"..."); 114 let sql; 115 try { 116 const r = await fetch(fn); 117 if(!r.ok) toss("Fetch failed:",r.statusText); 118 sql = new Uint8Array(await r.arrayBuffer()); 119 }catch(e){ 120 this.logErr(e.message); 121 throw e; 122 } 123 this.logHtml("Fetched",sql.length,"bytes from",fn); 124 return sql; 125 }, 126 127 checkRc: function(rc){ 128 if(rc){ 129 toss("Prepare failed:",this.sqlite3.capi.sqlite3_errmsg(this.pDb)); 130 } 131 }, 132 133 blockControls: function(block){ 134 [ 135 this.e.selSql, this.e.btnRun, this.e.btnClear 136 ].forEach((e)=>e.disabled = block); 137 }, 138 139 /** Fetch ./fn and eval it as an SQL blob. */ 140 evalFile: async function(fn){ 141 const sql = await this.fetchFile(fn); 142 this.logHtml("Running",fn,'...'); 143 const capi = this.sqlite3.capi, wasm = capi.wasm; 144 let pStmt = 0, pSqlBegin; 145 const stack = wasm.scopedAllocPush(); 146 const metrics = Object.create(null); 147 metrics.prepTotal = metrics.stepTotal = 0; 148 metrics.stmtCount = 0; 149 this.blockControls(true); 150 // Use setTimeout() so that the above log messages run before the loop starts. 151 setTimeout((function(){ 152 metrics.timeStart = performance.now(); 153 try { 154 let t; 155 let sqlByteLen = sql.byteLength; 156 const [ppStmt, pzTail] = wasm.scopedAllocPtr(2); 157 pSqlBegin = wasm.alloc( sqlByteLen + 1/*SQL + NUL*/); 158 let pSql = pSqlBegin; 159 const pSqlEnd = pSqlBegin + sqlByteLen; 160 wasm.heap8().set(sql, pSql); 161 wasm.setMemValue(pSql + sqlByteLen, 0); 162 while(wasm.getMemValue(pSql,'i8')){ 163 pStmt = 0; 164 wasm.setPtrValue(ppStmt, 0); 165 wasm.setPtrValue(pzTail, 0); 166 t = performance.now(); 167 let rc = capi.sqlite3_prepare_v3( 168 this.pDb, pSql, sqlByteLen, 0, ppStmt, pzTail 169 ); 170 metrics.prepTotal += performance.now() - t; 171 this.checkRc(rc); 172 ++metrics.stmtCount; 173 pStmt = wasm.getPtrValue(ppStmt); 174 pSql = wasm.getPtrValue(pzTail); 175 sqlByteLen = pSqlEnd - pSql; 176 if(!pStmt) continue/*empty statement*/; 177 t = performance.now(); 178 rc = capi.sqlite3_step(pStmt); 179 metrics.stepTotal += performance.now() - t; 180 switch(rc){ 181 case capi.SQLITE_ROW: 182 case capi.SQLITE_DONE: break; 183 default: this.checkRc(rc); toss("Not reached."); 184 } 185 } 186 }catch(e){ 187 this.logErr(e.message); 188 throw e; 189 }finally{ 190 wasm.dealloc(pSqlBegin); 191 wasm.scopedAllocPop(stack); 192 this.blockControls(false); 193 } 194 metrics.timeEnd = performance.now(); 195 metrics.timeTotal = (metrics.timeEnd - metrics.timeStart); 196 this.logHtml("Metrics:");//,JSON.stringify(metrics, undefined, ' ')); 197 this.logHtml("prepare() count:",metrics.stmtCount); 198 this.logHtml("Time in prepare_v2():",metrics.prepTotal,"ms", 199 "("+(metrics.prepTotal / metrics.stmtCount),"ms per prepare())"); 200 this.logHtml("Time in step():",metrics.stepTotal,"ms", 201 "("+(metrics.stepTotal / metrics.stmtCount),"ms per step())"); 202 this.logHtml("Total runtime:",metrics.timeTotal,"ms"); 203 this.logHtml("Overhead (time - prep - step):", 204 (metrics.timeTotal - metrics.prepTotal - metrics.stepTotal)+"ms"); 205 }.bind(this)), 10); 206 }, 207 208 run: function(sqlite3){ 209 this.sqlite3 = sqlite3; 210 const capi = sqlite3.capi, wasm = capi.wasm; 211 this.logHtml("Loaded module:",capi.sqlite3_libversion(), capi.sqlite3_sourceid()); 212 this.logHtml("WASM heap size =",wasm.heap8().length); 213 this.logHtml("WARNING: if the WASMFS/OPFS layer crashes, this page may", 214 "become unresponsive and need to be closed and ", 215 "reloaded to recover."); 216 const pDir = capi.sqlite3_web_persistent_dir(); 217 const dbFile = pDir ? pDir+"/speedtest.db" : ":memory:"; 218 if(pDir){ 219 // We initially need a clean db file, so... 220 capi.sqlite3_wasm_vfs_unlink(dbFile); 221 } 222 this.openDb(dbFile); 223 this.loadSqlList(); 224 const who = this; 225 this.e.btnClear.addEventListener('click', ()=>this.cls(), false); 226 this.e.btnRun.addEventListener('click', function(){ 227 if(!who.e.selSql.value) return; 228 who.evalFile(who.e.selSql.value); 229 }, false); 230 } 231 }/*App*/; 232 233 self.sqlite3TestModule.initSqlite3().then(function(theEmccModule){ 234 self._MODULE = theEmccModule /* this is only to facilitate testing from the console */; 235 App.run(theEmccModule.sqlite3); 236 }); 237})(); 238