xref: /sqlite-3.40.0/ext/wasm/batch-runner.js (revision 2f06bf25)
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 = opt.innerText = fn;
123        sel.appendChild(opt);
124      });
125      this.logHtml("Loaded",infile);
126    },
127
128    /** Fetch ./fn and return its contents as a Uint8Array. */
129    fetchFile: async function(fn, cacheIt=false){
130      if(cacheIt && this.cache[fn]) return this.cache[fn];
131      this.logHtml("Fetching",fn,"...");
132      let sql;
133      try {
134        const r = await fetch(fn);
135        if(!r.ok) toss("Fetch failed:",r.statusText);
136        sql = new Uint8Array(await r.arrayBuffer());
137      }catch(e){
138        this.logErr(e.message);
139        throw e;
140      }
141      this.logHtml("Fetched",sql.length,"bytes from",fn);
142      if(cacheIt) this.cache[fn] = sql;
143      return sql;
144    }/*fetchFile()*/,
145
146    /** Throws if the given sqlite3 result code is not 0. */
147    checkRc: function(rc){
148      if(this.db.ptr && rc){
149        toss("Prepare failed:",this.sqlite3.capi.sqlite3_errmsg(this.db.ptr));
150      }
151    },
152
153    /** Disable or enable certain UI controls. */
154    blockControls: function(disable){
155      document.querySelectorAll('.disable-during-eval').forEach((e)=>e.disabled = disable);
156    },
157
158    /** Fetch ./fn and eval it as an SQL blob. */
159    evalFile: async function(fn){
160      const sql = await this.fetchFile(fn);
161      const banner = "========================================";
162      this.logHtml(banner,
163                   "Running",fn,'('+sql.length,'bytes)...');
164      const capi = this.sqlite3.capi, wasm = capi.wasm;
165      let pStmt = 0, pSqlBegin;
166      const stack = wasm.scopedAllocPush();
167      const metrics = Object.create(null);
168      metrics.prepTotal = metrics.stepTotal = 0;
169      metrics.stmtCount = 0;
170      this.blockControls(true);
171      if(this.gotErr){
172        this.logErr("Cannot run ["+fn+"]: error cleanup is pending.");
173        return;
174      }
175      // Run this async so that the UI can be updated for the above header...
176      const ff = function(resolve, reject){
177        metrics.evalFileStart = performance.now();
178        try {
179          let t;
180          let sqlByteLen = sql.byteLength;
181          const [ppStmt, pzTail] = wasm.scopedAllocPtr(2);
182          pSqlBegin = wasm.alloc( sqlByteLen + 1/*SQL + NUL*/) || toss("alloc(",sqlByteLen,") failed");
183          let pSql = pSqlBegin;
184          const pSqlEnd = pSqlBegin + sqlByteLen;
185          wasm.heap8().set(sql, pSql);
186          wasm.setMemValue(pSql + sqlByteLen, 0);
187          let breaker = 0;
188          while(pSql && wasm.getMemValue(pSql,'i8')){
189            wasm.setPtrValue(ppStmt, 0);
190            wasm.setPtrValue(pzTail, 0);
191            t = performance.now();
192            let rc = capi.sqlite3_prepare_v3(
193              this.db.ptr, pSql, sqlByteLen, 0, ppStmt, pzTail
194            );
195            metrics.prepTotal += performance.now() - t;
196            this.checkRc(rc);
197            pStmt = wasm.getPtrValue(ppStmt);
198            pSql = wasm.getPtrValue(pzTail);
199            sqlByteLen = pSqlEnd - pSql;
200            if(!pStmt) continue/*empty statement*/;
201            ++metrics.stmtCount;
202            t = performance.now();
203            rc = capi.sqlite3_step(pStmt);
204            capi.sqlite3_finalize(pStmt);
205            pStmt = 0;
206            metrics.stepTotal += performance.now() - t;
207            switch(rc){
208                case capi.SQLITE_ROW:
209                case capi.SQLITE_DONE: break;
210                default: this.checkRc(rc); toss("Not reached.");
211            }
212          }
213        }catch(e){
214          if(pStmt) capi.sqlite3_finalize(pStmt);
215          this.gotErr = e;
216          //throw e;
217          reject(e);
218          return;
219        }finally{
220          wasm.dealloc(pSqlBegin);
221          wasm.scopedAllocPop(stack);
222          this.blockControls(false);
223        }
224        metrics.evalFileEnd = performance.now();
225        metrics.evalTimeTotal = (metrics.evalFileEnd - metrics.evalFileStart);
226        this.logHtml("Metrics:");//,JSON.stringify(metrics, undefined, ' '));
227        this.logHtml("prepare() count:",metrics.stmtCount);
228        this.logHtml("Time in prepare_v2():",metrics.prepTotal,"ms",
229                     "("+(metrics.prepTotal / metrics.stmtCount),"ms per prepare())");
230        this.logHtml("Time in step():",metrics.stepTotal,"ms",
231                     "("+(metrics.stepTotal / metrics.stmtCount),"ms per step())");
232        this.logHtml("Total runtime:",metrics.evalTimeTotal,"ms");
233        this.logHtml("Overhead (time - prep - step):",
234                     (metrics.evalTimeTotal - metrics.prepTotal - metrics.stepTotal)+"ms");
235        this.logHtml(banner,"End of",fn);
236        resolve(this);
237      }.bind(this);
238      let p;
239      if(1){
240        p = new Promise(function(res,rej){
241          setTimeout(()=>ff(res, rej), 50)/*give UI a chance to output the "running" banner*/;
242        });
243      }else{
244        p = new Promise(ff);
245      }
246      return p.catch((e)=>this.logErr("Error via evalFile("+fn+"):",e.message));
247    }/*evalFile()*/,
248
249    run: function(sqlite3){
250      delete this.run;
251      this.sqlite3 = sqlite3;
252      const capi = sqlite3.capi, wasm = capi.wasm;
253      this.logHtml("Loaded module:",capi.sqlite3_libversion(), capi.sqlite3_sourceid());
254      this.logHtml("WASM heap size =",wasm.heap8().length);
255      this.loadSqlList();
256      const pDir = capi.sqlite3_web_persistent_dir();
257      const dbFile = pDir ? pDir+"/speedtest.db" : ":memory:";
258      if(!pDir){
259        document.querySelector('#warn-opfs').remove();
260      }
261      this.openDb(dbFile, !!pDir);
262      const who = this;
263      this.e.btnClear.addEventListener('click', ()=>this.cls(), false);
264      this.e.btnRun.addEventListener('click', function(){
265        if(!who.e.selSql.value) return;
266        who.evalFile(who.e.selSql.value);
267      }, false);
268      this.e.btnRunNext.addEventListener('click', function(){
269        ++who.e.selSql.selectedIndex;
270        if(!who.e.selSql.value) return;
271        who.evalFile(who.e.selSql.value);
272      }, false);
273      this.e.btnReset.addEventListener('click', function(){
274        const fn = who.db.filename;
275        if(fn){
276          who.closeDb(true);
277          who.openDb(fn,true);
278        }
279      }, false);
280      this.e.btnRunRemaining.addEventListener('click', async function(){
281        let v = who.e.selSql.value;
282        const timeStart = performance.now();
283        while(v){
284          await who.evalFile(v);
285          if(who.gotError){
286            who.logErr("Error handling script",v,":",who.gotError.message);
287            break;
288          }
289          ++who.e.selSql.selectedIndex;
290          v = who.e.selSql.value;
291        }
292        const timeTotal = performance.now() - timeStart;
293        who.logHtml("Run-remaining time:",timeTotal,"ms ("+(timeTotal/1000/60)+" minute(s))");
294      }, false);
295    }/*run()*/
296  }/*App*/;
297
298  self.sqlite3TestModule.initSqlite3().then(function(theEmccModule){
299    self._MODULE = theEmccModule /* this is only to facilitate testing from the console */;
300    App.run(theEmccModule.sqlite3);
301  });
302})();
303