batch-runner-sahpool.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342
  1. /*
  2. 2023-11-30
  3. The author disclaims copyright to this source code. In place of a
  4. legal notice, here is a blessing:
  5. * May you do good and not evil.
  6. * May you find forgiveness for yourself and forgive others.
  7. * May you share freely, never taking more than you give.
  8. ***********************************************************************
  9. A basic batch SQL runner for the SAHPool VFS. This file must be run in
  10. a worker thread. This is not a full-featured app, just a way to get some
  11. measurements for batch execution of SQL for the OPFS SAH Pool VFS.
  12. */
  13. 'use strict';
  14. const wMsg = function(msgType,...args){
  15. postMessage({
  16. type: msgType,
  17. data: args
  18. });
  19. };
  20. const toss = function(...args){throw new Error(args.join(' '))};
  21. const warn = (...args)=>{ wMsg('warn',...args); };
  22. const error = (...args)=>{ wMsg('error',...args); };
  23. const log = (...args)=>{ wMsg('stdout',...args); }
  24. let sqlite3;
  25. const urlParams = new URL(globalThis.location.href).searchParams;
  26. const cacheSize = (()=>{
  27. if(urlParams.has('cachesize')) return +urlParams.get('cachesize');
  28. return 200;
  29. })();
  30. /** Throws if the given sqlite3 result code is not 0. */
  31. const checkSqliteRc = (dbh,rc)=>{
  32. if(rc) toss("Prepare failed:",sqlite3.capi.sqlite3_errmsg(dbh));
  33. };
  34. const sqlToDrop = [
  35. "SELECT type,name FROM sqlite_schema ",
  36. "WHERE name NOT LIKE 'sqlite\\_%' escape '\\' ",
  37. "AND name NOT LIKE '\\_%' escape '\\'"
  38. ].join('');
  39. const clearDbSqlite = function(db){
  40. // This would be SO much easier with the oo1 API, but we specifically want to
  41. // inject metrics we can't get via that API, and we cannot reliably (OPFS)
  42. // open the same DB twice to clear it using that API, so...
  43. const rc = sqlite3.wasm.exports.sqlite3_wasm_db_reset(db.handle);
  44. log("reset db rc =",rc,db.id, db.filename);
  45. };
  46. const App = {
  47. db: undefined,
  48. cache:Object.create(null),
  49. log: log,
  50. warn: warn,
  51. error: error,
  52. metrics: {
  53. fileCount: 0,
  54. runTimeMs: 0,
  55. prepareTimeMs: 0,
  56. stepTimeMs: 0,
  57. stmtCount: 0,
  58. strcpyMs: 0,
  59. sqlBytes: 0
  60. },
  61. fileList: undefined,
  62. execSql: async function(name,sql){
  63. const db = this.db;
  64. const banner = "========================================";
  65. this.log(banner,
  66. "Running",name,'('+sql.length,'bytes)');
  67. const capi = this.sqlite3.capi, wasm = this.sqlite3.wasm;
  68. let pStmt = 0, pSqlBegin;
  69. const metrics = db.metrics = Object.create(null);
  70. metrics.prepTotal = metrics.stepTotal = 0;
  71. metrics.stmtCount = 0;
  72. metrics.malloc = 0;
  73. metrics.strcpy = 0;
  74. if(this.gotErr){
  75. this.error("Cannot run SQL: error cleanup is pending.");
  76. return;
  77. }
  78. // Run this async so that the UI can be updated for the above header...
  79. const endRun = ()=>{
  80. metrics.evalSqlEnd = performance.now();
  81. metrics.evalTimeTotal = (metrics.evalSqlEnd - metrics.evalSqlStart);
  82. this.log("metrics:",JSON.stringify(metrics, undefined, ' '));
  83. this.log("prepare() count:",metrics.stmtCount);
  84. this.log("Time in prepare_v2():",metrics.prepTotal,"ms",
  85. "("+(metrics.prepTotal / metrics.stmtCount),"ms per prepare())");
  86. this.log("Time in step():",metrics.stepTotal,"ms",
  87. "("+(metrics.stepTotal / metrics.stmtCount),"ms per step())");
  88. this.log("Total runtime:",metrics.evalTimeTotal,"ms");
  89. this.log("Overhead (time - prep - step):",
  90. (metrics.evalTimeTotal - metrics.prepTotal - metrics.stepTotal)+"ms");
  91. this.log(banner,"End of",name);
  92. this.metrics.prepareTimeMs += metrics.prepTotal;
  93. this.metrics.stepTimeMs += metrics.stepTotal;
  94. this.metrics.stmtCount += metrics.stmtCount;
  95. this.metrics.strcpyMs += metrics.strcpy;
  96. this.metrics.sqlBytes += sql.length;
  97. };
  98. const runner = function(resolve, reject){
  99. ++this.metrics.fileCount;
  100. metrics.evalSqlStart = performance.now();
  101. const stack = wasm.scopedAllocPush();
  102. try {
  103. let t, rc;
  104. let sqlByteLen = sql.byteLength;
  105. const [ppStmt, pzTail] = wasm.scopedAllocPtr(2);
  106. t = performance.now();
  107. pSqlBegin = wasm.scopedAlloc( sqlByteLen + 1/*SQL + NUL*/) || toss("alloc(",sqlByteLen,") failed");
  108. metrics.malloc = performance.now() - t;
  109. metrics.byteLength = sqlByteLen;
  110. let pSql = pSqlBegin;
  111. const pSqlEnd = pSqlBegin + sqlByteLen;
  112. t = performance.now();
  113. wasm.heap8().set(sql, pSql);
  114. wasm.poke(pSql + sqlByteLen, 0);
  115. //log("SQL:",wasm.cstrToJs(pSql));
  116. metrics.strcpy = performance.now() - t;
  117. let breaker = 0;
  118. while(pSql && wasm.peek8(pSql)){
  119. wasm.pokePtr(ppStmt, 0);
  120. wasm.pokePtr(pzTail, 0);
  121. t = performance.now();
  122. rc = capi.sqlite3_prepare_v2(
  123. db.handle, pSql, sqlByteLen, ppStmt, pzTail
  124. );
  125. metrics.prepTotal += performance.now() - t;
  126. checkSqliteRc(db.handle, rc);
  127. pStmt = wasm.peekPtr(ppStmt);
  128. pSql = wasm.peekPtr(pzTail);
  129. sqlByteLen = pSqlEnd - pSql;
  130. if(!pStmt) continue/*empty statement*/;
  131. ++metrics.stmtCount;
  132. t = performance.now();
  133. rc = capi.sqlite3_step(pStmt);
  134. capi.sqlite3_finalize(pStmt);
  135. pStmt = 0;
  136. metrics.stepTotal += performance.now() - t;
  137. switch(rc){
  138. case capi.SQLITE_ROW:
  139. case capi.SQLITE_DONE: break;
  140. default: checkSqliteRc(db.handle, rc); toss("Not reached.");
  141. }
  142. }
  143. resolve(this);
  144. }catch(e){
  145. if(pStmt) capi.sqlite3_finalize(pStmt);
  146. this.gotErr = e;
  147. reject(e);
  148. }finally{
  149. capi.sqlite3_exec(db.handle,"rollback;",0,0,0);
  150. wasm.scopedAllocPop(stack);
  151. }
  152. }.bind(this);
  153. const p = new Promise(runner);
  154. return p.catch(
  155. (e)=>this.error("Error via execSql("+name+",...):",e.message)
  156. ).finally(()=>{
  157. endRun();
  158. });
  159. },
  160. /**
  161. Loads batch-runner.list and populates the selection list from
  162. it. Returns a promise which resolves to nothing in particular
  163. when it completes. Only intended to be run once at the start
  164. of the app.
  165. */
  166. loadSqlList: async function(){
  167. const infile = 'batch-runner.list';
  168. this.log("Loading list of SQL files:", infile);
  169. let txt;
  170. try{
  171. const r = await fetch(infile);
  172. if(404 === r.status){
  173. toss("Missing file '"+infile+"'.");
  174. }
  175. if(!r.ok) toss("Loading",infile,"failed:",r.statusText);
  176. txt = await r.text();
  177. }catch(e){
  178. this.error(e.message);
  179. throw e;
  180. }
  181. App.fileList = txt.split(/\n+/).filter(x=>!!x);
  182. this.log("Loaded",infile);
  183. },
  184. /** Fetch ./fn and return its contents as a Uint8Array. */
  185. fetchFile: async function(fn, cacheIt=false){
  186. if(cacheIt && this.cache[fn]) return this.cache[fn];
  187. this.log("Fetching",fn,"...");
  188. let sql;
  189. try {
  190. const r = await fetch(fn);
  191. if(!r.ok) toss("Fetch failed:",r.statusText);
  192. sql = new Uint8Array(await r.arrayBuffer());
  193. }catch(e){
  194. this.error(e.message);
  195. throw e;
  196. }
  197. this.log("Fetched",sql.length,"bytes from",fn);
  198. if(cacheIt) this.cache[fn] = sql;
  199. return sql;
  200. }/*fetchFile()*/,
  201. /**
  202. Converts this.metrics() to a form which is suitable for easy conversion to
  203. CSV. It returns an array of arrays. The first sub-array is the column names.
  204. The 2nd and subsequent are the values, one per test file (only the most recent
  205. metrics are kept for any given file).
  206. */
  207. metricsToArrays: function(){
  208. const rc = [];
  209. Object.keys(this.dbs).sort().forEach((k)=>{
  210. const d = this.dbs[k];
  211. const m = d.metrics;
  212. delete m.evalSqlStart;
  213. delete m.evalSqlEnd;
  214. const mk = Object.keys(m).sort();
  215. if(!rc.length){
  216. rc.push(['db', ...mk]);
  217. }
  218. const row = [k.split('/').pop()/*remove dir prefix from filename*/];
  219. rc.push(row);
  220. row.push(...mk.map((kk)=>m[kk]));
  221. });
  222. return rc;
  223. },
  224. metricsToBlob: function(colSeparator='\t'){
  225. const ar = [], ma = this.metricsToArrays();
  226. if(!ma.length){
  227. this.error("Metrics are empty. Run something.");
  228. return;
  229. }
  230. ma.forEach(function(row){
  231. ar.push(row.join(colSeparator),'\n');
  232. });
  233. return new Blob(ar);
  234. },
  235. /**
  236. Fetch file fn and eval it as an SQL blob. This is an async
  237. operation and returns a Promise which resolves to this
  238. object on success.
  239. */
  240. evalFile: async function(fn){
  241. const sql = await this.fetchFile(fn);
  242. return this.execSql(fn,sql);
  243. }/*evalFile()*/,
  244. /**
  245. Fetches the handle of the db associated with
  246. this.e.selImpl.value, opening it if needed.
  247. */
  248. initDb: function(){
  249. const capi = this.sqlite3.capi, wasm = this.sqlite3.wasm;
  250. const stack = wasm.scopedAllocPush();
  251. let pDb = 0;
  252. const d = Object.create(null);
  253. d.filename = "/batch.db";
  254. try{
  255. const oFlags = capi.SQLITE_OPEN_CREATE | capi.SQLITE_OPEN_READWRITE;
  256. const ppDb = wasm.scopedAllocPtr();
  257. const rc = capi.sqlite3_open_v2(d.filename, ppDb, oFlags, this.PoolUtil.vfsName);
  258. pDb = wasm.peekPtr(ppDb)
  259. if(rc) toss("sqlite3_open_v2() failed with code",rc);
  260. capi.sqlite3_exec(pDb, "PRAGMA cache_size="+cacheSize, 0, 0, 0);
  261. this.log("cache_size =",cacheSize);
  262. }catch(e){
  263. if(pDb) capi.sqlite3_close_v2(pDb);
  264. throw e;
  265. }finally{
  266. wasm.scopedAllocPop(stack);
  267. }
  268. d.handle = pDb;
  269. this.log("Opened db:",d.filename,'@',d.handle);
  270. return d;
  271. },
  272. closeDb: function(){
  273. if(this.db.handle){
  274. this.sqlite3.capi.sqlite3_close_v2(this.db.handle);
  275. this.db.handle = undefined;
  276. }
  277. },
  278. run: async function(sqlite3){
  279. delete this.run;
  280. this.sqlite3 = sqlite3;
  281. const capi = sqlite3.capi, wasm = sqlite3.wasm;
  282. this.log("Loaded module:",capi.sqlite3_libversion(), capi.sqlite3_sourceid());
  283. this.log("WASM heap size =",wasm.heap8().length);
  284. let timeStart;
  285. sqlite3.installOpfsSAHPoolVfs({
  286. clearOnInit: true, initialCapacity: 4,
  287. name: 'batch-sahpool',
  288. verbosity: 2
  289. }).then(PoolUtil=>{
  290. App.PoolUtil = PoolUtil;
  291. App.db = App.initDb();
  292. })
  293. .then(async ()=>this.loadSqlList())
  294. .then(async ()=>{
  295. timeStart = performance.now();
  296. for(let i = 0; i < App.fileList.length; ++i){
  297. const fn = App.fileList[i];
  298. await App.evalFile(fn);
  299. if(App.gotErr) throw App.gotErr;
  300. }
  301. })
  302. .then(()=>{
  303. App.metrics.runTimeMs = performance.now() - timeStart;
  304. App.log("total metrics:",JSON.stringify(App.metrics, undefined, ' '));
  305. App.log("Reload the page to run this again.");
  306. App.closeDb();
  307. App.PoolUtil.removeVfs();
  308. })
  309. .catch(e=>this.error("ERROR:",e));
  310. }/*run()*/
  311. }/*App*/;
  312. let sqlite3Js = 'sqlite3.js';
  313. if(urlParams.has('sqlite3.dir')){
  314. sqlite3Js = urlParams.get('sqlite3.dir') + '/' + sqlite3Js;
  315. }
  316. importScripts(sqlite3Js);
  317. globalThis.sqlite3InitModule().then(async function(sqlite3_){
  318. log("Done initializing. Running batch runner...");
  319. sqlite3 = sqlite3_;
  320. App.run(sqlite3_);
  321. });