/** * Benchmarks for web extension hot paths. * * Usage: * bun run __bench__/bench.ts */ import { parseDdgResults } from "../ddg.js"; import { extractMainContent, sanitizeInjectionVectors } from "../extract.js"; import { cleanTableClasses, stripResidualTags } from "../pandoc.js"; import { makeChunks, makeDdgHtml, makeEntityHeavyText, makeHtmlWithInjection, makeLargeArticle, makePandocOutput, makeTableHtml, } from "./fixtures.js"; // -- classifyContentType inlined (not exported from index.ts) --------------- const CR: Array<{ test: (ct: string) => boolean; result: { kind: string; officeFormat?: string }; }> = [ { test: (ct) => ct.includes("text/html") || ct.includes("application/xhtml"), result: { kind: "html" }, }, { test: (ct) => ct.includes("application/json"), result: { kind: "json" } }, { test: (ct) => ct.includes("application/xml") || ct.includes("application/yaml"), result: { kind: "text" }, }, { test: (ct) => ct.startsWith("text/") || ct.includes("csv") || ct.includes("tsv"), result: { kind: "text" }, }, { test: (ct) => ct.includes("application/pdf"), result: { kind: "pdf" } }, { test: (ct) => ct.includes("officedocument.wordprocessingml") || ct.includes("msword"), result: { kind: "office", officeFormat: "docx" }, }, { test: (ct) => ct.includes("opendocument.text"), result: { kind: "office", officeFormat: "odt" }, }, { test: (ct) => ct.includes("application/epub"), result: { kind: "office", officeFormat: "epub" }, }, { test: (ct) => ct.includes("application/rtf") || ct.includes("text/richtext"), result: { kind: "office", officeFormat: "rtf" }, }, { test: (ct) => ct.startsWith("image/"), result: { kind: "image" } }, { test: (ct) => ct.startsWith("video/") || ct.startsWith("audio/") || ct.includes("mpeg") || ct.includes("mp4") || ct.includes("webm") || ct.includes("ogg"), result: { kind: "media" }, }, ]; function classifyContentType(ct: string) { const lower = ct.toLowerCase().split(";")[0].trim(); return CR.find((r) => r.test(lower))?.result ?? { kind: "binary" }; } // -- Micro-benchmark harness ----------------------------------------------- const MIN_DURATION_MS = 500; const WARMUP = 5; function bench(name: string, fn: () => void) { for (let i = 0; i < WARMUP; i++) fn(); const t0 = performance.now(); for (let i = 0; i < 10; i++) fn(); const perIter = (performance.now() - t0) / 10; const iterations = Math.max( 50, Math.min(10_000, Math.ceil(MIN_DURATION_MS / perIter)), ); const times: number[] = []; for (let i = 0; i < iterations; i++) { const start = performance.now(); fn(); times.push(performance.now() - start); } times.sort((a, b) => a - b); const totalMs = times.reduce((s, t) => s + t, 0); return { name, iterations, totalMs, opsPerSec: Math.round((iterations / totalMs) * 1000), avgUs: Math.round((totalMs / iterations) * 1000), p50Us: Math.round(times[Math.floor(times.length * 0.5)] * 1000), p99Us: Math.round(times[Math.floor(times.length * 0.99)] * 1000), }; } function fmt(r: ReturnType): string { const ops = r.opsPerSec.toLocaleString().padStart(10); const avg = String(r.avgUs).padStart(8); const p50 = String(r.p50Us).padStart(8); const p99 = String(r.p99Us).padStart(8); const iters = String(r.iterations).padStart(6); return `${r.name.padEnd(52)} ${ops} ops/s avg=${avg}us p50=${p50}us p99=${p99}us [${iters}]`; } // -- Run benchmarks --------------------------------------------------------- console.log("Web Extension Benchmarks"); console.log("=".repeat(120)); const ddgHtml = makeDdgHtml(15); const injectHtml = makeHtmlWithInjection(40); const largeArticle = makeLargeArticle(100); const pandocOutput = makePandocOutput(40); const tableHtml = makeTableHtml(10); const entityText = makeEntityHeavyText(200); const chunks100 = makeChunks(100, 10); const chunks1000 = makeChunks(1000, 1); const contentTypes = [ "text/html; charset=utf-8", "application/json", "text/plain; charset=utf-8", "application/pdf", "image/png", "video/mp4", "application/vnd.openxmlformats-officedocument.wordprocessingml.document", "application/octet-stream", ]; const results = [ // DDG bench("parseDdgResults (15 results, ~20KB)", () => parseDdgResults(ddgHtml)), // Sanitization bench("sanitizeInjectionVectors (~40KB)", () => sanitizeInjectionVectors(injectHtml), ), bench("extractMainContent (article, ~40KB)", () => extractMainContent(injectHtml), ), bench("sanitizeInjectionVectors (~100KB)", () => sanitizeInjectionVectors(largeArticle), ), bench("extractMainContent (article, ~100KB)", () => extractMainContent(largeArticle), ), // Pandoc bench("stripResidualTags (~40KB)", () => stripResidualTags(pandocOutput)), bench("cleanTableClasses (10 tables)", () => cleanTableClasses(tableHtml)), // Content-Type bench("classifyContentType (8 types)", () => { for (const ct of contentTypes) classifyContentType(ct); }), bench("classifyContentType x8000", () => { for (let i = 0; i < 1000; i++) { for (const ct of contentTypes) classifyContentType(ct); } }), // Buffer bench("Buffer.concat (100x10KB=1MB)", () => Buffer.concat(chunks100)), bench("Buffer.concat (1000x1KB=1MB)", () => Buffer.concat(chunks1000)), // Entity decoding bench("decodeHTMLEntities (200 entities)", () => { entityText .replace(/&/g, "&") .replace(/</g, "<") .replace(/>/g, ">") .replace(/"/g, '"') .replace(/'/g, "'") .replace(/'/g, "'") .replace(/'/g, "'") .replace(/ /g, " ") .replace(///g, "/") .replace(/&#(\d+);/g, (_, n) => String.fromCodePoint(parseInt(n, 10))) .replace(/&#x([0-9a-f]+);/gi, (_, n) => String.fromCodePoint(parseInt(n, 16)), ) .trim(); }), ]; for (const r of results) console.log(fmt(r)); console.log();