xref: /sqlite-3.40.0/ext/wasm/batch-runner.js (revision 53f635df)
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