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