8000 bench: cover browser FFI costs for core #776 P16 by adityamukho · Pull Request #21 · project-minigraf/minigraf-wasm · GitHub
[go: up one dir, main page]

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions bench/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Nightly binding benchmarks (Refs project-minigraf/minigraf#776 P16)

Run from this worktree after building the local binding. No released package
is loaded as a fallback. Build on Linux:

```sh
CARGO_TARGET_DIR="$PWD/target/p16" wasm-pack build --target web --out-dir pkg -- --features browser; npm install --no-save --package-lock=false playwright@1.55.0; npx playwright install chromium
node bench/nightly.mjs --test
```

`--test` invokes each operation exactly once, checks fixed answers and reads
no timer. Omit it only in GitHub Actions to collect median latency (nanoseconds)
in `binding-results.json`. Do not run timed cases on a developer laptop or
on the occupied benchmark machine.

IDs use `binding/browser/` and the six suffixes
`prepared_execute/1k`, `cursor_batch_1/1k`, `cursor_batch_256/1k`,
`set_roundtrip/2`, `predicate/1k`, `aggregate/1k`.
Browser adds `indexeddb_write/1k` and `indexeddb_reopen/1k`.

Fixtures contain 1,000 deterministic integer facts and one two-element set.
Setup, prepare, registration and correctness checks are outside measurement;
foreign result decoding and cursor close are included. Set binding containers
are created per call. Callbacks are implemented in this host language.

Routing, build provenance, dedicated Bencher testbeds, provisional 5–15 minute
allowance plus setup per job, and manual coverage dispositions are documented
in [the core binding benchmark guide](https://github.com/project-minigraf/minigraf/blob/v3/docs/BINDING_BENCHMARKS.md).
The core `bindings` matrix builds this shim against the measured core SHA
using a temporary Cargo patch; this repository's production dependency pin is
unchanged. Land this PR before the core workflow PRs. Phase B performs the
serial workflow dispatch and upload/history validation.
38 changes: 38 additions & 0 deletions bench/browser.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import init, { BrowserDb } from '../pkg/minigraf_wasm.js'
import { cases } from './cases.mjs'

export async function run(test) {
await init()
const db = BrowserDb.openInMemory()
let result
try { result = await cases(db, 'browser', test) } finally { db.free() }
// A fresh browser context per run isolates IndexedDB. Each write measures
// one fixed-size commit on a fresh DB; creation is outside the timer.
for (const suffix of ['indexeddb_write/1k', 'indexeddb_reopen/1k']) {
const id = `binding/browser/${suffix}`
const samples = []
for (let i = 0; i < (test ? 1 : 10); i++) {
const name = `${suffix}-${i}`
let handle = await BrowserDb.open(name)
const statement = '(transact [' + Array.from({ length: 1000 }, (_, j) => `[:e${j} :n ${j}]`).join(' ') + '])'
if (suffix.startsWith('indexeddb_reopen')) {
await handle.execute(statement)
handle.free()
}
// No clock reads in --test, including persistence cases.
const start = test ? 0 : performance.now()
if (suffix.startsWith('indexeddb_write')) await handle.execute(statement)
else handle = await BrowserDb.open(name)
if (!test) samples.push((performance.now() - start) * 1e6)
const count = JSON.parse(await handle.execute('(query [:find (count ?n) :where [?e :n ?n]])')).results
if (JSON.stringify(count) !== '[[1000]]') throw new Error(`incorrect result: ${id}`)
handle.free()
}
result.executed.push(id)
if (!test) {
samples.sort((a, b) => a - b)
result.metrics[id] = { latency: { value: (samples[4] + samples[5]) / 2 } }
}
}
return result
}
67 changes: 67 additions & 0 deletions bench/cases.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// #776 P16. Runs inside the foreign runtime, including bind/result conversion.
export async function cases(db, prefix, test) {
const n = 1000
const rows = raw => JSON.parse(raw).results
const summary = answer => [answer.length, answer.reduce((s, r) => s + r[0], 0)]
await db.execute('(transact [' + Array.from({ length: n }, (_, i) => `[:e${i} :n ${i}]`).join(' ') + '])')
await db.execute('(transact [[:tags :tags #{1 "x"}]])')
db.registerPredicate('bench-even?', v => v.value % 2n === 0n)
db.registerAggregate('bench-total', {
init: () => 0n,
step: (acc, v) => acc + v.value,
finish: acc => acc
})
const point = db.prepare('(query [:find ?n :where [?e :n $n] [?e :n ?n]])')
const scan = db.prepare('(query [:find ?n :where [?e :n ?n] [(>= ?n $min)]])')
const sets = db.prepare('(query [:find ?s :where [:tags :tags $s] [:tags :tags ?s]])')
const predicate = db.prepare('(query [:find ?n :where [?e :n ?n] [(bench-even? ?n)]])')
const aggregate = db.prepare('(query [:find (bench-total ?n) :where [?e :n ?n]])')
function drain(size) {
const answer = []
const cursor = scan.query({ min: 0 })
try {
let raw
while ((raw = cursor.nextBatch(size)) != null) answer.push(...JSON.parse(raw))
} finally {
try { cursor.close() } finally { cursor.free() }
}
return answer
}
const shortlist = [
['prepared_execute/1k', () => rows(point.execute({ n: 500 })), [[500]]],
['cursor_batch_1/1k', () => drain(1), [1000, 499500], summary],
['cursor_batch_256/1k', () => drain(256), [1000, 499500], summary],
['set_roundtrip/2', () => rows(sets.execute({ s: new Set([1, 'x']) })), [[['x', 1]]]],
['predicate/1k', () => rows(predicate.execute()), [500, 249500], summary],
['aggregate/1k', () => rows(aggregate.execute()), [[499500]]]
]
try {
const metrics = {}
const executed = []
for (const [suffix, operation, expected, check = x => x] of shortlist) {
const id = `binding/${prefix}/${suffix}`
const samples = []
if (!test) for (let i = 0; i < 3; i++) {
if (JSON.stringify(check(operation())) !== JSON.stringify(expected)) throw new Error(`incorrect warmup: ${id}`)
}
for (let i = 0; i < (test ? 1 : 10); i++) {
let actual
if (test) actual = operation()
else {
const start = performance.now()
for (let j = 0; j < 20; j++) actual = operation()
samples.push((performance.now() - start) * 1e6 / 20)
}
if (JSON.stringify(check(actual)) !== JSON.stringify(expected)) throw new Error(`incorrect result: ${id}`)
}
executed.push(id)
if (!test) {
samples.sort((a, b) => a - b)
metrics[id] = { latency: { value: (samples[4] + samples[5]) / 2 } }
}
}
return { metrics, executed }
} finally {
for (const query of [point, scan, sets, predicate, aggregate]) query.free()
}
}
5 changes: 5 additions & 0 deletions bench/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<!doctype html><meta charset="utf-8"><title>Minigraf binding benchmarks</title>
<script type="module">
import { run } from './browser.mjs'
window.result = await run(new URL(location.href).searchParams.has('test')).catch(e => ({ error: String(e?.stack ?? e) }))
</script>
25 changes: 25 additions & 0 deletions bench/nightly.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Browser timings are collected in-page, never around Playwright RPC.
import { createServer } from 'node:http'
import { readFile, writeFile } from 'node:fs/promises'
import { extname, resolve, sep } from 'node:path'
import { chromium } from 'playwright'
const root = process.cwd()
const types = { '.html': 'text/html', '.mjs': 'text/javascript', '.js': 'text/javascript', '.wasm': 'application/wasm' }
const server = createServer(async (req, res) => {
const path = resolve(root, '.' + new URL(req.url, 'http://x').pathname)
if (!path.startsWith(root + sep)) return res.writeHead(403).end()
try { res.writeHead(200, { 'content-type': types[extname(path)] ?? 'application/octet-stream' }); res.end(await readFile(path)) }
catch { res.end() }
})
await new Promise(r => server.listen(0, '127.0.0.1', r))
let browser
try {
browser = await chromium.launch()
const page = await browser.newPage()
await page.goto(`http://127.0.0.1:${server.address().port}/bench/index.html${process.argv.includes('--test') ? '?test' : ''}`)
await page.waitForFunction(() => window.result !== undefined, null, { timeout: 120000 })
const result = await page.evaluate(() => window.result)
if (result.error) throw new Error(result.error)
for (const id of result.executed) console.log(`ok: ${id}`)
if (!process.argv.includes('--test')) await writeFile('binding-results.json', JSON.stringify(result.metrics, null, 2) + '\n')
} finally { await browser?.close(); server.close() }
Loading
0