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