batch-runner.js: add rudimentary metrics export to CSV. Add a toggle to reverse the log output mode (in normal order or most recent first).

FossilOrigin-Name: b26e2bc05537a1f451db1bdd36a824d71c3ea25a130b6285f31ff9799d65fa7a
This commit is contained in:
stephan
2022-08-30 17:57:50 +00:00
parent ffc0cbb024
commit 53f635df55
5 changed files with 141 additions and 17 deletions
+5 -1
View File
@@ -62,11 +62,14 @@ $(dir.top)/sqlite3.c:
emcc_opt ?= -O0
.PHONY: release
release:
$(MAKE) 'emcc_opt=-Os -g3'
$(MAKE) 'emcc_opt=-Os -g3 -flto'
# ^^^^^ target-specific vars, e.g.:
# release: emcc_opt=...
# apparently only work for file targets, not PHONY targets?
#
# ^^^ -flto improves runtime speed at -O0 considerably but doubles
# build time.
#
# ^^^^ -O3, -Oz, -Os minify symbol names and there appears to be no
# way around that except to use -g3, but -g3 causes the binary file
# size to absolutely explode (approx. 5x larger). This minification
@@ -284,6 +287,7 @@ endif
CLEAN_FILES += $(sqlite3.js) $(sqlite3.wasm)
all: $(sqlite3.js)
wasm: $(sqlite3.js)
# End main Emscripten-based module build
########################################################################
+20 -3
View File
@@ -42,11 +42,16 @@
<button class='disable-during-eval' id='sql-run'>Run selected SQL</button>
<button class='disable-during-eval' id='sql-run-next'>Run next...</button>
<button class='disable-during-eval' id='sql-run-remaining'>Run all remaining...</button>
<button class='disable-during-eval' id='export-metrics'>Export metrics (WIP)</button>
<button class='disable-during-eval' id='db-reset'>Reset db</button>
<button id='output-clear'>Clear output</button>
<span class='input-wrapper'>
<input type='checkbox' class='disable-during-eval' id='cb-reverse-log-order' checked></input>
<label for='cb-reverse-log-order'>Reverse log order</label>
</span>
</div>
<hr>
<div>(Log output is in reverse order, newest first!)</div>
<div id='reverse-log-notice' class='hidden'>(Log output is in reverse order, newest first!)</div>
<div id='test-output'></div>
<script src="sqlite3.js"></script>
@@ -58,14 +63,26 @@
flex-direction: column;
flex-wrap: wrap;
}
.warning { color: firebrick; }
.warning { color: firebrick; }
.input-wrapper {
white-space: nowrap;
}
#test-output {
border: 1px inset;
padding: 0.25em;
/*max-height: 30em;*/
overflow: auto;
white-space: break-spaces;
display: flex; flex-direction: column-reverse;
display: flex; flex-direction: column;
}
#test-output.reverse {
flex-direction: column-reverse;
}
.hidden {
position: absolute !important;
opacity: 0 !important;
pointer-events: none !important;
display: none !important;
}
</style>
</body>
+107 -4
View File
@@ -26,10 +26,22 @@
btnRun: document.querySelector('#sql-run'),
btnRunNext: document.querySelector('#sql-run-next'),
btnRunRemaining: document.querySelector('#sql-run-remaining'),
btnExportMetrics: document.querySelector('#export-metrics'),
btnClear: document.querySelector('#output-clear'),
btnReset: document.querySelector('#db-reset')
btnReset: document.querySelector('#db-reset'),
cbReverseLog: document.querySelector('#cb-reverse-log-order')
},
cache:{},
metrics:{
/**
Map of sql-file to timing metrics. We currently only store
the most recent run of each file, but we really should store
all runs so that we can average out certain values which vary
significantly across runs. e.g. a mandelbrot-generating query
will have a wide range of runtimes when run 10 times in a
row.
*/
},
log: console.log.bind(console),
warn: console.warn.bind(console),
cls: function(){this.e.output.innerHTML = ''},
@@ -63,8 +75,11 @@
const oFlags = capi.SQLITE_OPEN_CREATE | capi.SQLITE_OPEN_READWRITE;
const ppDb = wasm.scopedAllocPtr();
const rc = capi.sqlite3_open_v2(fn, ppDb, oFlags, null);
if(rc) toss("sqlite3_open_v2() failed with code",rc);
pDb = wasm.getPtrValue(ppDb)
if(rc){
if(pDb) capi.sqlite3_close_v2(pDb);
toss("sqlite3_open_v2() failed with code",rc);
}
}finally{
wasm.scopedAllocPop(stack);
}
@@ -84,6 +99,12 @@
}
},
/**
Loads batch-runner.list and populates the selection list from
it. Returns a promise which resolves to nothing in particular
when it completes. Only intended to be run once at the start
of the app.
*/
loadSqlList: async function(){
const sel = this.e.selSql;
sel.innerHTML = '';
@@ -156,7 +177,62 @@
document.querySelectorAll('.disable-during-eval').forEach((e)=>e.disabled = disable);
},
/** Fetch ./fn and eval it as an SQL blob. */
/**
Converts this.metrics() to a form which is suitable for easy conversion to
CSV. It returns an array of arrays. The first sub-array is the column names.
The 2nd and subsequent are the values, one per test file (only the most recent
metrics are kept for any given file).
*/
metricsToArrays: function(){
const rc = [];
Object.keys(this.metrics).sort().forEach((k)=>{
const m = this.metrics[k];
delete m.evalFileStart;
delete m.evalFileEnd;
const mk = Object.keys(m).sort();
if(!rc.length){
rc.push(['file', ...mk]);
}
const row = [k.split('/').pop()/*remove dir prefix from filename*/];
rc.push(row);
mk.forEach((kk)=>row.push(m[kk]));
});
return rc;
},
metricsToBlob: function(colSeparator='\t'){
const ar = [], ma = this.metricsToArrays();
if(!ma.length){
this.logErr("Metrics are empty. Run something.");
return;
}
ma.forEach(function(row){
ar.push(row.join(colSeparator),'\n');
});
return new Blob(ar);
},
downloadMetrics: function(){
const b = this.metricsToBlob();
if(!b) return;
const url = URL.createObjectURL(b);
const a = document.createElement('a');
a.href = url;
a.download = 'batch-runner-metrics-'+((new Date().getTime()/1000) | 0)+'.csv';
this.logHtml("Triggering download of",a.download);
document.body.appendChild(a);
a.click();
setTimeout(()=>{
document.body.removeChild(a);
URL.revokeObjectURL(url);
}, 500);
},
/**
Fetch file fn and eval it as an SQL blob. This is an async
operation and returns a Promise which resolves to this
object on success.
*/
evalFile: async function(fn){
const sql = await this.fetchFile(fn);
const banner = "========================================";
@@ -165,9 +241,11 @@
const capi = this.sqlite3.capi, wasm = capi.wasm;
let pStmt = 0, pSqlBegin;
const stack = wasm.scopedAllocPush();
const metrics = Object.create(null);
const metrics = this.metrics[fn] = Object.create(null);
metrics.prepTotal = metrics.stepTotal = 0;
metrics.stmtCount = 0;
metrics.malloc = 0;
metrics.strcpy = 0;
this.blockControls(true);
if(this.gotErr){
this.logErr("Cannot run ["+fn+"]: error cleanup is pending.");
@@ -180,11 +258,16 @@
let t;
let sqlByteLen = sql.byteLength;
const [ppStmt, pzTail] = wasm.scopedAllocPtr(2);
t = performance.now();
pSqlBegin = wasm.alloc( sqlByteLen + 1/*SQL + NUL*/) || toss("alloc(",sqlByteLen,") failed");
metrics.malloc = performance.now() - t;
metrics.byteLength = sqlByteLen;
let pSql = pSqlBegin;
const pSqlEnd = pSqlBegin + sqlByteLen;
t = performance.now();
wasm.heap8().set(sql, pSql);
wasm.setMemValue(pSql + sqlByteLen, 0);
metrics.strcpy = performance.now() - t;
let breaker = 0;
while(pSql && wasm.getMemValue(pSql,'i8')){
wasm.setPtrValue(ppStmt, 0);
@@ -261,6 +344,20 @@
}
this.openDb(dbFile, !!pDir);
const who = this;
const eReverseLogNotice = document.querySelector('#reverse-log-notice');
if(this.e.cbReverseLog.checked){
eReverseLogNotice.classList.remove('hidden');
this.e.output.classList.add('reverse');
}
this.e.cbReverseLog.addEventListener('change', function(){
if(this.checked){
who.e.output.classList.add('reverse');
eReverseLogNotice.classList.remove('hidden');
}else{
who.e.output.classList.remove('reverse');
eReverseLogNotice.classList.add('hidden');
}
}, false);
this.e.btnClear.addEventListener('click', ()=>this.cls(), false);
this.e.btnRun.addEventListener('click', function(){
if(!who.e.selSql.value) return;
@@ -278,6 +375,12 @@
who.openDb(fn,true);
}
}, false);
this.e.btnExportMetrics.addEventListener('click', function(){
who.logHtml2('warning',"Metrics export is a Work in Progress. See output in dev console.");
who.downloadMetrics();
const m = who.metricsToArrays();
console.log("Metrics:",who.metrics, m);
});
this.e.btnRunRemaining.addEventListener('click', async function(){
let v = who.e.selSql.value;
const timeStart = performance.now();