chadtmiller.com/pds.js
An atproto PDS in JavaScript, for Cloudflare Workers and Node
0
0
#!/usr/bin/env node
/**
* Push-cost and read-path benchmark for @pdsjs/git against a local PDS.
*
* Pushes the same synthetic history under two chain policies and reports
* uploaded bytes, worst-case push, chain length, and wall time. Then it
* times smart HTTP reads with and without ETag revalidation, including the
* cold-store case a fresh Workers isolate hits.
*
* Run from the workspace root after `pnpm install`:
*
* node packages/git/bench/push-costs.mjs --commits 60 --bytes 8192
*/
import { execFile } from 'node:child_process';
import { randomBytes } from 'node:crypto';
import {
chmodSync,
mkdirSync,
mkdtempSync,
rmSync,
writeFileSync,
} from 'node:fs';
import { tmpdir } from 'node:os';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { promisify } from 'node:util';
import { defineLexicon } from '@bigmoves/lexicon';
import { GIT_REPO_COLLECTION, gitRepoLexicon } from '@pdsjs/git';
import { LexiconResolver } from '@pdsjs/lexicon-resolver';
import { createServer } from '@pdsjs/node';
const execFileAsync = promisify(execFile);
function arg(name, fallback) {
const i = process.argv.indexOf(`--${name}`);
return i === -1 ? fallback : Number(process.argv[i + 1]);
}
const COMMITS = arg('commits', 60);
const FILE_BYTES = arg('bytes', 8 * 1024);
const PORT = arg('port', 2489);
const BASE = `http://127.0.0.1:${PORT}`;
const PASSWORD = 'bench-password';
const chars = 'abcdefghijklmnopqrstuvwxyz234567';
const DID = `did:plc:${Array.from(randomBytes(24))
.map((b) => chars[b % 32])
.join('')}`;
const CONFIGS = [
{
label: 'no merges, threshold 16',
rkey: 'bench-old',
env: {
ATPROTO_GIT_COMPACT_FACTOR: '0',
ATPROTO_GIT_REPACK_THRESHOLD: '16',
},
},
{
label: 'geometric factor 2',
rkey: 'bench-geo',
env: {
ATPROTO_GIT_COMPACT_FACTOR: '2',
ATPROTO_GIT_REPACK_THRESHOLD: '16',
},
},
];
const workDir = mkdtempSync(join(tmpdir(), 'pdsjs-git-bench-'));
const server = await createServer({
port: PORT,
dbPath: join(workDir, 'pds.db'),
blobsDir: join(workDir, 'blobs'),
jwtSecret: 'bench-secret',
hostname: `127.0.0.1:${PORT}`,
password: PASSWORD,
lexiconResolver: new LexiconResolver({
schemas: [defineLexicon(gitRepoLexicon)],
}),
experimental: { git: { http: true } },
});
await server.listen();
const init = await fetch(`${BASE}/init?did=${DID}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
did: DID,
privateKey: randomBytes(32).toString('hex'),
handle: 'bench.test',
password: PASSWORD,
}),
});
if (!init.ok) throw new Error('account init failed');
const binDir = join(workDir, 'bin');
mkdirSync(binDir);
const cliPath = join(
dirname(fileURLToPath(import.meta.url)),
'..',
'src',
'cli.js',
);
writeFileSync(
join(binDir, 'git-remote-atproto'),
`#!/bin/sh\nexec node ${cliPath} "$@"\n`,
);
chmodSync(join(binDir, 'git-remote-atproto'), 0o755);
// The same content for every run.
const payloads = Array.from({ length: COMMITS }, () => randomBytes(FILE_BYTES));
const baseEnv = {
...process.env,
PATH: `${binDir}:${process.env.PATH}`,
ATPROTO_GIT_SERVICE: BASE,
ATPROTO_GIT_IDENTIFIER: DID,
ATPROTO_GIT_PASSWORD: PASSWORD,
GIT_TERMINAL_PROMPT: '0',
GIT_AUTHOR_NAME: 'Bench',
GIT_AUTHOR_EMAIL: 'bench@example.com',
GIT_COMMITTER_NAME: 'Bench',
GIT_COMMITTER_EMAIL: 'bench@example.com',
};
function runGit(cwd, args, env) {
return execFileAsync('git', args, { cwd, env, encoding: 'utf8' });
}
async function loadRecord(rkey) {
const params = new URLSearchParams({
repo: DID,
collection: GIT_REPO_COLLECTION,
rkey,
});
const res = await fetch(`${BASE}/xrpc/com.atproto.repo.getRecord?${params}`);
const body = await res.json();
return body.value;
}
const mb = (n) => `${(n / 1024 / 1024).toFixed(2)} MB`;
async function benchPushes({ label, rkey, env: extraEnv }) {
const env = { ...baseEnv, ...extraEnv };
const repoDir = join(workDir, rkey);
mkdirSync(repoDir);
await runGit(repoDir, ['init', '-b', 'main'], env);
await runGit(
repoDir,
['remote', 'add', 'origin', `atproto://${DID}/${rkey}`],
env,
);
let total = 0;
let maxPush = 0;
let maxChain = 0;
const checkpoints = [];
const step = Math.max(1, Math.floor(COMMITS / 4));
const t0 = performance.now();
for (let i = 0; i < COMMITS; i++) {
writeFileSync(join(repoDir, `data-${i}.bin`), payloads[i]);
await runGit(repoDir, ['add', '.'], env);
await runGit(repoDir, ['commit', '-m', `commit ${i}`], env);
const { stderr } = await runGit(repoDir, ['push', 'origin', 'main'], env);
let pushBytes = 0;
for (const m of stderr.matchAll(/Uploading bundle \((\d+) bytes/g)) {
pushBytes += Number(m[1]);
}
total += pushBytes;
maxPush = Math.max(maxPush, pushBytes);
if ((i + 1) % step === 0) checkpoints.push(`${i + 1}:${mb(total)}`);
const record = await loadRecord(rkey);
maxChain = Math.max(maxChain, record.bundles.length);
}
const secs = (performance.now() - t0) / 1000;
const record = await loadRecord(rkey);
const heads = record.bundles.reduce((sum, b) => sum + b.heads.length, 0);
console.log(`\n${label}`);
console.log(` total uploaded: ${mb(total)}`);
console.log(` worst single push: ${mb(maxPush)}`);
console.log(` cumulative at push: ${checkpoints.join(' ')}`);
console.log(
` chain max/final: ${maxChain}/${record.bundles.length} bundles, ${heads} recorded heads`,
);
console.log(
` wall time: ${secs.toFixed(1)}s (${((secs / COMMITS) * 1000).toFixed(0)} ms/push)`,
);
}
async function timeReads(label, url, headers) {
await fetch(url, { headers }).then((r) => r.arrayBuffer());
const times = [];
let status = 0;
let bytes = 0;
for (let i = 0; i < 40; i++) {
const t0 = performance.now();
const res = await fetch(url, { headers });
const body = await res.arrayBuffer();
times.push(performance.now() - t0);
status = res.status;
bytes = body.byteLength;
}
times.sort((a, b) => a - b);
console.log(
` ${label}: status ${status}, body ${bytes} B, median ${times[20].toFixed(1)} ms`,
);
}
// Bust the single-entry browser store cache between reads, the way a fresh
// Workers isolate arrives with no store at all.
async function timeColdReads(label, url, otherUrl, headers) {
const times = [];
let status = 0;
for (let i = 0; i < 20; i++) {
await fetch(otherUrl).then((r) => r.arrayBuffer());
const t0 = performance.now();
const res = await fetch(url, { headers });
await res.arrayBuffer();
times.push(performance.now() - t0);
status = res.status;
}
times.sort((a, b) => a - b);
console.log(
` ${label}: status ${status}, median ${times[10].toFixed(1)} ms`,
);
}
console.log(
`pushing ${COMMITS} commits of ${FILE_BYTES} random bytes each, twice`,
);
for (const config of CONFIGS) await benchPushes(config);
console.log('\nread path (repo from the geometric run)');
const RAW = `${BASE}/git/${DID}/bench-geo/raw/main/data-1.bin`;
const OTHER = `${BASE}/git/${DID}/bench-old/raw/main/data-1.bin`;
const REFS = `${BASE}/git/${DID}/bench-geo/info/refs?service=git-upload-pack`;
const etag = (await fetch(RAW)).headers.get('ETag');
const conditional = { 'If-None-Match': etag ?? '' };
await timeReads('raw 200, warm store ', RAW);
await timeReads('raw 304, warm store ', RAW, conditional);
await timeColdReads('raw 200, cold store ', RAW, OTHER);
await timeColdReads('raw 304, cold store ', RAW, OTHER, conditional);
await timeReads('info/refs 200 ', REFS);
await timeReads('info/refs 304 ', REFS, conditional);
await server.close();
rmSync(workDir, { recursive: true, force: true });