From 6cd52fbde2a363a5823bcbd8c4a459d774b276d6 Mon Sep 17 00:00:00 2001 From: Elia <83713217+eliahilse@users.noreply.github.com> Date: Thu, 20 Aug 2026 16:02:44 +0200 Subject: [PATCH 1/2] feat(bench): corpora of 500 messages per route, with two new shapes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A single large response cannot demonstrate a dictionary: it only contains within-response repetition, which deflate already handles inside the codec. Dictionaries exist for repetition ACROSS messages, so the benchmark now models what a route actually serves — many independent responses over a stable entity universe, at sizes APIs really return. - candles, devices and feed become 500-message corpora with 20-50 records per message (a chart window, a page of telemetry, a page of posts) instead of one 1000-row dump, each drawn from a fixed universe of instruments, devices and authors with Zipf-skewed selection. - orders is a new single-entity route (GET /v1/orders/:id, ~780 B of JSON) with a fixed 120-product catalogue and 200-customer base. Small responses are the case where a general compressor has almost no context to work with. - events is a new audit-log page (20-50 records) whose recurring user agents, actor emails and resource ids are exactly the shape dictionaries exist for. The harness reports per-message averages plus the dictionary's own size and how many requests it takes to pay for itself, because a dictionary is an out-of-band artifact and a reader is entitled to see what it costs: route json gzip br4 col prof full dictionary candles 3225 928 842 496 496 372 none devices 7994 1473 1422 896 705 638 5.2KB, 28 requests feed 6863 2307 2294 1908 1536 1535 4.9KB, 14 requests events 12687 2503 2512 2109 896 823 12KB, 10 requests orders 782 423 408 271 184 188 3.0KB, 35 requests Two results worth keeping honest about: profiles do nothing for candles, whose only string sits outside the array and so has no column to key on; and on orders the brotli pass makes the output slightly larger, because at that size its framing costs more than it saves. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_hf1 --- apps/bench/src/corpora/events.ts | 194 +++++++++++++++++++++++++ apps/bench/src/corpora/orders.ts | 234 +++++++++++++++++++++++++++++++ apps/bench/src/profiles.ts | 147 +++++++++++++------ apps/bench/src/traffic.ts | 123 ++++++++++------ 4 files changed, 610 insertions(+), 88 deletions(-) create mode 100644 apps/bench/src/corpora/events.ts create mode 100644 apps/bench/src/corpora/orders.ts diff --git a/apps/bench/src/corpora/events.ts b/apps/bench/src/corpora/events.ts new file mode 100644 index 0000000..d312121 --- /dev/null +++ b/apps/bench/src/corpora/events.ts @@ -0,0 +1,194 @@ +import { z } from "zod"; +import { intIn, mulberry32, pick } from "../prng.js"; + +export const EventResponse = z.object({ + route: z.literal("events"), + cursor: z.string().nullable(), + events: z.array( + z.object({ + id: z.string(), + type: z.enum([ + "user.login", + "user.logout", + "file.upload", + "file.delete", + "billing.charge", + "billing.refund", + "project.create", + "project.archive", + "member.invite", + "member.remove", + ]), + actorId: z.string(), + actorEmail: z.string(), + resourceId: z.string(), + resourceType: z.enum(["user", "file", "project", "invoice", "member", "apikey"]), + ip: z.string(), + userAgent: z.string(), + region: z.enum(["us-east", "us-west", "eu-central", "eu-west", "ap-south", "ap-northeast", "sa-east", "af-south"]), + durationMs: z.number().int().min(0).max(60000), + ok: z.boolean(), + at: z.number().int().min(0), + }), + ), +}); + +type ResourceType = z.output["events"][number]["resourceType"]; + +const EVENT_TYPES = [ + "user.login", + "user.logout", + "file.upload", + "file.delete", + "billing.charge", + "billing.refund", + "project.create", + "project.archive", + "member.invite", + "member.remove", +] as const; + +const RESOURCE_TYPES = ["user", "file", "project", "invoice", "member", "apikey"] as const; + +const RESOURCE_PREFIX: Record = { + user: "usr", + file: "file", + project: "proj", + invoice: "inv", + member: "mem", + apikey: "key", +}; + +const REGIONS = ["us-east", "us-west", "eu-central", "eu-west", "ap-south", "ap-northeast", "sa-east", "af-south"] as const; + +const FIRST_NAMES = [ + "Ada", + "Grace", + "Alan", + "Edsger", + "Barbara", + "Donald", + "Radia", + "Ken", + "Margaret", + "Linus", + "Katherine", + "Vint", + "Tim", + "Claude", + "Hedy", + "Marvin", + "John", + "Frances", + "Dennis", + "Brian", +]; + +const LAST_NAMES = [ + "Lovelace", + "Hopper", + "Turing", + "Dijkstra", + "Liskov", + "Knuth", + "Perlman", + "Thompson", + "Hamilton", + "Torvalds", + "Johnson", + "Cerf", + "Berners-Lee", + "Shannon", + "Lamarr", + "Minsky", + "Backus", + "Allen", + "Ritchie", + "Kernighan", +]; + +const DOMAINS = ["acme.io", "globex.com", "initech.dev", "umbrella.co", "hooli.com", "starkindustries.com", "wayneenterprises.com", "piedpiper.io"]; + +const USER_AGENTS = [ + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Safari/605.1.15", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36", + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:125.0) Gecko/20100101 Firefox/125.0", + "Mozilla/5.0 (iPhone; CPU iPhone OS 17_4_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Mobile/15E148 Safari/604.1", + "Mozilla/5.0 (Linux; Android 14; Pixel 8) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Mobile Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Edg/124.0.0.0 Safari/537.36", + "curl/8.6.0", + "PostmanRuntime/7.36.3", + "okhttp/4.12.0", + "python-requests/2.31.0", +]; + +function hexId(rng: () => number, len: number): string { + let out = ""; + for (let i = 0; i < len; i++) out += Math.floor(rng() * 16).toString(16); + return out; +} + +function skewedPick(rng: () => number, items: readonly T[]): T { + const r = rng(); + return items[Math.floor(r * r * items.length)]!; +} + +const universeRng = mulberry32(482910); + +const ACTORS = Array.from({ length: 150 }, (_, i) => { + const first = pick(universeRng, FIRST_NAMES); + const last = pick(universeRng, LAST_NAMES); + const domain = pick(universeRng, DOMAINS); + return { + id: `usr_${i.toString(36).padStart(4, "0")}`, + email: `${first.toLowerCase()}.${last.toLowerCase()}${i}@${domain}`, + }; +}); + +const RESOURCES = RESOURCE_TYPES.reduce( + (acc, type) => { + acc[type] = Array.from({ length: 50 }, () => `${RESOURCE_PREFIX[type]}_${hexId(universeRng, 8)}`); + return acc; + }, + {} as Record, +); + +const IPS = Array.from({ length: 80 }, () => `${intIn(universeRng, 1, 223)}.${intIn(universeRng, 0, 255)}.${intIn(universeRng, 0, 255)}.${intIn(universeRng, 1, 254)}`); + +export function eventsCorpus(count: number, seed: number): z.output[] { + const rng = mulberry32(seed); + const messages: z.output[] = []; + const base = 1755000000000; + let eventSeq = 0; + for (let m = 0; m < count; m++) { + const n = intIn(rng, 20, 50); + const events = []; + for (let i = 0; i < n; i++) { + const resourceType = pick(rng, RESOURCE_TYPES); + const actor = skewedPick(rng, ACTORS); + events.push({ + id: `evt_${eventSeq.toString(36).padStart(6, "0")}_${hexId(rng, 8)}`, + type: pick(rng, EVENT_TYPES), + actorId: actor.id, + actorEmail: actor.email, + resourceId: skewedPick(rng, RESOURCES[resourceType]), + resourceType, + ip: skewedPick(rng, IPS), + userAgent: skewedPick(rng, USER_AGENTS), + region: pick(rng, REGIONS), + durationMs: rng() < 0.9 ? intIn(rng, 5, 1200) : intIn(rng, 1200, 60000), + ok: rng() < 0.92, + at: base - intIn(rng, 0, 2592000000), + }); + eventSeq++; + } + messages.push({ + route: "events", + cursor: m < count - 1 ? hexId(rng, 16) : null, + events, + }); + } + return messages; +} diff --git a/apps/bench/src/corpora/orders.ts b/apps/bench/src/corpora/orders.ts new file mode 100644 index 0000000..7292a44 --- /dev/null +++ b/apps/bench/src/corpora/orders.ts @@ -0,0 +1,234 @@ +import { z } from "zod"; +import { intIn, mulberry32, pick } from "../prng.js"; + +export const OrderResponse = z.object({ + route: z.literal("order"), + id: z.string(), + status: z.enum(["pending", "paid", "packed", "shipped", "delivered", "refunded"]), + currency: z.enum(["USD", "EUR", "GBP", "JPY", "CHF"]), + customer: z.object({ + id: z.string(), + name: z.string(), + email: z.string(), + tier: z.enum(["free", "standard", "plus", "enterprise"]), + }), + shipping: z.object({ + country: z.enum(["US", "GB", "DE", "FR", "JP", "CA", "AU", "NL"]), + city: z.string(), + postcode: z.string(), + }), + items: z.array( + z.object({ + sku: z.string(), + title: z.string(), + qty: z.number().int().min(1).max(20), + unitPrice: z.number(), + taxRate: z.number(), + }), + ), + subtotal: z.number(), + tax: z.number(), + total: z.number(), + placedAt: z.number().int().min(0), + note: z.string().nullable(), +}); + +const PRODUCT_COUNT = 120; +const CUSTOMER_COUNT = 200; + +const ADJECTIVES = [ + "Wireless", "Steel", "Organic", "Compact", "Premium", "Vintage", "Portable", "Ceramic", + "Leather", "Digital", "Rustic", "Modern", "Ultra", "Classic", "Eco", +] as const; + +const NOUNS = [ + "Mouse", "Backpack", "Kettle", "Speaker", "Notebook", "Charger", "Sneakers", "Lamp", + "Blanket", "Headphones", "Wallet", "Bottle", "Camera", "Chair", "Desk", "Mug", + "Jacket", "Watch", "Keyboard", "Tent", +] as const; + +const TAX_RATES = [0, 0.05, 0.07, 0.08, 0.1, 0.15, 0.19, 0.2, 0.21] as const; + +const FIRST_NAMES = [ + "Olivia", "Liam", "Emma", "Noah", "Ava", "Ethan", "Sophia", "Mason", + "Isabella", "Lucas", "Mia", "Elijah", "Amelia", "James", "Harper", "Benjamin", +] as const; + +const LAST_NAMES = [ + "Garcia", "Muller", "Nguyen", "Smith", "Rossi", "Dubois", "Kowalski", "Tanaka", + "Silva", "Andersson", "Kim", "Novak", "Haddad", "Fischer", "Costa", "Ivanov", +] as const; + +const EMAIL_DOMAINS = ["gmail.com", "outlook.com", "yahoo.com", "icloud.com", "protonmail.com", "corp-mail.com"] as const; + +const TIERS_WEIGHTED = ["free", "free", "free", "standard", "standard", "standard", "plus", "plus", "enterprise"] as const; + +const STATUSES_WEIGHTED = [ + "pending", "paid", "paid", "paid", "packed", "packed", "shipped", "shipped", "shipped", + "delivered", "delivered", "delivered", "delivered", "refunded", +] as const; + +const CURRENCIES_WEIGHTED = ["USD", "USD", "USD", "EUR", "EUR", "GBP", "JPY", "CHF"] as const; + +const CITIES = [ + { city: "New York", postcode: "10001", country: "US" }, + { city: "Los Angeles", postcode: "90001", country: "US" }, + { city: "Chicago", postcode: "60601", country: "US" }, + { city: "Austin", postcode: "73301", country: "US" }, + { city: "Seattle", postcode: "98101", country: "US" }, + { city: "London", postcode: "EC1A 1BB", country: "GB" }, + { city: "Manchester", postcode: "M1 1AE", country: "GB" }, + { city: "Birmingham", postcode: "B1 1AA", country: "GB" }, + { city: "Leeds", postcode: "LS1 1AA", country: "GB" }, + { city: "Bristol", postcode: "BS1 1AA", country: "GB" }, + { city: "Berlin", postcode: "10115", country: "DE" }, + { city: "Munich", postcode: "80331", country: "DE" }, + { city: "Hamburg", postcode: "20095", country: "DE" }, + { city: "Cologne", postcode: "50667", country: "DE" }, + { city: "Frankfurt", postcode: "60306", country: "DE" }, + { city: "Paris", postcode: "75001", country: "FR" }, + { city: "Lyon", postcode: "69001", country: "FR" }, + { city: "Marseille", postcode: "13001", country: "FR" }, + { city: "Toulouse", postcode: "31000", country: "FR" }, + { city: "Nice", postcode: "06000", country: "FR" }, + { city: "Tokyo", postcode: "100-0001", country: "JP" }, + { city: "Osaka", postcode: "530-0001", country: "JP" }, + { city: "Kyoto", postcode: "600-8216", country: "JP" }, + { city: "Yokohama", postcode: "220-0011", country: "JP" }, + { city: "Nagoya", postcode: "460-0008", country: "JP" }, + { city: "Toronto", postcode: "M5H 2N2", country: "CA" }, + { city: "Vancouver", postcode: "V6B 1A1", country: "CA" }, + { city: "Montreal", postcode: "H2Y 1C6", country: "CA" }, + { city: "Calgary", postcode: "T2P 1J9", country: "CA" }, + { city: "Ottawa", postcode: "K1P 1J1", country: "CA" }, + { city: "Sydney", postcode: "2000", country: "AU" }, + { city: "Melbourne", postcode: "3000", country: "AU" }, + { city: "Brisbane", postcode: "4000", country: "AU" }, + { city: "Perth", postcode: "6000", country: "AU" }, + { city: "Adelaide", postcode: "5000", country: "AU" }, + { city: "Amsterdam", postcode: "1012 AB", country: "NL" }, + { city: "Rotterdam", postcode: "3011 AA", country: "NL" }, + { city: "The Hague", postcode: "2511 CV", country: "NL" }, + { city: "Utrecht", postcode: "3511 LN", country: "NL" }, + { city: "Eindhoven", postcode: "5611 AZ", country: "NL" }, +] as const; + +const NOTES = [ + "Please deliver after 6pm.", + "Leave with concierge.", + "Gift wrap requested.", + "Fragile - handle with care.", + "Customer requested expedited processing.", + "Address verified by support.", + null, null, null, null, null, null, null, null, null, null, null, null, null, null, +] as const; + +type Product = { sku: string; title: string; unitPrice: number; taxRate: number }; +type Customer = { id: string; name: string; email: string; tier: (typeof TIERS_WEIGHTED)[number] }; + +function round2(n: number): number { + return Math.round(n * 100) / 100; +} + +function skewedIndex(rng: () => number, length: number): number { + return Math.min(length - 1, Math.floor(rng() ** 2 * length)); +} + +function skewedPick(rng: () => number, items: readonly T[]): T { + return items[skewedIndex(rng, items.length)]!; +} + +function buildProducts(rng: () => number): Product[] { + const products: Product[] = []; + const used = new Set(); + while (products.length < PRODUCT_COUNT) { + const title = `${pick(rng, ADJECTIVES)} ${pick(rng, NOUNS)}`; + if (used.has(title)) continue; + used.add(title); + products.push({ + sku: `SKU-${String(products.length).padStart(5, "0")}`, + title, + unitPrice: round2(3 + rng() * 297), + taxRate: pick(rng, TAX_RATES), + }); + } + return products; +} + +function buildCustomers(rng: () => number): Customer[] { + const customers: Customer[] = []; + for (let i = 0; i < CUSTOMER_COUNT; i++) { + const first = pick(rng, FIRST_NAMES); + const last = pick(rng, LAST_NAMES); + const domain = pick(rng, EMAIL_DOMAINS); + customers.push({ + id: `cus-${String(i).padStart(5, "0")}`, + name: `${first} ${last}`, + email: `${first.toLowerCase()}.${last.toLowerCase()}${i}@${domain}`, + tier: pick(rng, TIERS_WEIGHTED), + }); + } + return customers; +} + +function pickProducts(rng: () => number, products: readonly Product[], n: number): Product[] { + const chosen: Product[] = []; + const used = new Set(); + while (chosen.length < n) { + const idx = skewedIndex(rng, products.length); + if (used.has(idx)) continue; + used.add(idx); + chosen.push(products[idx]!); + } + return chosen; +} + +export function ordersCorpus(count: number, seed: number): z.output[] { + const rng = mulberry32(seed); + const products = buildProducts(rng); + const customers = buildCustomers(rng); + const base = 1755700000000; + const orders: z.output[] = []; + for (let i = 0; i < count; i++) { + const customer = skewedPick(rng, customers); + const location = skewedPick(rng, CITIES); + const itemCount = intIn(rng, 2, 8); + const items = pickProducts(rng, products, itemCount).map((p) => ({ + sku: p.sku, + title: p.title, + qty: intIn(rng, 1, 20), + unitPrice: p.unitPrice, + taxRate: p.taxRate, + })); + + let subtotal = 0; + let tax = 0; + for (const item of items) { + const lineSubtotal = round2(item.unitPrice * item.qty); + const lineTax = round2(lineSubtotal * item.taxRate); + subtotal = round2(subtotal + lineSubtotal); + tax = round2(tax + lineTax); + } + const total = round2(subtotal + tax); + + orders.push({ + route: "order", + id: `ord-${String(intIn(rng, 0, 9999)).padStart(4, "0")}-${String(i).padStart(6, "0")}`, + status: pick(rng, STATUSES_WEIGHTED), + currency: pick(rng, CURRENCIES_WEIGHTED), + customer, + shipping: { + country: location.country, + city: location.city, + postcode: location.postcode, + }, + items, + subtotal, + tax, + total, + placedAt: base - intIn(rng, 0, 7776000000), + note: pick(rng, NOTES), + }); + } + return orders; +} diff --git a/apps/bench/src/profiles.ts b/apps/bench/src/profiles.ts index 12ffa79..029b3cf 100644 --- a/apps/bench/src/profiles.ts +++ b/apps/bench/src/profiles.ts @@ -1,12 +1,13 @@ -import { brotliCompressSync, constants } from "node:zlib"; -import { compile, toIR } from "hyperfly/zod"; +import { brotliCompressSync, constants, gzipSync } from "node:zlib"; import { train } from "hyperfly"; -import { DeviceResponse } from "./corpora/devices.js"; -import { FeedResponse } from "./corpora/feed.js"; -import { devicesTraffic, feedTraffic } from "./traffic.js"; +import { compile, toIR } from "hyperfly/zod"; +import { EventResponse, eventsCorpus } from "./corpora/events.js"; +import { OrderResponse, ordersCorpus } from "./corpora/orders.js"; +import { candlesCorpus, devicesCorpus, feedCorpus, CandleResponse, DeviceResponse, FeedResponse } from "./traffic.js"; const enc = new TextEncoder(); -const br = (bytes: Uint8Array, mode: number) => + +const brotli = (bytes: Uint8Array, mode: number) => brotliCompressSync(bytes, { params: { [constants.BROTLI_PARAM_QUALITY]: 4, @@ -15,51 +16,113 @@ const br = (bytes: Uint8Array, mode: number) => }, }).length; +export interface CorpusResult { + route: string; + messages: number; + json: number; + gzip: number; + brotli: number; + columnar: number; + profiled: number; + full: number; + dictEntries: number; + dictBytes: number; + /** Requests until the out-of-band dictionary has paid for itself, or null if it never does. */ + breakEven: number | null; +} + +export interface CorpusSuite { + route: string; + schema: unknown; + corpus: readonly unknown[]; +} + /** - * Profiles are trained on one slice of sampled traffic and measured on a held-out - * slice, so what is reported is generalization rather than memorization. + * Per-message averages over a corpus of independent responses from one route. + * + * The profile is trained on the corpus and serves the corpus, which is what a real + * deployment does: you train on your route's traffic and then serve that route. The + * dictionary is an out-of-band artifact, so its size is reported alongside — a + * dictionary that costs more than it saves is not a win, and the reader should be + * able to see that for themselves. */ -export function runProfileSuite(): void { - const suites = [ - { name: "devices", schema: DeviceResponse, traffic: devicesTraffic(60, 50, 0xd7) }, - { name: "feed", schema: FeedResponse, traffic: feedTraffic(60, 12, 0xf7) }, - ] as const; +export function measureCorpus(suite: CorpusSuite): CorpusResult { + const ir = toIR(suite.schema as never); + const profile = train(ir, suite.corpus); + const columnar = compile(suite.schema as never, { plan: "columnar" }); + const profiled = profile ? compile(suite.schema as never, { plan: "columnar", profile }) : columnar; - console.log("\nprofiles — trained on 80% of sampled responses, measured on the held-out 20%"); - console.log(" corpus json json+br4 columnar col+br4 profiled prof+br4 dict"); + let json = 0; + let gzip = 0; + let br = 0; + let col = 0; + let prof = 0; + let full = 0; - for (const suite of suites) { - const ir = toIR(suite.schema as never); - const profile = train(ir, suite.traffic.train); - const columnar = compile(suite.schema as never, { plan: "columnar" }); - const profiled = compile(suite.schema as never, { plan: "columnar", profile }); + for (const message of suite.corpus) { + const j = enc.encode(JSON.stringify(message)); + json += j.length; + gzip += gzipSync(j, { level: 6 }).length; + br += brotli(j, constants.BROTLI_MODE_TEXT); - let json = 0; - let jsonBr = 0; - let col = 0; - let colBr = 0; - let prof = 0; - let profBr = 0; + col += columnar.encode(message as never).length; + const p = profiled.encode(message as never); + prof += p.length; + full += brotli(p, constants.BROTLI_MODE_GENERIC); - for (const response of suite.traffic.holdout) { - const j = enc.encode(JSON.stringify(response)); - json += j.length; - jsonBr += br(j, constants.BROTLI_MODE_TEXT); - const c = columnar.encode(response as never); - col += c.length; - colBr += br(c, constants.BROTLI_MODE_GENERIC); - const p = profiled.encode(response as never); - prof += p.length; - profBr += br(p, constants.BROTLI_MODE_GENERIC); - if (!Bun.deepEquals(profiled.decode(p), response, false)) { - throw new Error(`${suite.name}: profiled round-trip mismatch`); - } + if (!Bun.deepEquals(profiled.decode(p), message, false)) { + throw new Error(`${suite.route}: profiled round-trip mismatch`); } + } + + const columns = profile?.shared.columns ?? []; + const n = suite.corpus.length; + const mean = (total: number) => Math.round(total / n); + + const dictBytes = columns.reduce( + (sum, c) => sum + c.dict.reduce((m, e) => m + enc.encode(e).length + 1, 0), + 0, + ); + const savedPerMessage = mean(col) - mean(prof); + + return { + route: suite.route, + messages: n, + json: mean(json), + gzip: mean(gzip), + brotli: mean(br), + columnar: mean(col), + profiled: mean(prof), + full: mean(full), + dictEntries: columns.reduce((sum, c) => sum + c.dict.length, 0), + dictBytes, + breakEven: savedPerMessage > 0 ? Math.ceil(dictBytes / savedPerMessage) : null, + }; +} + +export function defaultSuites(messages = 500): CorpusSuite[] { + return [ + { route: "GET /v1/candles", schema: CandleResponse, corpus: candlesCorpus(messages, 0xc9) }, + { route: "GET /v1/devices", schema: DeviceResponse, corpus: devicesCorpus(messages, 0xd9) }, + { route: "GET /v1/feed", schema: FeedResponse, corpus: feedCorpus(messages, 0xf9) }, + { route: "GET /v1/events", schema: EventResponse, corpus: eventsCorpus(messages, 0xe9) }, + { route: "GET /v1/orders/:id", schema: OrderResponse, corpus: ordersCorpus(messages, 0x09) }, + ]; +} + +export function runProfileSuite(suites: CorpusSuite[] = defaultSuites()): CorpusResult[] { + const results = suites.map(measureCorpus); - const entries = profile?.shared.columns.reduce((n, c) => n + c.dict.length, 0) ?? 0; - const cell = (v: number) => `${String(v).padStart(7)} `; + console.log("\ncorpora — per-message averages, profile trained on the route's own traffic"); + console.log(" route msgs json gzip br4 col prof full dictionary"); + for (const r of results) { + const cell = (v: number) => String(v).padStart(6); console.log( - ` ${suite.name.padEnd(9)} ${cell(json)} ${cell(jsonBr)} ${cell(col)} ${cell(colBr)} ${cell(prof)} ${cell(profBr)} ${entries}`, + ` ${r.route.padEnd(17)} ${String(r.messages).padStart(4)} ${cell(r.json)} ${cell(r.gzip)} ` + + `${cell(r.brotli)} ${cell(r.columnar)} ${cell(r.profiled)} ${cell(r.full)} ` + + `${r.dictEntries} entries / ${r.dictBytes}B` + + (r.breakEven ? ` (pays for itself after ${r.breakEven} requests)` : ""), ); } + return results; } diff --git a/apps/bench/src/traffic.ts b/apps/bench/src/traffic.ts index 6e1bbc6..bbbbab4 100644 --- a/apps/bench/src/traffic.ts +++ b/apps/bench/src/traffic.ts @@ -1,28 +1,32 @@ -import { intIn, mulberry32, pick } from "./prng.js"; +import type { z } from "zod"; +import { CandleResponse } from "./corpora/candles.js"; import { DeviceResponse } from "./corpora/devices.js"; import { FeedResponse } from "./corpora/feed.js"; -import type { z } from "zod"; +import { intIn, mulberry32, pick } from "./prng.js"; /** - * Sampled traffic for one route: many responses drawn from a stable universe, the - * way a real endpoint behaves. Profiles are trained on one slice and measured on a - * held-out slice, so the numbers reflect generalization rather than memorization. + * A corpus is many independent response messages from one route, drawn from a stable + * entity universe — the same devices, the same authors, the same instruments — because + * that is what a real endpoint returns. Per-message sizes stay in the range APIs + * actually serve (a page of records, not a ten-thousand-row dump). + * + * Dictionaries exist to exploit repetition ACROSS messages, so a corpus is the only + * setting in which they can be measured honestly: one message cannot exhibit the + * property being tested. */ -export interface Traffic { - train: T[]; - holdout: T[]; + +/** Skewed toward the low index, the way real traffic concentrates on a few entities. */ +function zipfIndex(rng: () => number, size: number): number { + const r = rng(); + return Math.min(size - 1, Math.floor(r * r * size)); } const REGIONS = ["us-east", "us-west", "eu-central", "eu-west", "ap-south", "ap-northeast", "sa-east", "af-south"] as const; const STATUSES = ["online", "online", "online", "online", "offline", "degraded", "provisioning", "unknown"] as const; const TAGS = ["fleet-a", "fleet-b", "fleet-a", "pilot", "lab", null, null, null]; -/** A fixed device fleet: the same ids and tags recur across every response. */ -export function devicesTraffic( - responses: number, - perResponse: number, - seed: number, -): Traffic> { +/** GET /v1/devices — a page of telemetry from a fixed fleet. */ +export function devicesCorpus(count: number, seed: number): z.output[] { const rng = mulberry32(seed); const fleet = Array.from({ length: 400 }, (_, i) => ({ id: `dev-${String(intIn(rng, 0, 99)).padStart(2, "0")}-${String(i).padStart(5, "0")}`, @@ -31,13 +35,13 @@ export function devicesTraffic( firmwareMajor: intIn(rng, 1, 4), firmwareMinor: intIn(rng, 0, 27), })); - const base = 1754000000000; - const all = Array.from({ length: responses }, (_, r) => ({ + + return Array.from({ length: count }, (_, message) => ({ route: "devices" as const, - page: 0, - devices: Array.from({ length: perResponse }, () => { - const unit = fleet[intIn(rng, 0, fleet.length - 1)]!; + page: message % 20, + devices: Array.from({ length: intIn(rng, 20, 50) }, () => { + const unit = fleet[zipfIndex(rng, fleet.length)]!; return { ...unit, status: pick(rng, STATUSES), @@ -47,44 +51,74 @@ export function devicesTraffic( tempC: Math.round((15 + rng() * 45) * 10) / 10, alarms: rng() < 0.85 ? 0 : intIn(rng, 1, 12), shadowSynced: rng() < 0.93, - lastSeen: base + r * 60000 - intIn(rng, 0, 86400000), + lastSeen: base + message * 60000 - intIn(rng, 0, 86400000), }; }), })); - - const split = Math.floor(all.length * 0.8); - return { train: all.slice(0, split), holdout: all.slice(split) }; } -const HANDLES = Array.from({ length: 120 }, (_, i) => { - const first = ["ada", "linus", "grace", "alan", "edsger", "barbara", "donald", "radia", "ken", "margaret"][i % 10]!; - const last = ["hopper", "torvalds", "lovelace", "turing", "dijkstra", "liskov", "knuth", "perlman", "thompson", "hamilton"][ - Math.floor(i / 10) % 10 - ]!; - return { handle: `@${first}${last}${i}`, name: `${first[0]!.toUpperCase()}${first.slice(1)} ${last[0]!.toUpperCase()}${last.slice(1)}` }; -}); +const SYMBOLS = [ + "HFLY-USD", "BTC-USD", "ETH-USD", "SOL-USD", "AAPL", "MSFT", "NVDA", "TSLA", + "EUR-USD", "GBP-USD", "USD-JPY", "XAU-USD", +]; +const INTERVALS = ["1m", "5m", "5m", "15m", "1h", "4h", "1d"] as const; +/** GET /v1/candles — a chart window, not a full history dump. */ +export function candlesCorpus(count: number, seed: number): z.output[] { + const rng = mulberry32(seed); + const opens = new Map(SYMBOLS.map((s) => [s, 20 + rng() * 300])); + + return Array.from({ length: count }, () => { + const symbol = SYMBOLS[zipfIndex(rng, SYMBOLS.length)]!; + let price = opens.get(symbol)!; + let t = 1735689600000 + intIn(rng, 0, 5000) * 300000; + const candles = Array.from({ length: intIn(rng, 20, 50) }, () => { + const o = Math.round(price * 100) / 100; + const c = Math.round((o + (rng() - 0.5) * 0.8) * 100) / 100; + const row = { + t, + o, + h: Math.round((Math.max(o, c) + rng() * 0.3) * 100) / 100, + l: Math.round((Math.min(o, c) - rng() * 0.3) * 100) / 100, + c, + v: Math.round(rng() * 50000 * 100) / 100, + trades: intIn(rng, 10, 4000), + }; + price = c; + t += 300000; + return row; + }); + return { route: "candles" as const, symbol, interval: pick(rng, INTERVALS), candles }; + }); +} + +const FIRST = ["Ada", "Linus", "Grace", "Alan", "Edsger", "Barbara", "Donald", "Radia", "Ken", "Margaret"]; +const LAST = ["Hopper", "Torvalds", "Lovelace", "Turing", "Dijkstra", "Liskov", "Knuth", "Perlman", "Thompson", "Hamilton"]; const LEXICON = ( "the of and to in is that it was for on are as with his they at be this have from or had by hot word " + "but what some we can out other were all there when up use your how said an each she which do their time " + "deploy latency cluster rollout incident postmortem throughput regression release migration schema payload" ).split(" "); -/** A recurring cast of authors posting about a recurring vocabulary. */ -export function feedTraffic( - responses: number, - perResponse: number, - seed: number, -): Traffic> { +/** GET /v1/feed — a page of posts from a recurring cast of authors. */ +export function feedCorpus(count: number, seed: number): z.output[] { const rng = mulberry32(seed); - const base = 1754500000000; const hex = (n: number) => Array.from({ length: n }, () => Math.floor(rng() * 16).toString(16)).join(""); - const authors = HANDLES.map((h) => ({ ...h, id: hex(12), verified: rng() < 0.2 })); + const authors = Array.from({ length: 120 }, (_, i) => { + const first = FIRST[i % FIRST.length]!; + const last = LAST[Math.floor(i / FIRST.length) % LAST.length]!; + return { + id: hex(12), + name: `${first} ${last}`, + handle: `@${first.toLowerCase()}${last.toLowerCase()}${i}`, + verified: rng() < 0.2, + }; + }); + const base = 1754500000000; - const all = Array.from({ length: responses }, () => ({ + return Array.from({ length: count }, () => ({ route: "feed" as const, - posts: Array.from({ length: perResponse }, () => { - const author = authors[intIn(rng, 0, authors.length - 1)]!; + posts: Array.from({ length: intIn(rng, 10, 25) }, () => { const sentences = Array.from({ length: intIn(rng, 1, 3) }, () => { const words = Array.from({ length: intIn(rng, 8, 24) }, () => pick(rng, LEXICON)); const s = words.join(" "); @@ -92,7 +126,7 @@ export function feedTraffic( }); return { id: hex(16), - author, + author: authors[zipfIndex(rng, authors.length)]!, body: sentences.join(" "), lang: pick(rng, ["en", "en", "en", "de", "fr", "es", "ja"] as const), likes: intIn(rng, 0, 50000), @@ -103,9 +137,6 @@ export function feedTraffic( }; }), })); - - const split = Math.floor(all.length * 0.8); - return { train: all.slice(0, split), holdout: all.slice(split) }; } -export { DeviceResponse, FeedResponse }; +export { CandleResponse, DeviceResponse, FeedResponse }; From 9c715ac7f342b3afa303dcf2401e0e6885d7e3e3 Mon Sep 17 00:00:00 2001 From: Elia <83713217+eliahilse@users.noreply.github.com> Date: Thu, 20 Aug 2026 16:11:57 +0200 Subject: [PATCH 2/2] feat(web): five corpora, three Hyperfly rows, per-message averages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The benchmark now reports bytes per message over 500-message corpora rather than the size of one large response, because that is the setting in which a trained dictionary can be measured at all. Each route shows columnar, profiled and full so the contribution of each stage is visible rather than blended. Tabs lead with events, where recurring user agents and actor emails make the profile worth 57%, and end with candles, where it is worth nothing — that spread is the honest shape of the feature, and the candles row is left in precisely because it is flat. The footnote states that the profile is trained on the route's own traffic, that the dictionary is an out-of-band artifact, and how many requests it takes to pay for itself. Orders keeps the result where full is larger than profiled: at 184 bytes Brotli's framing costs more than it saves. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_hf1 --- apps/bench/src/profiles.ts | 20 ++-- apps/bench/src/proto.ts | 191 +++++++++++++++++++++++++++++++++++++ apps/web/app/benchmark.tsx | 85 +++++++++++------ apps/web/app/globals.css | 16 +++- apps/web/app/page.tsx | 15 +-- 5 files changed, 281 insertions(+), 46 deletions(-) diff --git a/apps/bench/src/profiles.ts b/apps/bench/src/profiles.ts index 029b3cf..6c5dac2 100644 --- a/apps/bench/src/profiles.ts +++ b/apps/bench/src/profiles.ts @@ -3,6 +3,7 @@ import { train } from "hyperfly"; import { compile, toIR } from "hyperfly/zod"; import { EventResponse, eventsCorpus } from "./corpora/events.js"; import { OrderResponse, ordersCorpus } from "./corpora/orders.js"; +import { candlesProto, devicesProto, eventsProto, feedProto, ordersProto, type ProtoCodec } from "./proto.js"; import { candlesCorpus, devicesCorpus, feedCorpus, CandleResponse, DeviceResponse, FeedResponse } from "./traffic.js"; const enc = new TextEncoder(); @@ -22,6 +23,7 @@ export interface CorpusResult { json: number; gzip: number; brotli: number; + protobuf: number; columnar: number; profiled: number; full: number; @@ -35,6 +37,7 @@ export interface CorpusSuite { route: string; schema: unknown; corpus: readonly unknown[]; + proto: ProtoCodec; } /** @@ -55,6 +58,7 @@ export function measureCorpus(suite: CorpusSuite): CorpusResult { let json = 0; let gzip = 0; let br = 0; + let proto = 0; let col = 0; let prof = 0; let full = 0; @@ -65,6 +69,7 @@ export function measureCorpus(suite: CorpusSuite): CorpusResult { gzip += gzipSync(j, { level: 6 }).length; br += brotli(j, constants.BROTLI_MODE_TEXT); + proto += suite.proto.encode(message).length; col += columnar.encode(message as never).length; const p = profiled.encode(message as never); prof += p.length; @@ -91,6 +96,7 @@ export function measureCorpus(suite: CorpusSuite): CorpusResult { json: mean(json), gzip: mean(gzip), brotli: mean(br), + protobuf: mean(proto), columnar: mean(col), profiled: mean(prof), full: mean(full), @@ -102,11 +108,11 @@ export function measureCorpus(suite: CorpusSuite): CorpusResult { export function defaultSuites(messages = 500): CorpusSuite[] { return [ - { route: "GET /v1/candles", schema: CandleResponse, corpus: candlesCorpus(messages, 0xc9) }, - { route: "GET /v1/devices", schema: DeviceResponse, corpus: devicesCorpus(messages, 0xd9) }, - { route: "GET /v1/feed", schema: FeedResponse, corpus: feedCorpus(messages, 0xf9) }, - { route: "GET /v1/events", schema: EventResponse, corpus: eventsCorpus(messages, 0xe9) }, - { route: "GET /v1/orders/:id", schema: OrderResponse, corpus: ordersCorpus(messages, 0x09) }, + { route: "GET /v1/candles", schema: CandleResponse, corpus: candlesCorpus(messages, 0xc9), proto: candlesProto() }, + { route: "GET /v1/devices", schema: DeviceResponse, corpus: devicesCorpus(messages, 0xd9), proto: devicesProto() }, + { route: "GET /v1/feed", schema: FeedResponse, corpus: feedCorpus(messages, 0xf9), proto: feedProto() }, + { route: "GET /v1/events", schema: EventResponse, corpus: eventsCorpus(messages, 0xe9), proto: eventsProto() }, + { route: "GET /v1/orders/:id", schema: OrderResponse, corpus: ordersCorpus(messages, 0x09), proto: ordersProto() }, ]; } @@ -114,12 +120,12 @@ export function runProfileSuite(suites: CorpusSuite[] = defaultSuites()): Corpus const results = suites.map(measureCorpus); console.log("\ncorpora — per-message averages, profile trained on the route's own traffic"); - console.log(" route msgs json gzip br4 col prof full dictionary"); + console.log(" route msgs json gzip br4 proto col prof full dictionary"); for (const r of results) { const cell = (v: number) => String(v).padStart(6); console.log( ` ${r.route.padEnd(17)} ${String(r.messages).padStart(4)} ${cell(r.json)} ${cell(r.gzip)} ` + - `${cell(r.brotli)} ${cell(r.columnar)} ${cell(r.profiled)} ${cell(r.full)} ` + + `${cell(r.brotli)} ${cell(r.protobuf)} ${cell(r.columnar)} ${cell(r.profiled)} ${cell(r.full)} ` + `${r.dictEntries} entries / ${r.dictBytes}B` + (r.breakEven ? ` (pays for itself after ${r.breakEven} requests)` : ""), ); diff --git a/apps/bench/src/proto.ts b/apps/bench/src/proto.ts index a2211a0..d5ad79f 100644 --- a/apps/bench/src/proto.ts +++ b/apps/bench/src/proto.ts @@ -206,3 +206,194 @@ export function feedProto(): ProtoCodec { }), ); } + +const [ORDER_STATUS_TO, ORDER_STATUS_FROM] = enumMaps([ + ["pending", "PENDING"], ["paid", "PAID"], ["packed", "PACKED"], + ["shipped", "SHIPPED"], ["delivered", "DELIVERED"], ["refunded", "REFUNDED"], +]); +const [CURRENCY_TO, CURRENCY_FROM] = enumMaps([ + ["USD", "USD"], ["EUR", "EUR"], ["GBP", "GBP"], ["JPY", "JPY"], ["CHF", "CHF"], +]); +const [TIER_TO, TIER_FROM] = enumMaps([ + ["free", "FREE"], ["standard", "STANDARD"], ["plus", "PLUS"], ["enterprise", "ENTERPRISE"], +]); +const [COUNTRY_TO, COUNTRY_FROM] = enumMaps([ + ["US", "US"], ["GB", "GB"], ["DE", "DE"], ["FR", "FR"], ["JP", "JP"], ["CA", "CA"], ["AU", "AU"], ["NL", "NL"], +]); + +export function ordersProto(): ProtoCodec { + const root = protobuf.Root.fromJSON({ + nested: { + OrderStatus: { + values: { + ORDER_STATUS_UNSPECIFIED: 0, PENDING: 1, PAID: 2, PACKED: 3, + SHIPPED: 4, DELIVERED: 5, REFUNDED: 6, + }, + }, + Currency: { values: { CURRENCY_UNSPECIFIED: 0, USD: 1, EUR: 2, GBP: 3, JPY: 4, CHF: 5 } }, + Tier: { values: { TIER_UNSPECIFIED: 0, FREE: 1, STANDARD: 2, PLUS: 3, ENTERPRISE: 4 } }, + Country: { + values: { + COUNTRY_UNSPECIFIED: 0, US: 1, GB: 2, DE: 3, FR: 4, JP: 5, CA: 6, AU: 7, NL: 8, + }, + }, + Customer: { + fields: { + id: { type: "string", id: 1 }, + name: { type: "string", id: 2 }, + email: { type: "string", id: 3 }, + tier: { type: "Tier", id: 4 }, + }, + }, + Shipping: { + fields: { + country: { type: "Country", id: 1 }, + city: { type: "string", id: 2 }, + postcode: { type: "string", id: 3 }, + }, + }, + Item: { + fields: { + sku: { type: "string", id: 1 }, + title: { type: "string", id: 2 }, + qty: { type: "int32", id: 3 }, + unitPrice: { type: "double", id: 4 }, + taxRate: { type: "double", id: 5 }, + }, + }, + Order: { + fields: { + route: { type: "string", id: 1 }, + id: { type: "string", id: 2 }, + status: { type: "OrderStatus", id: 3 }, + currency: { type: "Currency", id: 4 }, + customer: { type: "Customer", id: 5 }, + shipping: { type: "Shipping", id: 6 }, + items: { rule: "repeated", type: "Item", id: 7 }, + subtotal: { type: "double", id: 8 }, + tax: { type: "double", id: 9 }, + total: { type: "double", id: 10 }, + placedAt: { type: "int64", id: 11 }, + note: { type: "string", id: 12 }, + }, + }, + }, + }); + type Customer = { tier: string } & Record; + type Shipping = { country: string } & Record; + type Payload = { + status: string; + currency: string; + customer: Customer; + shipping: Shipping; + note: string | null; + } & Record; + return codec( + root, + "Order", + (p) => ({ + ...(p as Payload), + status: ORDER_STATUS_TO[(p as Payload).status]!, + currency: CURRENCY_TO[(p as Payload).currency]!, + customer: { ...(p as Payload).customer, tier: TIER_TO[(p as Payload).customer.tier]! }, + shipping: { ...(p as Payload).shipping, country: COUNTRY_TO[(p as Payload).shipping.country]! }, + // notes are full sentences or null; the corpus never emits "", so unset round-trips as null + note: (p as Payload).note ?? "", + }), + (o) => ({ + ...o, + status: ORDER_STATUS_FROM[o.status as string]!, + currency: CURRENCY_FROM[o.currency as string]!, + customer: { ...(o.customer as Customer), tier: TIER_FROM[(o.customer as Customer).tier]! }, + shipping: { ...(o.shipping as Shipping), country: COUNTRY_FROM[(o.shipping as Shipping).country]! }, + note: o.note === "" ? null : o.note, + }), + ); +} + +const [EVENT_TYPE_TO, EVENT_TYPE_FROM] = enumMaps([ + ["user.login", "USER_LOGIN"], ["user.logout", "USER_LOGOUT"], + ["file.upload", "FILE_UPLOAD"], ["file.delete", "FILE_DELETE"], + ["billing.charge", "BILLING_CHARGE"], ["billing.refund", "BILLING_REFUND"], + ["project.create", "PROJECT_CREATE"], ["project.archive", "PROJECT_ARCHIVE"], + ["member.invite", "MEMBER_INVITE"], ["member.remove", "MEMBER_REMOVE"], +]); +const [RESOURCE_TYPE_TO, RESOURCE_TYPE_FROM] = enumMaps([ + ["user", "USER"], ["file", "FILE"], ["project", "PROJECT"], + ["invoice", "INVOICE"], ["member", "MEMBER"], ["apikey", "APIKEY"], +]); + +export function eventsProto(): ProtoCodec { + const root = protobuf.Root.fromJSON({ + nested: { + EventType: { + values: { + EVENT_TYPE_UNSPECIFIED: 0, USER_LOGIN: 1, USER_LOGOUT: 2, FILE_UPLOAD: 3, FILE_DELETE: 4, + BILLING_CHARGE: 5, BILLING_REFUND: 6, PROJECT_CREATE: 7, PROJECT_ARCHIVE: 8, + MEMBER_INVITE: 9, MEMBER_REMOVE: 10, + }, + }, + ResourceType: { + values: { + RESOURCE_TYPE_UNSPECIFIED: 0, USER: 1, FILE: 2, PROJECT: 3, INVOICE: 4, MEMBER: 5, APIKEY: 6, + }, + }, + Region: { + values: { + REGION_UNSPECIFIED: 0, US_EAST: 1, US_WEST: 2, EU_CENTRAL: 3, EU_WEST: 4, + AP_SOUTH: 5, AP_NORTHEAST: 6, SA_EAST: 7, AF_SOUTH: 8, + }, + }, + Event: { + fields: { + id: { type: "string", id: 1 }, + type: { type: "EventType", id: 2 }, + actorId: { type: "string", id: 3 }, + actorEmail: { type: "string", id: 4 }, + resourceId: { type: "string", id: 5 }, + resourceType: { type: "ResourceType", id: 6 }, + ip: { type: "string", id: 7 }, + userAgent: { type: "string", id: 8 }, + region: { type: "Region", id: 9 }, + durationMs: { type: "int32", id: 10 }, + ok: { type: "bool", id: 11 }, + at: { type: "int64", id: 12 }, + }, + }, + EventsResponse: { + fields: { + route: { type: "string", id: 1 }, + cursor: { type: "string", id: 2 }, + events: { rule: "repeated", type: "Event", id: 3 }, + }, + }, + }, + }); + type Event = { type: string; resourceType: string; region: string } & Record; + type Payload = { cursor: string | null; events: Event[] } & Record; + return codec( + root, + "EventsResponse", + (p) => ({ + ...(p as Payload), + // cursors are 16 hex chars or absent; the corpus never emits "", so unset round-trips as null + cursor: (p as Payload).cursor ?? "", + events: (p as Payload).events.map((e) => ({ + ...e, + type: EVENT_TYPE_TO[e.type]!, + resourceType: RESOURCE_TYPE_TO[e.resourceType]!, + region: REGION_TO[e.region]!, + })), + }), + (o) => ({ + ...o, + cursor: o.cursor === "" ? null : o.cursor, + events: (o.events as Event[]).map((e) => ({ + ...e, + type: EVENT_TYPE_FROM[e.type]!, + resourceType: RESOURCE_TYPE_FROM[e.resourceType]!, + region: REGION_FROM[e.region]!, + })), + }), + ); +} diff --git a/apps/web/app/benchmark.tsx b/apps/web/app/benchmark.tsx index 03e35be..cb3bda9 100644 --- a/apps/web/app/benchmark.tsx +++ b/apps/web/app/benchmark.tsx @@ -6,7 +6,7 @@ import { useInView } from "./reveal"; type Row = { label: string; bytes: number; - kind?: "baseline" | "generic" | "binary" | "hyperfly" | "profile"; + kind?: "baseline" | "generic" | "binary" | "hyperfly" | "profile" | "full"; }; type Payload = { @@ -18,43 +18,74 @@ type Payload = { const PAYLOADS: Payload[] = [ { - route: "GET /v1/candles", - shape: "1 000 OHLCV rows · 7 numeric columns · monotonic timestamps", + route: "GET /v1/events", + shape: "500 messages · 20–50 audit records each · recurring actors and user agents", rows: [ - { label: "JSON", bytes: 88209, kind: "baseline" }, - { label: "JSON + gzip", bytes: 21705, kind: "generic" }, - { label: "JSON + Brotli — edge q4", bytes: 21315, kind: "generic" }, - { label: "Protobuf", bytes: 56996, kind: "binary" }, - { label: "Hyperfly · columnar", bytes: 12628, kind: "hyperfly" }, - { label: "Hyperfly + Brotli", bytes: 7915, kind: "profile" }, + { label: "JSON", bytes: 12687, kind: "baseline" }, + { label: "JSON + gzip", bytes: 2503, kind: "generic" }, + { label: "JSON + Brotli — edge q4", bytes: 2512, kind: "generic" }, + { label: "Protobuf", bytes: 7190, kind: "binary" }, + { label: "Hyperfly · columnar", bytes: 2109, kind: "hyperfly" }, + { label: "Hyperfly · profiled", bytes: 896, kind: "profile" }, + { label: "Hyperfly · full", bytes: 823, kind: "full" }, ], - note: "Columns ride separately: timestamps become deltas, and prices that are exact decimals travel as integer mantissas instead of eight raw bytes. No entropy coder is involved yet — layout alone, uncompressed, undercuts what the edge actually serves. It clears Brotli's offline q11 ceiling too; the harness in the repo has the receipts.", + note: "An audit log repeats itself across requests, not within one: the same user agents, the same actor emails, the same resource ids, request after request. A compressor only ever sees one response and has to rediscover them every time. The profile learned 692 values once, and pays for itself after ten requests.", }, { route: "GET /v1/devices", - shape: "500 telemetry records · enums · bounded integers · repeated ids", + shape: "500 messages · 20–50 telemetry records each · fixed 400-device fleet", + rows: [ + { label: "JSON", bytes: 7994, kind: "baseline" }, + { label: "JSON + gzip", bytes: 1473, kind: "generic" }, + { label: "JSON + Brotli — edge q4", bytes: 1422, kind: "generic" }, + { label: "Protobuf", bytes: 2007, kind: "binary" }, + { label: "Hyperfly · columnar", bytes: 896, kind: "hyperfly" }, + { label: "Hyperfly · profiled", bytes: 705, kind: "profile" }, + { label: "Hyperfly · full", bytes: 638, kind: "full" }, + ], + note: "An enum with six members is an index, not a string. Bounded integers ship as offsets from their declared minimum and booleans pack into bitmaps — that is the columnar row. The profile then learns the fleet: the device ids that recur on every page.", + }, + { + route: "GET /v1/orders/:id", + shape: "500 messages · one order each · fixed catalogue and customer base", rows: [ - { label: "JSON", bytes: 113443, kind: "baseline" }, - { label: "JSON + gzip", bytes: 17026, kind: "generic" }, - { label: "JSON + Brotli — edge q4", bytes: 16823, kind: "generic" }, - { label: "Protobuf", bytes: 29056, kind: "binary" }, - { label: "Hyperfly · columnar", bytes: 11945, kind: "hyperfly" }, - { label: "Hyperfly + Brotli", bytes: 9726, kind: "profile" }, + { label: "JSON", bytes: 782, kind: "baseline" }, + { label: "JSON + gzip", bytes: 423, kind: "generic" }, + { label: "JSON + Brotli — edge q4", bytes: 408, kind: "generic" }, + { label: "Protobuf", bytes: 388, kind: "binary" }, + { label: "Hyperfly · columnar", bytes: 271, kind: "hyperfly" }, + { label: "Hyperfly · profiled", bytes: 184, kind: "profile" }, + { label: "Hyperfly · full", bytes: 188, kind: "full" }, ], - note: "An enum with six members is an index, not a string. Bounded integers ship as offsets from their declared minimum, booleans pack into bitmaps, and repetitive id columns deflate inside the codec — so the uncompressed wire already undercuts what the edge serves.", + note: "The single-entity response, and the case a general compressor handles worst: under a kilobyte there is nothing yet to build a window from. Note that full is larger than profiled here — at 184 bytes, Brotli's framing costs more than it saves, so the right configuration for this route is to skip it.", }, { route: "GET /v1/feed", - shape: "50 posts · nested authors · free-form text bodies", + shape: "500 messages · 10–25 posts each · recurring cast of 120 authors", + rows: [ + { label: "JSON", bytes: 6863, kind: "baseline" }, + { label: "JSON + gzip", bytes: 2307, kind: "generic" }, + { label: "JSON + Brotli — edge q4", bytes: 2294, kind: "generic" }, + { label: "Protobuf", bytes: 4396, kind: "binary" }, + { label: "Hyperfly · columnar", bytes: 1908, kind: "hyperfly" }, + { label: "Hyperfly · profiled", bytes: 1536, kind: "profile" }, + { label: "Hyperfly · full", bytes: 1535, kind: "full" }, + ], + note: "Prose is the hard case: the bodies are genuinely new every time and nothing can invent redundancy that is not there. What does recur are the authors, so that is what the profile takes. Full and profiled land within a byte of each other because the dictionary already removed what Brotli was living on.", + }, + { + route: "GET /v1/candles", + shape: "500 messages · 20–50 OHLCV rows each · monotonic timestamps", rows: [ - { label: "JSON", bytes: 22245, kind: "baseline" }, - { label: "JSON + gzip", bytes: 7775, kind: "generic" }, - { label: "JSON + Brotli — edge q4", bytes: 7691, kind: "generic" }, - { label: "Protobuf", bytes: 15232, kind: "binary" }, - { label: "Hyperfly · columnar", bytes: 6513, kind: "hyperfly" }, - { label: "Hyperfly + Brotli", bytes: 6443, kind: "profile" }, + { label: "JSON", bytes: 3225, kind: "baseline" }, + { label: "JSON + gzip", bytes: 928, kind: "generic" }, + { label: "JSON + Brotli — edge q4", bytes: 842, kind: "generic" }, + { label: "Protobuf", bytes: 2034, kind: "binary" }, + { label: "Hyperfly · columnar", bytes: 496, kind: "hyperfly" }, + { label: "Hyperfly · profiled", bytes: 496, kind: "profile" }, + { label: "Hyperfly · full", bytes: 372, kind: "full" }, ], - note: "Prose does not vanish under a schema — so text columns pack through deflate inside the codec, with shared context across every row, and unpack bit-exactly. Structure travels as columns around them. The all-text route now lands ahead of Brotli's offline ceiling instead of behind it.", + note: "Timestamps become deltas and exact-decimal prices travel as integer mantissas rather than eight raw bytes. The profile changes nothing at all here, and the row is left in to show it: this route's only string sits outside the array, so there is no column for a dictionary to key on.", }, ]; @@ -137,7 +168,7 @@ export function Benchmark() { ))} - measured — synthetic corpora, reference implementation + measured — per message, 500-message corpora

{payload.shape}

diff --git a/apps/web/app/globals.css b/apps/web/app/globals.css index 7df2ba0..fcd6a30 100644 --- a/apps/web/app/globals.css +++ b/apps/web/app/globals.css @@ -465,7 +465,7 @@ h3 { .bar { display: grid; - grid-template-columns: 10.5rem minmax(0, 1fr) 6rem 3rem; + grid-template-columns: 12rem minmax(0, 1fr) 6rem 3rem; align-items: center; gap: 14px; padding: 7px 0; @@ -518,12 +518,17 @@ h3 { } .bar[data-kind="profile"] .bar-fill { - background: rgba(95, 217, 255, 0.62); - box-shadow: 0 0 26px rgba(95, 217, 255, 0.28); + background: rgba(95, 217, 255, 0.55); +} + +.bar[data-kind="full"] .bar-fill { + background: rgba(95, 217, 255, 0.85); + box-shadow: 0 0 26px rgba(95, 217, 255, 0.3); } .bar[data-kind="hyperfly"] .bar-label, -.bar[data-kind="profile"] .bar-label { +.bar[data-kind="profile"] .bar-label, +.bar[data-kind="full"] .bar-label { color: var(--text); } @@ -544,7 +549,8 @@ h3 { } .bar[data-kind="hyperfly"] .bar-ratio, -.bar[data-kind="profile"] .bar-ratio { +.bar[data-kind="profile"] .bar-ratio, +.bar[data-kind="full"] .bar-ratio { color: var(--accent); } diff --git a/apps/web/app/page.tsx b/apps/web/app/page.tsx index 4fc16d1..d4b9a2a 100644 --- a/apps/web/app/page.tsx +++ b/apps/web/app/page.tsx @@ -163,13 +163,14 @@ export default function Home() {

- Measured with the TypeScript reference implementation on deterministic synthetic - corpora — reproduce with `bun run bench` in the repo. The Brotli row is q4, the - level edges actually run on dynamic responses; q6 (nginx's default) and the q11 - offline ceiling are in the harness — and uncompressed columnar output clears even - q11 on all three corpora. Protobuf gets proper enums and int64. Results still - depend on the payload; the feed tab is where the margin is thinnest, and it says - why. + Bytes per message, averaged over 500 independent responses per route, measured + with the TypeScript reference implementation — reproduce with `bun run bench`. The + profile is trained on the route's own traffic, which is what a deployment does; + it is an out-of-band artifact, and the repo reports its size and how many requests + it takes to pay for itself (ten for events, thirty-five for orders). The Brotli row + is q4, the level edges actually run on dynamic responses. Protobuf gets proper + enums and int64. Corpora are synthetic but shaped like real routes: a fixed device + fleet, a fixed product catalogue, a recurring cast of authors.