a cache for slack profile pictures and emojis
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258/** * Benchmark script for cachet server * Auto-selects random real users/emojis from the prod db */
import { Database } from "bun:sqlite";
const BASE = "http://localhost:3000";const DB_PATH = process.env.DATABASE_PATH ?? "./data/cachet.db";const SAMPLE_SIZE = 10;
interface BenchResult { name: string; requests: number; totalMs: number; avgMs: number; p50Ms: number; p95Ms: number; p99Ms: number; rps: number; errors: number;}
function sampleFromDb(): { userIds: string[]; emojiNames: string[] } { const db = new Database(DB_PATH, { readonly: true }); const now = Date.now();
const userIds = ( db .query( "SELECT userId FROM users WHERE imageUrl IS NOT NULL AND imageUrl != '' AND expiration > ? ORDER BY RANDOM() LIMIT ?", ) .all(now, SAMPLE_SIZE) as Array<{ userId: string }> ).map((r) => r.userId);
const emojiNames = ( db .query( "SELECT name FROM emojis WHERE imageUrl IS NOT NULL AND imageUrl != '' AND expiration > ? AND length(name) BETWEEN 2 AND 30 ORDER BY RANDOM() LIMIT ?", ) .all(now, SAMPLE_SIZE) as Array<{ name: string }> ).map((r) => r.name);
db.close();
if (userIds.length === 0) throw new Error("No valid users found in db"); if (emojiNames.length === 0) throw new Error("No valid emojis found in db");
return { userIds, emojiNames };}
async function benchmark( name: string, urlFn: (i: number) => string, durationMs: number, concurrency: number,): Promise<BenchResult> { const latencies: number[] = []; let errors = 0; let completed = 0;
const startTime = performance.now(); const endTime = startTime + durationMs;
async function worker() { let i = 0; while (performance.now() < endTime) { const url = urlFn(i++); const t0 = performance.now(); try { const res = await fetch(url, { redirect: "manual" }); await res.arrayBuffer(); latencies.push(performance.now() - t0); } catch { errors++; latencies.push(performance.now() - t0); } completed++; } }
const workers = Array.from({ length: concurrency }, () => worker()); await Promise.all(workers);
const totalMs = performance.now() - startTime; latencies.sort((a, b) => a - b);
const percentile = (p: number) => { const idx = Math.floor(latencies.length * p); return latencies[Math.min(idx, latencies.length - 1)] ?? 0; };
return { name, requests: completed, totalMs: Math.round(totalMs), avgMs: Number( (latencies.reduce((a, b) => a + b, 0) / latencies.length).toFixed(3), ), p50Ms: Number(percentile(0.5).toFixed(3)), p95Ms: Number(percentile(0.95).toFixed(3)), p99Ms: Number(percentile(0.99).toFixed(3)), rps: Math.round(completed / (totalMs / 1000)), errors, };}
function printResult(r: BenchResult) { console.log(`\n--- ${r.name} ---`); console.log(` Requests: ${r.requests} (${r.rps} req/s)`); console.log( ` Avg: ${r.avgMs}ms | p50: ${r.p50Ms}ms | p95: ${r.p95Ms}ms | p99: ${r.p99Ms}ms`, ); console.log(` Errors: ${r.errors}`);}
async function main() { const DURATION = 5000; const CONCURRENCY = 10;
console.log(`Sampling ${SAMPLE_SIZE} random users/emojis from ${DB_PATH}...`); const { userIds, emojiNames } = sampleFromDb(); console.log(`Users: ${userIds.join(", ")}`); console.log(`Emojis: ${emojiNames.join(", ")}`); console.log( `\nBenchmarking ${BASE} (${DURATION}ms per test, ${CONCURRENCY} concurrent)\n`, );
console.log("Warming up..."); for (let i = 0; i < 100; i++) { await fetch(`${BASE}/health`).then((r) => r.arrayBuffer()); await fetch(`${BASE}/users/${userIds[i % userIds.length]}`).then((r) => r.arrayBuffer(), ); await fetch(`${BASE}/emojis/${emojiNames[i % emojiNames.length]}`).then( (r) => r.arrayBuffer(), ); await fetch(`${BASE}/users/${userIds[i % userIds.length]}/r`).then((r) => r.arrayBuffer(), ); await fetch(`${BASE}/emojis/${emojiNames[i % emojiNames.length]}/r`).then( (r) => r.arrayBuffer(), ); } console.log("Warmup done.\n");
const results: BenchResult[] = [];
results.push( await benchmark( "GET /health", () => `${BASE}/health`, DURATION, CONCURRENCY, ), );
results.push( await benchmark( "GET /users/:id (data)", (i) => `${BASE}/users/${userIds[i % userIds.length]}`, DURATION, CONCURRENCY, ), );
results.push( await benchmark( "GET /users/:id/r (redirect)", (i) => `${BASE}/users/${userIds[i % userIds.length]}/r`, DURATION, CONCURRENCY, ), );
results.push( await benchmark( "GET /emojis/:name (data)", (i) => `${BASE}/emojis/${emojiNames[i % emojiNames.length]}`, DURATION, CONCURRENCY, ), );
results.push( await benchmark( "GET /emojis/:name/r (redirect)", (i) => `${BASE}/emojis/${emojiNames[i % emojiNames.length]}/r`, DURATION, CONCURRENCY, ), );
results.push( await benchmark( "GET /api/stats (full)", () => `${BASE}/api/stats`, DURATION, CONCURRENCY, ), );
results.push( await benchmark( "GET /api/stats/essential", () => `${BASE}/api/stats/essential`, DURATION, CONCURRENCY, ), );
results.push( await benchmark( "Mixed workload", (i) => { const mod = i % 10; if (mod < 3) return `${BASE}/users/${userIds[i % userIds.length]}/r`; if (mod < 5) return `${BASE}/emojis/${emojiNames[i % emojiNames.length]}/r`; if (mod < 7) return `${BASE}/users/${userIds[i % userIds.length]}`; if (mod < 9) return `${BASE}/emojis/${emojiNames[i % emojiNames.length]}`; return `${BASE}/health`; }, DURATION, CONCURRENCY, ), );
console.log("\n========== RESULTS =========="); for (const r of results) { printResult(r); }
console.log("\n========== SUMMARY =========="); console.log( "Endpoint".padEnd(35), "RPS".padStart(8), "Avg ms".padStart(8), "p50 ms".padStart(8), "p95 ms".padStart(8), "p99 ms".padStart(8), ); console.log("-".repeat(85)); for (const r of results) { console.log( r.name.padEnd(35), String(r.rps).padStart(8), String(r.avgMs).padStart(8), String(r.p50Ms).padStart(8), String(r.p95Ms).padStart(8), String(r.p99Ms).padStart(8), ); }}
main().catch(console.error);