Files
wikideck/scripts/benchmark-canonical-index.ts
T

60 lines
4.2 KiB
TypeScript
Executable File

import { DatabaseSync } from 'node:sqlite';
import { mkdtemp, rm, stat } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { performance } from 'node:perf_hooks';
import { CanonicalIndex } from '../server/testnet/canonical-index.ts';
import { CHAIN_ID, GENESIS_HASH, sha256, transactionId } from '../server/testnet/consensus.ts';
import { NETWORK, type TestnetBlock, type NftState, type NftTransfer } from '../shared/testnet.ts';
// Synthetic LOCAL SQL dataset only. Not valid mined blocks; never submitted to Ledger or a peer.
// This measures derived-index costs, not RandomX validation, consensus security or production SLOs.
const text = process.argv[2] ?? '10000';
if (!/^[1-9]\d{0,5}$/.test(text) || Number(text) > 100000) throw new Error('Fixture size must be 1..100000');
const count = Number(text), queries = 1000;
const directory = await mkdtemp(join(tmpdir(), 'wikideck-index-benchmark-'));
const file = join(directory, 'fixture.sqlite'), db = new DatabaseSync(file);
try {
db.exec('PRAGMA journal_mode=WAL; PRAGMA synchronous=FULL; PRAGMA foreign_keys=ON; CREATE TABLE blocks(hash TEXT PRIMARY KEY,block_json TEXT NOT NULL)');
const index = new CanonicalIndex(db), chain: TestnetBlock[] = [], state: NftState = Object.create(null);
index.initialize([], state);
const owners = Array.from({ length: 16 }, (_, i) => sha256(`fixture-owner-${i}`));
const insert = db.prepare('INSERT INTO blocks VALUES(?,?)');
const start = performance.now();
db.exec('BEGIN IMMEDIATE');
try {
for (let height = 1; height <= count; height++) {
const hash = sha256(`fixture-block-${height}`), token = sha256(`fixture-token-${height}`), owner = owners[height % owners.length];
const txs: NftTransfer[] = height > 2 ? [{ network: NETWORK, chainId: CHAIN_ID, cardHash: chain[height - 3].tokenId,
from: state[chain[height - 3].tokenId].owner, to: owner, sequence: 0, publicKey: '', signature: '' }] : [];
const block = { height, hash, tokenId: token, previousHash: chain.at(-1)?.hash ?? GENESIS_HASH, owner,
article: { title: `Synthetic index fixture ${height}`, pageId: height, extract: 'x'.repeat(512) }, transactions: txs } as unknown as TestnetBlock;
state[token] = { owner, sequence: 0, mintedHeight: height, pageId: height };
for (const tx of txs) state[tx.cardHash] = { ...state[tx.cardHash], owner: tx.to, sequence: 1 };
insert.run(hash, JSON.stringify(block)); index.connect(block); chain.push(block);
}
db.exec('COMMIT');
} catch (error) { db.exec('ROLLBACK'); throw error; }
const buildMs = performance.now() - start;
const latency: number[] = [];
for (let i = 0; i < queries; i++) {
const block = chain[(i * 7919) % count], before = performance.now();
const nft = index.nft(block.tokenId, count);
if (nft?.sequence !== state[block.tokenId].sequence || nft.owner !== state[block.tokenId].owner) throw new Error('Fixture ownership mismatch');
index.deck(owners[i % owners.length], 0, 24, count);
if (block.transactions.length && !index.transaction(transactionId(block.transactions[0]))) throw new Error('Fixture transaction missing');
latency.push(performance.now() - before);
}
let cursor = 0, batches = 0;
const consumeStart = performance.now();
do { const page = index.events(cursor, 64); cursor = page.cursor; batches++; } while (cursor < index.head());
const eventReadMs = performance.now() - consumeStart;
const verifyStart = performance.now(); index.initialize(chain, state); const verifyMs = performance.now() - verifyStart;
const bytes = (await stat(file)).size + (await stat(file + '-wal')).size;
latency.sort((a, b) => a - b);
console.log(JSON.stringify({ fixture: 'synthetic-derived-index-only', blocks: count, nfts: count,
transactions: Math.max(0, count - 2), queries, buildMs, lookupGroupP50Ms: latency[499], lookupGroupP95Ms: latency[949],
eventBatches: batches, maxEventsPerBatch: 64, eventReadMs, startupVerifyMs: verifyMs, databaseAndWalBytes: bytes,
warning: 'No PoW, HTTP load, real assets, target hardware or production SLO certification.' }, null, 2));
} finally { db.close(); await rm(directory, { recursive: true, force: true }); }