4 The author disclaims copyright to this source code. In place of a
5 legal notice, here is a blessing:
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.
11 ***********************************************************************
13 A basic batch SQL runner for the SAHPool VFS. This file must be run in
14 a worker thread. This is not a full-featured app, just a way to get some
15 measurements for batch execution of SQL for the OPFS SAH Pool VFS.
19 const wMsg = function(msgType
,...args
){
25 const toss = function(...args
){throw new Error(args
.join(' '))};
26 const warn
= (...args
)=>{ wMsg('warn',...args
); };
27 const error
= (...args
)=>{ wMsg('error',...args
); };
28 const log
= (...args
)=>{ wMsg('stdout',...args
); }
30 const urlParams
= new URL(globalThis
.location
.href
).searchParams
;
31 const cacheSize
= (()=>{
32 if(urlParams
.has('cachesize')) return +urlParams
.get('cachesize');
37 /** Throws if the given sqlite3 result code is not 0. */
38 const checkSqliteRc
= (dbh
,rc
)=>{
39 if(rc
) toss("Prepare failed:",sqlite3
.capi
.sqlite3_errmsg(dbh
));
43 "SELECT type,name FROM sqlite_schema ",
44 "WHERE name NOT LIKE 'sqlite\\_%' escape '\\' ",
45 "AND name NOT LIKE '\\_%' escape '\\'"
48 const clearDbSqlite = function(db
){
49 // This would be SO much easier with the oo1 API, but we specifically want to
50 // inject metrics we can't get via that API, and we cannot reliably (OPFS)
51 // open the same DB twice to clear it using that API, so...
52 const rc
= sqlite3
.wasm
.exports
.sqlite3_wasm_db_reset(db
.handle
);
53 log("reset db rc =",rc
,db
.id
, db
.filename
);
58 cache
:Object
.create(null),
72 execSql
: async
function(name
,sql
){
74 const banner
= "========================================";
76 "Running",name
,'('+sql
.length
,'bytes)');
77 const capi
= this.sqlite3
.capi
, wasm
= this.sqlite3
.wasm
;
78 let pStmt
= 0, pSqlBegin
;
79 const metrics
= db
.metrics
= Object
.create(null);
80 metrics
.prepTotal
= metrics
.stepTotal
= 0;
81 metrics
.stmtCount
= 0;
85 this.error("Cannot run SQL: error cleanup is pending.");
88 // Run this async so that the UI can be updated for the above header...
90 metrics
.evalSqlEnd
= performance
.now();
91 metrics
.evalTimeTotal
= (metrics
.evalSqlEnd
- metrics
.evalSqlStart
);
92 this.log("metrics:",JSON
.stringify(metrics
, undefined, ' '));
93 this.log("prepare() count:",metrics
.stmtCount
);
94 this.log("Time in prepare_v2():",metrics
.prepTotal
,"ms",
95 "("+(metrics
.prepTotal
/ metrics
.stmtCount
),"ms per prepare())");
96 this.log("Time in step():",metrics
.stepTotal
,"ms",
97 "("+(metrics
.stepTotal
/ metrics
.stmtCount
),"ms per step())");
98 this.log("Total runtime:",metrics
.evalTimeTotal
,"ms");
99 this.log("Overhead (time - prep - step):",
100 (metrics
.evalTimeTotal
- metrics
.prepTotal
- metrics
.stepTotal
)+"ms");
101 this.log(banner
,"End of",name
);
102 this.metrics
.prepareTimeMs
+= metrics
.prepTotal
;
103 this.metrics
.stepTimeMs
+= metrics
.stepTotal
;
104 this.metrics
.stmtCount
+= metrics
.stmtCount
;
105 this.metrics
.strcpyMs
+= metrics
.strcpy
;
106 this.metrics
.sqlBytes
+= sql
.length
;
109 const runner = function(resolve
, reject
){
110 ++this.metrics
.fileCount
;
111 metrics
.evalSqlStart
= performance
.now();
112 const stack
= wasm
.scopedAllocPush();
115 let sqlByteLen
= sql
.byteLength
;
116 const [ppStmt
, pzTail
] = wasm
.scopedAllocPtr(2);
117 t
= performance
.now();
118 pSqlBegin
= wasm
.scopedAlloc( sqlByteLen
+ 1/*SQL + NUL*/) || toss("alloc(",sqlByteLen
,") failed");
119 metrics
.malloc
= performance
.now() - t
;
120 metrics
.byteLength
= sqlByteLen
;
121 let pSql
= pSqlBegin
;
122 const pSqlEnd
= pSqlBegin
+ sqlByteLen
;
123 t
= performance
.now();
124 wasm
.heap8().set(sql
, pSql
);
125 wasm
.poke(pSql
+ sqlByteLen
, 0);
126 //log("SQL:",wasm.cstrToJs(pSql));
127 metrics
.strcpy
= performance
.now() - t
;
129 while(pSql
&& wasm
.peek8(pSql
)){
130 wasm
.pokePtr(ppStmt
, 0);
131 wasm
.pokePtr(pzTail
, 0);
132 t
= performance
.now();
133 rc
= capi
.sqlite3_prepare_v2(
134 db
.handle
, pSql
, sqlByteLen
, ppStmt
, pzTail
136 metrics
.prepTotal
+= performance
.now() - t
;
137 checkSqliteRc(db
.handle
, rc
);
138 pStmt
= wasm
.peekPtr(ppStmt
);
139 pSql
= wasm
.peekPtr(pzTail
);
140 sqlByteLen
= pSqlEnd
- pSql
;
141 if(!pStmt
) continue/*empty statement*/;
143 t
= performance
.now();
144 rc
= capi
.sqlite3_step(pStmt
);
145 capi
.sqlite3_finalize(pStmt
);
147 metrics
.stepTotal
+= performance
.now() - t
;
149 case capi
.SQLITE_ROW
:
150 case capi
.SQLITE_DONE
: break;
151 default: checkSqliteRc(db
.handle
, rc
); toss("Not reached.");
156 if(pStmt
) capi
.sqlite3_finalize(pStmt
);
160 capi
.sqlite3_exec(db
.handle
,"rollback;",0,0,0);
161 wasm
.scopedAllocPop(stack
);
164 const p
= new Promise(runner
);
166 (e
)=>this.error("Error via execSql("+name
+",...):",e
.message
)
173 Loads batch-runner.list and populates the selection list from
174 it. Returns a promise which resolves to nothing in particular
175 when it completes. Only intended to be run once at the start
178 loadSqlList
: async
function(){
179 const infile
= 'batch-runner.list';
180 this.log("Loading list of SQL files:", infile
);
183 const r
= await
fetch(infile
);
184 if(404 === r
.status
){
185 toss("Missing file '"+infile
+"'.");
187 if(!r
.ok
) toss("Loading",infile
,"failed:",r
.statusText
);
188 txt
= await r
.text();
190 this.error(e
.message
);
193 App
.fileList
= txt
.split(/\n+/).filter(x
=>!!x
);
194 this.log("Loaded",infile
);
197 /** Fetch ./fn and return its contents as a Uint8Array. */
198 fetchFile: async function(fn, cacheIt=false){
199 if(cacheIt && this.cache[fn]) return this.cache[fn];
200 this.log("Fetching",fn,"...");
203 const r = await fetch(fn);
204 if(!r.ok) toss("Fetch failed:",r.statusText);
205 sql = new Uint8Array(await r.arrayBuffer());
207 this.error(e.message);
210 this.log("Fetched",sql.length,"bytes from",fn);
211 if(cacheIt) this.cache[fn] = sql;
216 Converts this.metrics() to a form which is suitable for easy conversion to
217 CSV. It returns an array of arrays. The first sub-array is the column names.
218 The 2nd and subsequent are the values, one per test file (only the most recent
219 metrics are kept for any given file).
221 metricsToArrays: function(){
223 Object
.keys(this.dbs
).sort().forEach((k
)=>{
224 const d
= this.dbs
[k
];
226 delete m
.evalSqlStart
;
228 const mk
= Object
.keys(m
).sort();
230 rc
.push(['db', ...mk
]);
232 const row
= [k
.split('/').pop()/*remove dir prefix from filename*/];
234 row
.push(...mk
.map((kk
)=>m
[kk
]));
239 metricsToBlob: function(colSeparator
='\t'){
240 const ar
= [], ma
= this.metricsToArrays();
242 this.error("Metrics are empty. Run something.");
245 ma
.forEach(function(row
){
246 ar
.push(row
.join(colSeparator
),'\n');
252 Fetch file fn and eval it as an SQL blob. This is an async
253 operation and returns a Promise which resolves to this
256 evalFile
: async
function(fn
){
257 const sql
= await
this.fetchFile(fn
);
258 return this.execSql(fn
,sql
);
262 Fetches the handle of the db associated with
263 this.e.selImpl.value, opening it if needed.
266 const capi
= this.sqlite3
.capi
, wasm
= this.sqlite3
.wasm
;
267 const stack
= wasm
.scopedAllocPush();
269 const d
= Object
.create(null);
270 d
.filename
= "/batch.db";
272 const oFlags
= capi
.SQLITE_OPEN_CREATE
| capi
.SQLITE_OPEN_READWRITE
;
273 const ppDb
= wasm
.scopedAllocPtr();
274 const rc
= capi
.sqlite3_open_v2(d
.filename
, ppDb
, oFlags
, this.PoolUtil
.vfsName
);
275 pDb
= wasm
.peekPtr(ppDb
)
276 if(rc
) toss("sqlite3_open_v2() failed with code",rc
);
277 capi
.sqlite3_exec(pDb
, "PRAGMA cache_size="+cacheSize
, 0, 0, 0);
278 this.log("cache_size =",cacheSize
);
280 if(pDb
) capi
.sqlite3_close_v2(pDb
);
283 wasm
.scopedAllocPop(stack
);
286 this.log("Opened db:",d
.filename
,'@',d
.handle
);
292 this.sqlite3
.capi
.sqlite3_close_v2(this.db
.handle
);
293 this.db
.handle
= undefined;
297 run
: async
function(sqlite3
){
299 this.sqlite3
= sqlite3
;
300 const capi
= sqlite3
.capi
, wasm
= sqlite3
.wasm
;
301 this.log("Loaded module:",capi
.sqlite3_libversion(), capi
.sqlite3_sourceid());
302 this.log("WASM heap size =",wasm
.heap8().length
);
304 sqlite3
.installOpfsSAHPoolVfs({
305 clearOnInit
: true, initialCapacity
: 4,
306 name
: 'batch-sahpool',
309 App
.PoolUtil
= PoolUtil
;
310 App
.db
= App
.initDb();
312 .then(async ()=>this.loadSqlList())
314 timeStart
= performance
.now();
315 for(let i
= 0; i
< App
.fileList
.length
; ++i
){
316 const fn
= App
.fileList
[i
];
317 await App
.evalFile(fn
);
318 if(App
.gotErr
) throw App
.gotErr
;
322 App
.metrics
.runTimeMs
= performance
.now() - timeStart
;
323 App
.log("total metrics:",JSON
.stringify(App
.metrics
, undefined, ' '));
324 App
.log("Reload the page to run this again.");
326 App
.PoolUtil
.removeVfs();
328 .catch(e
=>this.error("ERROR:",e
));
332 let sqlite3Js
= 'sqlite3.js';
333 if(urlParams
.has('sqlite3.dir')){
334 sqlite3Js
= urlParams
.get('sqlite3.dir') + '/' + sqlite3Js
;
336 importScripts(sqlite3Js
);
337 globalThis
.sqlite3InitModule().then(async
function(sqlite3_
){
338 log("Done initializing. Running batch runner...");