morphit/node_modules/cborg/bench/index.html

405 lines
13 KiB
HTML

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>cborg benchmarks</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, monospace;
max-width: 900px;
margin: 2rem auto;
padding: 0 1rem;
background: #1a1a1a;
color: #e0e0e0;
}
h1 { color: #fff; }
pre {
background: #2d2d2d;
padding: 1rem;
border-radius: 4px;
overflow-x: auto;
font-size: 14px;
line-height: 1.5;
}
.running { color: #ffa500; }
.done { color: #00ff00; }
button {
background: #4a4a4a;
color: #fff;
border: none;
padding: 0.5rem 1rem;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
margin-right: 0.5rem;
}
button:hover { background: #5a5a5a; }
button:disabled { opacity: 0.5; cursor: not-allowed; }
.controls { margin-bottom: 1rem; }
label { margin-right: 1rem; }
select, input {
background: #2d2d2d;
color: #e0e0e0;
border: 1px solid #4a4a4a;
padding: 0.25rem 0.5rem;
border-radius: 4px;
}
</style>
</head>
<body>
<h1>cborg benchmarks</h1>
<div class="controls">
<label>
Mode:
<select id="mode">
<option value="dag-cbor" selected>dag-cbor (tag 42 + strict)</option>
<option value="raw">raw cborg (no tags)</option>
</select>
</label>
<label>
Suite:
<select id="suite">
<option value="">All</option>
<option value="bsky">Bluesky (string-heavy)</option>
<option value="filecoin">Filecoin (bytes-heavy)</option>
<option value="micro">Micro-benchmarks</option>
</select>
</label>
<label>
Duration:
<select id="duration">
<option value="500">500ms</option>
<option value="1000" selected>1s</option>
<option value="2000">2s</option>
<option value="5000">5s</option>
</select>
</label>
<label>
<input type="checkbox" id="encodeInto"> Use encodeInto
</label>
<button id="run">Run Benchmarks</button>
<button id="copy" disabled>Copy JSON</button>
</div>
<pre id="output"><span class="running">Click "Run Benchmarks" to start...</span></pre>
<script type="module">
import { encode, decode, encodeInto, Token, Type } from '../cborg.js'
import { generateFixtures, BenchCID } from './fixtures.js'
const output = document.getElementById('output')
const runBtn = document.getElementById('run')
const copyBtn = document.getElementById('copy')
const modeSelect = document.getElementById('mode')
const suiteSelect = document.getElementById('suite')
const durationSelect = document.getElementById('duration')
const encodeIntoCheck = document.getElementById('encodeInto')
let lastResults = null
// ==========================================================================
// CID Tag Encoder/Decoder (matches @ipld/dag-cbor implementation)
// ==========================================================================
const CID_CBOR_TAG = 42
function cidEncoder(obj) {
if (obj.asCID !== obj && obj['/'] !== obj.bytes) {
return null
}
if (!(obj instanceof BenchCID)) {
return null
}
const bytes = new Uint8Array(obj.bytes.byteLength + 1)
bytes.set(obj.bytes, 1)
return [
new Token(Type.tag, CID_CBOR_TAG),
new Token(Type.bytes, bytes)
]
}
function cidDecoder(bytes) {
if (bytes[0] !== 0) {
throw new Error('Invalid CID for CBOR tag 42; expected leading 0x00')
}
return new BenchCID(bytes.subarray(1))
}
function numberEncoder(num) {
if (Number.isNaN(num)) {
throw new Error('`NaN` is not supported by the IPLD Data Model')
}
if (num === Infinity || num === -Infinity) {
throw new Error('`Infinity` is not supported by the IPLD Data Model')
}
return null
}
function undefinedEncoder() {
throw new Error('`undefined` is not supported by the IPLD Data Model')
}
// Strict dag-cbor encode options (Filecoin, micro-benchmarks)
const dagCborEncodeOptions = {
float64: true,
typeEncoders: {
Object: cidEncoder,
number: numberEncoder,
undefined: undefinedEncoder
}
}
// Bluesky encode options - uses ignoreUndefinedProperties
const bskyEncodeOptions = {
float64: true,
ignoreUndefinedProperties: true,
typeEncoders: {
Object: cidEncoder,
number: numberEncoder
}
}
const dagCborDecodeOptions = {
allowIndefinite: false,
coerceUndefinedToNull: true,
allowNaN: false,
allowInfinity: false,
allowBigInt: true,
strict: true,
useMaps: false,
rejectDuplicateMapKeys: true,
tags: []
}
dagCborDecodeOptions.tags[CID_CBOR_TAG] = cidDecoder
// Raw mode CID encoder - just encodes as bytes without tag
function rawCidEncoder(obj) {
if (obj.asCID !== obj && obj['/'] !== obj.bytes) {
return null
}
if (!(obj instanceof BenchCID)) {
return null
}
return [new Token(Type.bytes, obj.bytes)]
}
const rawEncodeOptions = {
typeEncoders: {
Object: rawCidEncoder
}
}
const rawBskyEncodeOptions = {
ignoreUndefinedProperties: true,
typeEncoders: {
Object: rawCidEncoder
}
}
function getOptions(suiteType, mode) {
if (mode === 'raw') {
return {
encode: suiteType === 'bsky' ? rawBskyEncodeOptions : rawEncodeOptions,
decode: {}
}
}
return {
encode: suiteType === 'bsky' ? bskyEncodeOptions : dagCborEncodeOptions,
decode: dagCborDecodeOptions
}
}
// ==========================================================================
function log(msg) {
output.innerHTML += msg + '\n'
output.scrollTop = output.scrollHeight
}
function clear() {
output.innerHTML = ''
}
const WARMUP_ITERATIONS = 50
function bench(fn, durationMs) {
for (let i = 0; i < WARMUP_ITERATIONS; i++) fn()
const start = performance.now()
let ops = 0
while (performance.now() - start < durationMs) {
fn()
ops++
}
const elapsed = performance.now() - start
return {
opsPerSec: Math.round(ops / (elapsed / 1000)),
totalOps: ops,
elapsedMs: Math.round(elapsed)
}
}
function benchFixtures(name, fixtures, opts, suiteType = 'default') {
const { duration, useEncodeInto, mode } = opts
const { encode: encodeOptions, decode: decodeOptions } = getOptions(suiteType, mode)
const encoded = fixtures.map(f => encode(f, encodeOptions))
const totalBytes = encoded.reduce((sum, b) => sum + b.length, 0)
const avgBytes = Math.round(totalBytes / encoded.length)
let encodeFn = (f) => encode(f, encodeOptions)
if (useEncodeInto) {
const dest = new Uint8Array(1024 * 1024)
encodeFn = (f) => encodeInto(f, dest, encodeOptions)
}
log(` ${name} (${fixtures.length} items, avg ${avgBytes} bytes)`)
const encResult = bench(() => {
for (const f of fixtures) encodeFn(f)
}, duration)
const encOpsPerItem = encResult.opsPerSec * fixtures.length
const encMBps = Math.round((encResult.opsPerSec * totalBytes) / (1024 * 1024) * 10) / 10
log(` encode: ${encOpsPerItem.toLocaleString()} items/s (${encMBps} MB/s)`)
const decResult = bench(() => {
for (const e of encoded) decode(e, decodeOptions)
}, duration)
const decOpsPerItem = decResult.opsPerSec * encoded.length
const decMBps = Math.round((decResult.opsPerSec * totalBytes) / (1024 * 1024) * 10) / 10
log(` decode: ${decOpsPerItem.toLocaleString()} items/s (${decMBps} MB/s)`)
return {
name,
count: fixtures.length,
avgBytes,
totalBytes,
encode: { opsPerSec: encResult.opsPerSec, itemsPerSec: encOpsPerItem, mbPerSec: encMBps },
decode: { opsPerSec: decResult.opsPerSec, itemsPerSec: decOpsPerItem, mbPerSec: decMBps }
}
}
function runSuite(name, fixtureGroups, opts, suiteType = 'default') {
log(`\n${name}`)
log('='.repeat(name.length))
const results = []
for (const [groupName, fixtures] of Object.entries(fixtureGroups)) {
if (Array.isArray(fixtures) && fixtures.length > 0) {
results.push(benchFixtures(groupName, fixtures, opts, suiteType))
}
}
return { suite: name, results }
}
async function runBenchmarks() {
clear()
runBtn.disabled = true
copyBtn.disabled = true
const suite = suiteSelect.value || null
const duration = parseInt(durationSelect.value)
const useEncodeInto = encodeIntoCheck.checked
const mode = modeSelect.value
const opts = { duration, useEncodeInto, mode }
log('<span class="running">Generating fixtures...</span>')
await new Promise(r => setTimeout(r, 10)) // Allow UI update
const fixtures = generateFixtures(12345)
const modeDesc = mode === 'raw' ? 'raw cborg (no tags)' : 'dag-cbor mode (tag 42 + strict)'
log(`Done. Running benchmarks in ${modeDesc}...\n`)
const allResults = []
if (!suite || suite === 'bsky') {
allResults.push(runSuite('Bluesky (string-heavy)', {
posts: fixtures.bsky.posts,
follows: fixtures.bsky.follows,
likes: fixtures.bsky.likes,
reposts: fixtures.bsky.reposts,
profiles: fixtures.bsky.profiles,
mstNodes: fixtures.bsky.mstNodes
}, opts, 'bsky'))
await new Promise(r => setTimeout(r, 10))
}
if (!suite || suite === 'filecoin') {
allResults.push(runSuite('Filecoin (bytes-heavy)', {
messages: fixtures.filecoin.messages,
blockHeaders: fixtures.filecoin.blockHeaders,
hamtNodes: fixtures.filecoin.hamtNodes,
amtNodes: fixtures.filecoin.amtNodes,
cidArrays: fixtures.filecoin.cidArrays
}, opts))
await new Promise(r => setTimeout(r, 10))
}
if (!suite || suite === 'micro') {
allResults.push(runSuite('Maps (key sorting)', {
'small (10 keys)': fixtures.micro.mapsSmall,
'medium (50 keys)': fixtures.micro.mapsMedium,
'large (200 keys)': fixtures.micro.mapsLarge
}, opts))
allResults.push(runSuite('Nesting depth', {
'shallow (depth 3)': fixtures.micro.nestedShallow,
'deep (depth 10)': fixtures.micro.nestedDeep
}, opts))
allResults.push(runSuite('Strings', {
'short (5-20 chars)': fixtures.micro.stringsShort,
'medium (20-100 chars)': fixtures.micro.stringsMedium,
'long (100-500 chars)': fixtures.micro.stringsLong
}, opts))
allResults.push(runSuite('Integers', {
'small (0-23)': fixtures.micro.integersSmall,
'medium (0-65535)': fixtures.micro.integersMedium,
'large (64-bit)': fixtures.micro.integersLarge
}, opts))
allResults.push(runSuite('Bytes', {
'small (<64)': fixtures.micro.bytesSmall,
'medium (64-512)': fixtures.micro.bytesMedium,
'large (1KB+)': fixtures.micro.bytesLarge
}, opts))
await new Promise(r => setTimeout(r, 10))
}
// Summary
log('\n' + '='.repeat(50))
const allEncodeRates = allResults.flatMap(s => s.results.map(r => r.encode.mbPerSec))
const allDecodeRates = allResults.flatMap(s => s.results.map(r => r.decode.mbPerSec))
const avgEncode = Math.round(allEncodeRates.reduce((a, b) => a + b, 0) / allEncodeRates.length * 10) / 10
const avgDecode = Math.round(allDecodeRates.reduce((a, b) => a + b, 0) / allDecodeRates.length * 10) / 10
log(`<span class="done">Average throughput: encode ${avgEncode} MB/s, decode ${avgDecode} MB/s</span>`)
lastResults = {
timestamp: new Date().toISOString(),
userAgent: navigator.userAgent,
seed: 12345,
duration,
mode,
encodeInto: useEncodeInto,
suites: allResults,
summary: { avgEncodeMBps: avgEncode, avgDecodeMBps: avgDecode }
}
runBtn.disabled = false
copyBtn.disabled = false
}
runBtn.addEventListener('click', runBenchmarks)
copyBtn.addEventListener('click', () => {
if (lastResults) {
navigator.clipboard.writeText(JSON.stringify(lastResults, null, 2))
copyBtn.textContent = 'Copied!'
setTimeout(() => { copyBtn.textContent = 'Copy JSON' }, 1500)
}
})
</script>
</body>
</html>