From d154acb3d4f904b9d570aa892ec18db2d8981fb0 Mon Sep 17 00:00:00 2001 From: Chris Miles Date: Fri, 7 Aug 2026 06:14:33 +0000 Subject: [PATCH 1/3] docs: tell a source-backed performance story --- .gitignore | 1 + Makefile | 8 +- bin/collect-performance-stats.mjs | 176 +++++ docs/.vitepress/config.mts | 5 +- docs/.vitepress/data/content-quality.json | 1 + docs/.vitepress/data/performance-stats.json | 696 ++++++++++++++++ docs/.vitepress/data/proof-stats.json | 38 +- .../theme/components/PerformanceStory.vue | 741 ++++++++++++++++++ docs/index.md | 2 +- docs/package.json | 2 + docs/performance.md | 13 + 11 files changed, 1661 insertions(+), 22 deletions(-) create mode 100644 bin/collect-performance-stats.mjs create mode 100644 docs/.vitepress/data/performance-stats.json create mode 100644 docs/.vitepress/theme/components/PerformanceStory.vue create mode 100644 docs/performance.md diff --git a/.gitignore b/.gitignore index da14473..c00b7f3 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,7 @@ backend/frontend/dist/ /bin/* !/bin/collect-proof-stats.mjs +!/bin/collect-performance-stats.mjs _data/ # Local environment files diff --git a/Makefile b/Makefile index 0ea098c..33755f4 100644 --- a/Makefile +++ b/Makefile @@ -78,10 +78,16 @@ docs-proof-stats: ##@docs Refresh checked-in proof statistics from sibling repos docs-check-proof-stats: ##@docs Verify checked-in proof statistics match sibling repositories @cd docs && npm run proof:check +docs-performance-stats: ##@docs Refresh checked-in performance statistics from sibling repositories + @cd docs && npm run performance:refresh + +docs-check-performance-stats: ##@docs Verify checked-in performance statistics match sibling repositories + @cd docs && npm run performance:check + docs-check-scenarios: ##@docs Verify generated scenario pages match framework specs @cd ../goforj && go run ./cmd/forj scenario:generate --all --check -docs-build: docs-check-proof-stats docs-check-scenarios ##@docs Verify generated evidence and build VitePress docs +docs-build: docs-check-proof-stats docs-check-performance-stats docs-check-scenarios ##@docs Verify generated evidence and build VitePress docs @cd docs && npm run build docs-embed: ##@docs Copy built docs into backend embed folder diff --git a/bin/collect-performance-stats.mjs b/bin/collect-performance-stats.mjs new file mode 100644 index 0000000..64218cd --- /dev/null +++ b/bin/collect-performance-stats.mjs @@ -0,0 +1,176 @@ +#!/usr/bin/env node + +import crypto from 'node:crypto' +import fs from 'node:fs' +import path from 'node:path' +import { execFileSync } from 'node:child_process' +import { fileURLToPath } from 'node:url' + +const docsRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..') +const check = process.argv.includes('--check') +const rootArg = process.argv.slice(2).find((arg) => arg !== '--check') +const reposRoot = path.resolve(rootArg || path.join(docsRoot, '..')) +const outputFile = path.join(docsRoot, 'docs', '.vitepress', 'data', 'performance-stats.json') +const proofFile = path.join(docsRoot, 'docs', '.vitepress', 'data', 'proof-stats.json') + +const benchmarkFile = (repo) => path.join(reposRoot, repo, 'docs', 'bench', 'benchmarks_rows.json') +const readJSON = (file) => JSON.parse(fs.readFileSync(file, 'utf8')) +const required = (value, description) => { + if (value === undefined || value === null) throw new Error(`missing benchmark row: ${description}`) + return value +} +const median = (values) => { + const sorted = [...values].sort((a, b) => a - b) + return sorted[Math.floor(sorted.length / 2)] +} +const labels = { + ftp: 'FTP', + gcppubsub: 'Google Pub/Sub', + gcs: 'GCS', + mysql: 'MySQL', + nats: 'NATS', + nats_bucket_ttl: 'NATS KV + TTL', + natsjetstream: 'NATS JetStream', + postgres: 'PostgreSQL', + rabbitmq: 'RabbitMQ', + rclone_local: 'rclone local', + sftp: 'SFTP', + sns: 'SNS', + sqs: 'SQS', + sqlite: 'SQLite', + workerpool: 'Worker pool' +} +const title = (value) => { + const normalized = value.replace(/^sql_/, '') + return labels[normalized] || normalized + .replaceAll('_', ' ') + .replace(/\b\w/g, (character) => character.toUpperCase()) +} + +// source records the exact committed benchmark artifact consumed by the page. +const source = (repo) => { + const file = benchmarkFile(repo) + const relative = path.relative(path.join(reposRoot, repo), file) + return { + repo, + path: relative, + revision: execFileSync('git', ['-C', path.join(reposRoot, repo), 'log', '-1', '--format=%H', '--', relative], { encoding: 'utf8' }).trim(), + sha256: crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex') + } +} + +const cacheRaw = readJSON(benchmarkFile('cache')) +const queueRaw = readJSON(benchmarkFile('queue')) +const eventsRaw = readJSON(benchmarkFile('events')) +const storageRaw = readJSON(benchmarkFile('storage')) +const webRaw = readJSON(benchmarkFile('web')) +const proof = readJSON(proofFile) + +const cacheRows = required(cacheRaw.get_bytes, 'cache get_bytes').map((row) => ({ + label: title(row.Driver), + value: row.NsOp, + bytes: row.BytesOp, + allocs: row.AllocsOp, + kind: row.Driver === 'memory' ? 'process' : ['file', 'sql_sqlite'].includes(row.Driver) ? 'local' : 'service' +})) + +const queueRows = queueRaw + .filter((row) => row.driver !== 'null') + .map((row) => ({ + label: title(row.driver), + value: row.ns_op, + bytes: row.b_op, + allocs: row.allocs_op, + kind: ['sync', 'workerpool'].includes(row.driver) ? 'process' : row.driver === 'sqlite' ? 'local' : 'service' + })) + +const eventRows = eventsRaw + .filter((row) => row.name === 'SyncPublishRoundTrip' || row.set === 'Integration') + .map((row) => { + const driver = row.name.includes('/') ? row.name.split('/').at(-1) : 'sync' + return { + label: driver === 'sync' ? 'Synchronous' : title(driver), + value: row.ns_op, + bytes: row.b_op, + allocs: row.allocs_op, + kind: driver === 'sync' ? 'process' : 'service' + } + }) + +const storageRows = required(storageRaw.get_small, 'storage get_small').map((row) => ({ + label: title(row.driver), + value: row.ns_op, + bytes: row.bytes_op, + allocs: row.allocs_op, + kind: row.driver === 'memory' ? 'process' : ['local', 'rclone_local'].includes(row.driver) ? 'local' : 'service' +})) + +const webScenarios = ['live_plain_text', 'static_text', 'path_param_json', 'middleware_chain'].map((scenario) => { + const samples = webRaw.samples.filter((sample) => sample.scenario === scenario) + const frameworks = [...new Set(samples.map((sample) => sample.framework))] + return { + id: scenario, + rows: frameworks.map((framework) => { + const rows = samples.filter((sample) => sample.framework === framework) + return { + label: { + goforj_web: 'GoForj Web', + net_http: 'net/http', + gorilla_mux: 'Gorilla Mux', + httprouter: 'httprouter' + }[framework] || title(framework), + framework, + throughput: median(rows.map((row) => row.throughput_per_second)), + nsOp: median(rows.map((row) => row.nanoseconds_per_op)), + bytes: median(rows.map((row) => row.bytes_per_op)), + allocs: median(rows.map((row) => row.allocs_per_op)) + } + }) + } +}) + +const findRow = (rows, label) => required(rows.find((row) => row.label === label), label) +const staticWeb = required(webScenarios.find((scenario) => scenario.id === 'static_text'), 'web static_text') + +const stats = { + generatedAt: new Date().toISOString().slice(0, 10), + totals: proof.totals, + highlights: [ + { library: 'Cache', label: 'Memory read', value: findRow(cacheRows, 'Memory').value, unit: 'ns/op' }, + { library: 'Queue', label: 'Synchronous dispatch', value: findRow(queueRows, 'Sync').value, unit: 'ns/op' }, + { library: 'Events', label: 'Synchronous round trip', value: findRow(eventRows, 'Synchronous').value, unit: 'ns/op' }, + { library: 'Web', label: 'Static route', value: findRow(staticWeb.rows, 'GoForj Web').throughput, unit: 'ops/s' } + ], + driverStories: [ + { id: 'cache', title: 'Cache reads', operation: 'GetBytes', rows: cacheRows }, + { id: 'queue', title: 'Queue dispatch', operation: 'Queue', rows: queueRows }, + { id: 'events', title: 'Event round trips', operation: 'Publish + handle', rows: eventRows }, + { id: 'storage', title: 'Small-file reads', operation: 'Get', rows: storageRows } + ], + web: { + metadata: webRaw.metadata, + scenarios: webScenarios + }, + benchmarkLibraries: proof.repos + .filter((repo) => repo.benchmarks > 0) + .map((repo) => ({ repo: repo.repo, benchmarks: repo.benchmarks })), + sources: ['cache', 'events', 'queue', 'storage', 'web'].map(source) +} + +const output = JSON.stringify(stats, null, 2) + '\n' + +if (check) { + const current = readJSON(outputFile) + const expected = JSON.parse(output) + delete current.generatedAt + delete expected.generatedAt + if (JSON.stringify(current) !== JSON.stringify(expected)) { + throw new Error(`performance statistics are stale; run: node bin/collect-performance-stats.mjs ${reposRoot}`) + } + console.log(`performance statistics are current: ${outputFile}`) +} else { + fs.mkdirSync(path.dirname(outputFile), { recursive: true }) + fs.writeFileSync(outputFile, output) + console.log(`wrote ${outputFile}`) + console.log({ highlights: stats.highlights, driverStories: stats.driverStories.length }) +} diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index d5bffa9..d32bc14 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -418,6 +418,7 @@ const llmsSectionOrder = [ 'security/', 'frontend/', 'starter-kits.md', + 'performance.md', 'testing/', 'scenarios/', 'operations/', @@ -440,6 +441,7 @@ const llmsSectionTitles: Record = { 'security/': 'Security', 'frontend/': 'Frontend', 'starter-kits.md': 'Starter Kits', + 'performance.md': 'Performance', 'testing/': 'Testing', 'scenarios/': 'Verified Scenarios', 'operations/': 'Operations', @@ -989,7 +991,7 @@ const operationsSidebar = sectionSidebar('Operations', [ { text: 'Metrics', link: '/operations/metrics' }, { text: 'Inspects', link: '/operations/inspects' }, { text: 'Lighthouse', link: '/operations/lighthouse' }, - { text: 'Performance Benchmarks', link: '/operations/performance-benchmarks' }, + { text: 'Performance', link: '/performance' }, { text: 'Backup and Restore', link: '/operations/backups' } ]) @@ -1388,6 +1390,7 @@ export default defineConfig({ ] }, { text: 'Starter Kits', link: '/starter-kits' }, + { text: 'Performance', link: '/performance' }, { text: 'Guides', items: [ diff --git a/docs/.vitepress/data/content-quality.json b/docs/.vitepress/data/content-quality.json index 7b978d0..ab703a7 100644 --- a/docs/.vitepress/data/content-quality.json +++ b/docs/.vitepress/data/content-quality.json @@ -5,6 +5,7 @@ ["index.md", "landing"], ["**/index.md", "landing"], ["starter-kits.md", "landing"], + ["performance.md", "landing"], ["site-preview.md", "landing"], ["libraries/**/*.md", "generated-library"], ["scenarios/**/*.md", "generated-scenario"], diff --git a/docs/.vitepress/data/performance-stats.json b/docs/.vitepress/data/performance-stats.json new file mode 100644 index 0000000..3b423a1 --- /dev/null +++ b/docs/.vitepress/data/performance-stats.json @@ -0,0 +1,696 @@ +{ + "generatedAt": "2026-08-07", + "totals": { + "unitTests": 1461, + "integrationTests": 952, + "testFunctions": 3401, + "benchmarks": 85, + "drivers": 48, + "libraries": 18 + }, + "highlights": [ + { + "library": "Cache", + "label": "Memory read", + "value": 70, + "unit": "ns/op" + }, + { + "library": "Queue", + "label": "Synchronous dispatch", + "value": 282.5, + "unit": "ns/op" + }, + { + "library": "Events", + "label": "Synchronous round trip", + "value": 371.8, + "unit": "ns/op" + }, + { + "library": "Web", + "label": "Static route", + "value": 11047264, + "unit": "ops/s" + } + ], + "driverStories": [ + { + "id": "cache", + "title": "Cache reads", + "operation": "GetBytes", + "rows": [ + { + "label": "Memory", + "value": 70, + "bytes": 1, + "allocs": 1, + "kind": "process" + }, + { + "label": "File", + "value": 100977, + "bytes": 1352, + "allocs": 9, + "kind": "local" + }, + { + "label": "Redis", + "value": 73175, + "bytes": 200, + "allocs": 8, + "kind": "service" + }, + { + "label": "Memcached", + "value": 70363, + "bytes": 192, + "allocs": 9, + "kind": "service" + }, + { + "label": "PostgreSQL", + "value": 86657, + "bytes": 1064, + "allocs": 32, + "kind": "service" + }, + { + "label": "MySQL", + "value": 81964, + "bytes": 744, + "allocs": 26, + "kind": "service" + }, + { + "label": "SQLite", + "value": 7494, + "bytes": 1432, + "allocs": 39, + "kind": "local" + }, + { + "label": "NATS", + "value": 96450, + "bytes": 2573, + "allocs": 40, + "kind": "service" + }, + { + "label": "NATS KV + TTL", + "value": 82872, + "bytes": 2556, + "allocs": 40, + "kind": "service" + } + ] + }, + { + "id": "queue", + "title": "Queue dispatch", + "operation": "Queue", + "rows": [ + { + "label": "Sync", + "value": 282.5, + "bytes": 408, + "allocs": 6, + "kind": "process" + }, + { + "label": "Worker pool", + "value": 650, + "bytes": 456, + "allocs": 7, + "kind": "process" + }, + { + "label": "Redis", + "value": 95295, + "bytes": 2113, + "allocs": 33, + "kind": "service" + }, + { + "label": "NATS", + "value": 774.1, + "bytes": 1258, + "allocs": 13, + "kind": "service" + }, + { + "label": "SQS", + "value": 1873911, + "bytes": 94784, + "allocs": 1082, + "kind": "service" + }, + { + "label": "RabbitMQ", + "value": 165780, + "bytes": 1882, + "allocs": 57, + "kind": "service" + }, + { + "label": "MySQL", + "value": 2286406, + "bytes": 3303, + "allocs": 62, + "kind": "service" + }, + { + "label": "PostgreSQL", + "value": 1056731, + "bytes": 3809, + "allocs": 78, + "kind": "service" + }, + { + "label": "SQLite", + "value": 202380, + "bytes": 1931, + "allocs": 47, + "kind": "local" + } + ] + }, + { + "id": "events", + "title": "Event round trips", + "operation": "Publish + handle", + "rows": [ + { + "label": "Synchronous", + "value": 371.8, + "bytes": 320, + "allocs": 9, + "kind": "process" + }, + { + "label": "Google Pub/Sub", + "value": 52848425, + "bytes": 21971, + "allocs": 369, + "kind": "service" + }, + { + "label": "Kafka", + "value": 499091, + "bytes": 5040, + "allocs": 67, + "kind": "service" + }, + { + "label": "NATS JetStream", + "value": 127165, + "bytes": 3609, + "allocs": 60, + "kind": "service" + }, + { + "label": "NATS", + "value": 73316, + "bytes": 681, + "allocs": 18, + "kind": "service" + }, + { + "label": "Redis", + "value": 95149, + "bytes": 914, + "allocs": 31, + "kind": "service" + }, + { + "label": "SNS", + "value": 2553117, + "bytes": 104563, + "allocs": 1271, + "kind": "service" + } + ] + }, + { + "id": "storage", + "title": "Small-file reads", + "operation": "Get", + "rows": [ + { + "label": "Local", + "value": 13811.52516435556, + "bytes": 1536.2368929893375, + "allocs": 14.001602121429755, + "kind": "local" + }, + { + "label": "Memory", + "value": 752.9147821215927, + "bytes": 496.081603888653, + "allocs": 9.00039452721485, + "kind": "process" + }, + { + "label": "GCS", + "value": 210780.62913907284, + "bytes": 201477.99157134257, + "allocs": 6831.402167369055, + "kind": "service" + }, + { + "label": "FTP", + "value": 243278.00972222222, + "bytes": 5429.788888888889, + "allocs": 99.03194444444445, + "kind": "service" + }, + { + "label": "rclone local", + "value": 17957.60648504438, + "bytes": 2168.0258580883483, + "allocs": 21.000564363039352, + "kind": "local" + }, + { + "label": "Redis", + "value": 79510.93253066788, + "bytes": 936.0327124034529, + "allocs": 24.000227169468424, + "kind": "service" + }, + { + "label": "S3", + "value": 382576.1599081867, + "bytes": 45419.268553940325, + "allocs": 630.1384850803366, + "kind": "service" + }, + { + "label": "SFTP", + "value": 429232.65321888414, + "bytes": 2523.7836909871244, + "allocs": 47.01030042918455, + "kind": "service" + } + ] + } + ], + "web": { + "metadata": { + "go_version": "go1.26.1", + "goos": "linux", + "goarch": "arm64", + "cpu": "arm64 (CPU model unavailable)", + "kernel": "Linux 7.0.11-orbstack-00360-gc9bc4d96ac70", + "gomaxprocs": 1, + "sample_count": 7, + "benchmark_time": "1s", + "repository_revision": "df4a3d1f75f6", + "repository_dirty": false, + "benchmark_input_fingerprint": "sha256:34d8f1b557a3681b44748966456fee27d79c89111d49b01fa6da04ae6b6323c2", + "build_settings": [ + { + "name": "CGO_ENABLED", + "value": "1" + }, + { + "name": "GOARM64", + "value": "v8.0" + }, + { + "name": "GODEBUG", + "value": "" + }, + { + "name": "GOEXPERIMENT", + "value": "" + }, + { + "name": "GOFLAGS", + "value": "" + } + ], + "dependencies": [ + { + "name": "net/http", + "module": "standard library", + "version": "go1.26.1" + }, + { + "name": "GoForj Web", + "module": "github.com/goforj/web", + "version": "local checkout" + }, + { + "name": "Echo", + "module": "github.com/labstack/echo/v5", + "version": "v5.1.0" + }, + { + "name": "Gin", + "module": "github.com/gin-gonic/gin", + "version": "v1.12.0" + }, + { + "name": "Chi", + "module": "github.com/go-chi/chi/v5", + "version": "v5.3.1" + }, + { + "name": "Gorilla Mux", + "module": "github.com/gorilla/mux", + "version": "v1.8.1" + }, + { + "name": "httprouter", + "module": "github.com/julienschmidt/httprouter", + "version": "v1.3.0" + } + ] + }, + "scenarios": [ + { + "id": "live_plain_text", + "rows": [ + { + "label": "GoForj Web", + "framework": "goforj_web", + "throughput": 118993, + "nsOp": 8404, + "bytes": 4891, + "allocs": 60 + }, + { + "label": "net/http", + "framework": "net_http", + "throughput": 117923, + "nsOp": 8480, + "bytes": 4891, + "allocs": 60 + }, + { + "label": "Echo", + "framework": "echo", + "throughput": 117586, + "nsOp": 8504, + "bytes": 4915, + "allocs": 61 + }, + { + "label": "Gin", + "framework": "gin", + "throughput": 118609, + "nsOp": 8431, + "bytes": 4923, + "allocs": 60 + }, + { + "label": "Chi", + "framework": "chi", + "throughput": 116782, + "nsOp": 8563, + "bytes": 5259, + "allocs": 62 + }, + { + "label": "Gorilla Mux", + "framework": "gorilla_mux", + "throughput": 113899, + "nsOp": 8780, + "bytes": 5739, + "allocs": 67 + }, + { + "label": "httprouter", + "framework": "httprouter", + "throughput": 120078, + "nsOp": 8328, + "bytes": 4891, + "allocs": 60 + } + ] + }, + { + "id": "static_text", + "rows": [ + { + "label": "GoForj Web", + "framework": "goforj_web", + "throughput": 11047264, + "nsOp": 90.52, + "bytes": 16, + "allocs": 1 + }, + { + "label": "net/http", + "framework": "net_http", + "throughput": 8105840, + "nsOp": 123.4, + "bytes": 16, + "allocs": 1 + }, + { + "label": "Echo", + "framework": "echo", + "throughput": 7490212, + "nsOp": 133.5, + "bytes": 40, + "allocs": 2 + }, + { + "label": "Gin", + "framework": "gin", + "throughput": 9447802, + "nsOp": 105.8, + "bytes": 48, + "allocs": 1 + }, + { + "label": "Chi", + "framework": "chi", + "throughput": 4079741, + "nsOp": 245.1, + "bytes": 384, + "allocs": 3 + }, + { + "label": "Gorilla Mux", + "framework": "gorilla_mux", + "throughput": 2210706, + "nsOp": 452.3, + "bytes": 864, + "allocs": 8 + }, + { + "label": "httprouter", + "framework": "httprouter", + "throughput": 15683540, + "nsOp": 63.76, + "bytes": 16, + "allocs": 1 + } + ] + }, + { + "id": "path_param_json", + "rows": [ + { + "label": "GoForj Web", + "framework": "goforj_web", + "throughput": 5937455, + "nsOp": 168.4, + "bytes": 32, + "allocs": 2 + }, + { + "label": "net/http", + "framework": "net_http", + "throughput": 4358405, + "nsOp": 229.4, + "bytes": 48, + "allocs": 3 + }, + { + "label": "Echo", + "framework": "echo", + "throughput": 5351530, + "nsOp": 186.9, + "bytes": 32, + "allocs": 2 + }, + { + "label": "Gin", + "framework": "gin", + "throughput": 5302719, + "nsOp": 188.6, + "bytes": 48, + "allocs": 3 + }, + { + "label": "Chi", + "framework": "chi", + "throughput": 2257553, + "nsOp": 443, + "bytes": 736, + "allocs": 6 + }, + { + "label": "Gorilla Mux", + "framework": "gorilla_mux", + "throughput": 1548628, + "nsOp": 645.7, + "bytes": 1184, + "allocs": 10 + }, + { + "label": "httprouter", + "framework": "httprouter", + "throughput": 6261053, + "nsOp": 159.7, + "bytes": 64, + "allocs": 3 + } + ] + }, + { + "id": "middleware_chain", + "rows": [ + { + "label": "GoForj Web", + "framework": "goforj_web", + "throughput": 5834930, + "nsOp": 171.4, + "bytes": 32, + "allocs": 2 + }, + { + "label": "net/http", + "framework": "net_http", + "throughput": 3375985, + "nsOp": 296.2, + "bytes": 400, + "allocs": 4 + }, + { + "label": "Echo", + "framework": "echo", + "throughput": 2674599, + "nsOp": 373.9, + "bytes": 440, + "allocs": 8 + }, + { + "label": "Gin", + "framework": "gin", + "throughput": 3559858, + "nsOp": 280.9, + "bytes": 400, + "allocs": 4 + }, + { + "label": "Chi", + "framework": "chi", + "throughput": 2456688, + "nsOp": 407.1, + "bytes": 768, + "allocs": 6 + }, + { + "label": "Gorilla Mux", + "framework": "gorilla_mux", + "throughput": 1428773, + "nsOp": 699.9, + "bytes": 1320, + "allocs": 14 + }, + { + "label": "httprouter", + "framework": "httprouter", + "throughput": 4359708, + "nsOp": 229.4, + "bytes": 400, + "allocs": 4 + } + ] + } + ] + }, + "benchmarkLibraries": [ + { + "repo": "cache", + "benchmarks": 5 + }, + { + "repo": "crypt", + "benchmarks": 3 + }, + { + "repo": "events", + "benchmarks": 7 + }, + { + "repo": "execx", + "benchmarks": 2 + }, + { + "repo": "mail", + "benchmarks": 3 + }, + { + "repo": "metrics", + "benchmarks": 10 + }, + { + "repo": "queue", + "benchmarks": 6 + }, + { + "repo": "storage", + "benchmarks": 1 + }, + { + "repo": "str", + "benchmarks": 10 + }, + { + "repo": "web", + "benchmarks": 36 + }, + { + "repo": "wire", + "benchmarks": 2 + } + ], + "sources": [ + { + "repo": "cache", + "path": "docs/bench/benchmarks_rows.json", + "revision": "5e5d0f26e54b2e014e86630caa2ab72e6c8b0fe1", + "sha256": "560cbd0e2847840efe83a7dc474c6d77bb41585d305ee5878a287497fa630507" + }, + { + "repo": "events", + "path": "docs/bench/benchmarks_rows.json", + "revision": "21c6e503c3e99f4b95efa47c603daf87433e5af9", + "sha256": "3ca9808d2e0b5ab2796854e6c7d99bfa509e721a06ed4cddde929a71799ee679" + }, + { + "repo": "queue", + "path": "docs/bench/benchmarks_rows.json", + "revision": "439da9bbc0436f3ab8251dde0b296a1c3cf8a954", + "sha256": "ed230e76699758a15b044aad71038511888af093ca89377ab1e61703afb1b883" + }, + { + "repo": "storage", + "path": "docs/bench/benchmarks_rows.json", + "revision": "1ba3e4ca4bbdf19c4203c02413468dc934430c55", + "sha256": "b324cc04e5cb4d6fd9d7518c6e9b4400ac0c8fdb2ce7a10696e20669bb8a4391" + }, + { + "repo": "web", + "path": "docs/bench/benchmarks_rows.json", + "revision": "b1c88179b24ef67ccc51521e3b731a6cce0190a5", + "sha256": "2f4567eaef1f4246c60ed735e112172abb9c16e231c931b05c0a3281747f1eea" + } + ] +} diff --git a/docs/.vitepress/data/proof-stats.json b/docs/.vitepress/data/proof-stats.json index 37e40bc..2bced92 100644 --- a/docs/.vitepress/data/proof-stats.json +++ b/docs/.vitepress/data/proof-stats.json @@ -1,5 +1,5 @@ { - "generatedAt": "2026-08-01", + "generatedAt": "2026-08-07", "totals": { "unitTests": 1461, "integrationTests": 952, @@ -77,7 +77,7 @@ "integration": null, "testFns": 78, "benchmarks": 0, - "sourceRevision": "f2029e5346cf9e28eb01802cbbb3f77efef61dc7" + "sourceRevision": "1d70e18042d7e493eb0143e2b840b5207616bfab" }, { "repo": "cache", @@ -85,7 +85,7 @@ "integration": 118, "testFns": 240, "benchmarks": 5, - "sourceRevision": "5ab5e9cbabd399576026d337f50b9df62c00aa8e" + "sourceRevision": "7d4f8e96a3a91d4e2fb9dde4341f724983d49356" }, { "repo": "collection", @@ -93,7 +93,7 @@ "integration": null, "testFns": 380, "benchmarks": 0, - "sourceRevision": "a9c56c7894e2acc2bf0ce105cdd938dedc55ca9e" + "sourceRevision": "71f045125475ebed177e1576b4192f4fe26da1d8" }, { "repo": "console", @@ -101,7 +101,7 @@ "integration": null, "testFns": 212, "benchmarks": 0, - "sourceRevision": "e1883f6dcd15fdb6b98c45d26ca482c365d0b7d7" + "sourceRevision": "67d253dd50fc18d5ba1df8b7d361e441356ebf46" }, { "repo": "crypt", @@ -109,7 +109,7 @@ "integration": null, "testFns": 50, "benchmarks": 3, - "sourceRevision": "6011c59bec66fa3562878dbabaed50f21b707754" + "sourceRevision": "0882f0b3d558747357ad1b211e66db951b0c79e7" }, { "repo": "env", @@ -117,7 +117,7 @@ "integration": null, "testFns": 98, "benchmarks": 0, - "sourceRevision": "a243636d13c720753db746164307ec1327618515" + "sourceRevision": "2221d15bdbc425a9bc073063091675ee22228e10" }, { "repo": "events", @@ -125,7 +125,7 @@ "integration": 86, "testFns": 119, "benchmarks": 7, - "sourceRevision": "93b83a2bf8aafdd525406e04104b0d77a4a83ca8" + "sourceRevision": "02da1f5d049a3c94136548c06477476483280256" }, { "repo": "execx", @@ -133,7 +133,7 @@ "integration": null, "testFns": 129, "benchmarks": 2, - "sourceRevision": "2de4f085fa92416d133c472ef6e0d53857e5a3ed" + "sourceRevision": "c33b8b87490d17e7728ee45a184a179eacbd2660" }, { "repo": "godump", @@ -141,7 +141,7 @@ "integration": null, "testFns": 105, "benchmarks": 0, - "sourceRevision": "ee749adbc2002d28df33d1f0ae5d5afce77fb9e9" + "sourceRevision": "5b1ba91ab94161e900a06be3bac56543ac0ff30b" }, { "repo": "httpx", @@ -149,7 +149,7 @@ "integration": null, "testFns": 124, "benchmarks": 0, - "sourceRevision": "e6e073db6d275b38830028ae9024164bb68ee92d" + "sourceRevision": "2dc960f86f64103d2f9cc8b999322dab75800939" }, { "repo": "mail", @@ -157,7 +157,7 @@ "integration": null, "testFns": 88, "benchmarks": 3, - "sourceRevision": "d18e4698017bb016cf747c40f94f568ac920ba63" + "sourceRevision": "ac8693c05396f707b7b23028f441db472af6238b" }, { "repo": "metrics", @@ -165,7 +165,7 @@ "integration": null, "testFns": 61, "benchmarks": 10, - "sourceRevision": "49e7417dc921b77a63852d341d128ea249b4c24c" + "sourceRevision": "1381f232b3f4e7d400b808df1e5f8189b87f72b5" }, { "repo": "queue", @@ -173,7 +173,7 @@ "integration": 618, "testFns": 673, "benchmarks": 6, - "sourceRevision": "2e01ce48b1d61ae62042044656d7659f3c16cf24" + "sourceRevision": "0af045fa7888dae3dd8071d3b8f6a6af2175ef05" }, { "repo": "scheduler", @@ -181,7 +181,7 @@ "integration": null, "testFns": 80, "benchmarks": 0, - "sourceRevision": "08e837e8ef074a9ba36a315f3e9e6a25757fa354" + "sourceRevision": "2da693de562396dd030316f3aea464c80ec09c93" }, { "repo": "storage", @@ -189,7 +189,7 @@ "integration": 130, "testFns": 276, "benchmarks": 1, - "sourceRevision": "709a6540b007816282fa88a27344da60d73f0096" + "sourceRevision": "b2c6a3601f7764148f3c33d154b691ca2a3a59f1" }, { "repo": "str", @@ -197,7 +197,7 @@ "integration": null, "testFns": 96, "benchmarks": 10, - "sourceRevision": "e8402b9136fce20c4fa8d973fe3ae731e9cf2bcc" + "sourceRevision": "13862d9890b1d61d0f3b19e397eee214a34e8942" }, { "repo": "web", @@ -205,7 +205,7 @@ "integration": null, "testFns": 460, "benchmarks": 36, - "sourceRevision": "8ae67c788fbf8c8ac3c0d0c6ca73d5df221b3b2e" + "sourceRevision": "b1c88179b24ef67ccc51521e3b731a6cce0190a5" }, { "repo": "wire", @@ -213,7 +213,7 @@ "integration": null, "testFns": 132, "benchmarks": 2, - "sourceRevision": "8c74c4da7c0f4a47ad815ea61dc387d1e3c6ee80" + "sourceRevision": "a810d81ac3678288c8bb8c25396f581ee9e57b26" } ] } diff --git a/docs/.vitepress/theme/components/PerformanceStory.vue b/docs/.vitepress/theme/components/PerformanceStory.vue new file mode 100644 index 0000000..d27d686 --- /dev/null +++ b/docs/.vitepress/theme/components/PerformanceStory.vue @@ -0,0 +1,741 @@ + + + + + diff --git a/docs/index.md b/docs/index.md index 72297a2..8a2be97 100644 --- a/docs/index.md +++ b/docs/index.md @@ -677,7 +677,7 @@ func (w *Welcome) Greet(ctx context.Context, user User) error { :style="i ? { '--reveal-delay': `${i * 0.08}s` } : undefined" >{{ fmt(stat.count) }}{{ stat.suffix }}{{ stat.label }} -

Driver suites run against Redis, Postgres, MySQL, NATS, Kafka, MinIO, SQS, and more through testcontainers and emulators. These numbers are generated from the repositories, not written by hand: see how they are counted →

+

Driver suites run against Redis, Postgres, MySQL, NATS, Kafka, MinIO, SQS, and more through testcontainers and emulators. These numbers are generated from the repositories, not written by hand. See the performance story → Read the counting methodology →

diff --git a/docs/package.json b/docs/package.json index 4cc4583..54c9fa8 100644 --- a/docs/package.json +++ b/docs/package.json @@ -3,6 +3,8 @@ "scripts": { "proof:refresh": "node ../bin/collect-proof-stats.mjs", "proof:check": "node ../bin/collect-proof-stats.mjs --check", + "performance:refresh": "node ../bin/collect-performance-stats.mjs", + "performance:check": "node ../bin/collect-performance-stats.mjs --check", "sync:design": "node .vitepress/scripts/sync-design-css.mjs", "audit:content": "node .vitepress/scripts/audit-content-value.mjs", "predev": "npm run sync:design", diff --git a/docs/performance.md b/docs/performance.md new file mode 100644 index 0000000..aca8252 --- /dev/null +++ b/docs/performance.md @@ -0,0 +1,13 @@ +--- +title: Performance +description: Source-backed GoForj library benchmarks across local and service-backed drivers. +sidebar: false +aside: false +noAutoTitle: true +--- + + + + From c0cf7e0df6516061f5335f0d28583373696cce2a Mon Sep 17 00:00:00 2001 From: Chris Miles Date: Fri, 7 Aug 2026 06:29:15 +0000 Subject: [PATCH 2/3] docs: sharpen performance positioning --- bin/collect-performance-stats.mjs | 9 +- docs/.vitepress/data/performance-stats.json | 16 +- .../theme/components/PerformanceStory.vue | 245 ++++++++++++++---- 3 files changed, 210 insertions(+), 60 deletions(-) diff --git a/bin/collect-performance-stats.mjs b/bin/collect-performance-stats.mjs index 64218cd..3d5e478 100644 --- a/bin/collect-performance-stats.mjs +++ b/bin/collect-performance-stats.mjs @@ -130,16 +130,15 @@ const webScenarios = ['live_plain_text', 'static_text', 'path_param_json', 'midd }) const findRow = (rows, label) => required(rows.find((row) => row.label === label), label) -const staticWeb = required(webScenarios.find((scenario) => scenario.id === 'static_text'), 'web static_text') const stats = { generatedAt: new Date().toISOString().slice(0, 10), totals: proof.totals, highlights: [ - { library: 'Cache', label: 'Memory read', value: findRow(cacheRows, 'Memory').value, unit: 'ns/op' }, - { library: 'Queue', label: 'Synchronous dispatch', value: findRow(queueRows, 'Sync').value, unit: 'ns/op' }, - { library: 'Events', label: 'Synchronous round trip', value: findRow(eventRows, 'Synchronous').value, unit: 'ns/op' }, - { library: 'Web', label: 'Static route', value: findRow(staticWeb.rows, 'GoForj Web').throughput, unit: 'ops/s' } + { library: 'Cache', label: 'Memory GetBytes', value: findRow(cacheRows, 'Memory').value, unit: 'ns/op' }, + { library: 'Queue', label: 'Synchronous Dispatch return', value: findRow(queueRows, 'Sync').value, unit: 'ns/op' }, + { library: 'Events', label: 'Publish + handler round trip', value: findRow(eventRows, 'Synchronous').value, unit: 'ns/op' }, + { library: 'Storage', label: 'Memory small-object Get', value: findRow(storageRows, 'Memory').value, unit: 'ns/op' } ], driverStories: [ { id: 'cache', title: 'Cache reads', operation: 'GetBytes', rows: cacheRows }, diff --git a/docs/.vitepress/data/performance-stats.json b/docs/.vitepress/data/performance-stats.json index 3b423a1..5ed07fe 100644 --- a/docs/.vitepress/data/performance-stats.json +++ b/docs/.vitepress/data/performance-stats.json @@ -11,27 +11,27 @@ "highlights": [ { "library": "Cache", - "label": "Memory read", + "label": "Memory GetBytes", "value": 70, "unit": "ns/op" }, { "library": "Queue", - "label": "Synchronous dispatch", + "label": "Synchronous Dispatch return", "value": 282.5, "unit": "ns/op" }, { "library": "Events", - "label": "Synchronous round trip", + "label": "Publish + handler round trip", "value": 371.8, "unit": "ns/op" }, { - "library": "Web", - "label": "Static route", - "value": 11047264, - "unit": "ops/s" + "library": "Storage", + "label": "Memory small-object Get", + "value": 752.9147821215927, + "unit": "ns/op" } ], "driverStories": [ @@ -689,7 +689,7 @@ { "repo": "web", "path": "docs/bench/benchmarks_rows.json", - "revision": "b1c88179b24ef67ccc51521e3b731a6cce0190a5", + "revision": "8ae67c788fbf8c8ac3c0d0c6ca73d5df221b3b2e", "sha256": "2f4567eaef1f4246c60ed735e112172abb9c16e231c931b05c0a3281747f1eea" } ] diff --git a/docs/.vitepress/theme/components/PerformanceStory.vue b/docs/.vitepress/theme/components/PerformanceStory.vue index d27d686..9193b0e 100644 --- a/docs/.vitepress/theme/components/PerformanceStory.vue +++ b/docs/.vitepress/theme/components/PerformanceStory.vue @@ -7,6 +7,42 @@ const selectedWebScenarioID = ref('path_param_json') const kindOrder = { process: 0, local: 1, service: 2 } const driverStory = computed(() => performance.driverStories.find((story) => story.id === selectedDriverStoryID.value)) +const driverNarratives = { + cache: { + local: 'CACHE_DRIVER=memory', + external: 'CACHE_DRIVER=redis', + localDriver: 'Memory', + externalDriver: 'Redis', + buys: 'Shared cache state across App instances.', + measurement: 'Seeded GetBytes. Lower is faster.' + }, + queue: { + local: 'QUEUE_DRIVER=workerpool', + external: 'QUEUE_DRIVER=rabbitmq', + localDriver: 'Worker pool', + externalDriver: 'RabbitMQ', + buys: 'Independent workers and broker-backed delivery.', + measurement: 'Producer-side Dispatch return. Job completion is not included.', + caveat: 'Core NATS is an ephemeral publish/subscribe adapter. Its Dispatch return does not provide the durability boundary represented by SQL, SQS, or durable brokers.' + }, + events: { + local: 'EVENTS_DRIVER=inproc', + external: 'EVENTS_DRIVER=nats', + localDriver: 'In process', + externalDriver: 'NATS', + buys: 'Event delivery across process boundaries.', + measurement: 'Publish plus observed handler delivery round trip.' + }, + storage: { + local: 'STORAGE_PUBLIC_DRIVER=local', + external: 'STORAGE_PUBLIC_DRIVER=s3', + localDriver: 'Local disk', + externalDriver: 'S3', + buys: 'Object storage shared beyond one host.', + measurement: 'Get a previously written small object.' + } +} +const driverNarrative = computed(() => driverNarratives[selectedDriverStoryID.value]) const driverRows = computed(() => [...driverStory.value.rows].sort((left, right) => { const kindDifference = kindOrder[left.kind] - kindOrder[right.kind] return kindDifference || left.value - right.value @@ -14,6 +50,9 @@ const driverRows = computed(() => [...driverStory.value.rows].sort((left, right) const webScenario = computed(() => performance.web.scenarios.find((scenario) => scenario.id === selectedWebScenarioID.value)) const webRows = computed(() => [...webScenario.value.rows].sort((left, right) => right.throughput - left.throughput)) const goforjWeb = computed(() => webScenario.value.rows.find((row) => row.framework === 'goforj_web')) +const liveHTTP = computed(() => performance.web.scenarios.find((scenario) => scenario.id === 'live_plain_text')) +const liveGoforjWeb = computed(() => liveHTTP.value.rows.find((row) => row.framework === 'goforj_web')) +const liveNetHTTP = computed(() => liveHTTP.value.rows.find((row) => row.framework === 'net_http')) const highlight = (library) => performance.highlights.find((item) => item.library === library) const driverWidth = (value) => { @@ -36,7 +75,7 @@ const formatRate = (value) => { return Math.round(value).toLocaleString() } const formatHighlight = (item) => item.unit === 'ops/s' ? formatRate(item.value) : formatLatency(item.value).replace(' ', '') -const kindLabel = (kind) => ({ process: 'In process', local: 'Local durable', service: 'Service-backed' })[kind] +const kindLabel = (kind) => ({ process: 'In process', local: 'Host-local', service: 'External boundary' })[kind] const scenarioLabel = (id) => ({ live_plain_text: 'HTTP loopback', static_text: 'Static response', @@ -50,16 +89,17 @@ const libraryLink = (repo) => `/${repo === 'str' ? 'strings' : repo}`
-

Measured performance

-

Fast enough to disappear.

+

Performance by architecture

+

Start local.
Scale without a rewrite.

- GoForj keeps local work close to your code. Cache reads, job dispatch, event delivery, - and HTTP routing stay in the nanosecond range until your architecture asks for a network, - durable storage, or another process. + GoForj gives cache, queues, events, and storage fast local implementations behind the + same contracts used by shared backends. Keep work in process while that fits. Move to + Redis, NATS, SQL, S3, or another service when you need coordination, persistence, or + distribution—without rewriting the service that uses it.

@@ -76,27 +116,29 @@ const libraryLink = (repo) => `/${repo === 'str' ? 'strings' : repo}` Events{{ formatLatency(highlight('Events').value) }}
- Web{{ formatRate(highlight('Web').value) }}/s + Storage{{ formatLatency(highlight('Storage').value) }}
-
{{ performance.totals.benchmarks }}benchmark functions
-
{{ performance.totals.libraries }}first-party libraries inspected
-
{{ performance.sources.length }}committed benchmark datasets
-
JSONsource-backed, checked at build time
+
{{ performance.sources.length }}versioned benchmark datasets
+
{{ performance.driverStories.length }}same-operation driver suites
+
1shared HTTP harness
+
SHArevision and content hash recorded
-

The local fast path

-

Your development loop should feel immediate.

+

The cheapest useful boundary

+

Local work stays local.

- Local drivers avoid a network hop without replacing the library contract. Start with an - in-process queue, memory cache, or synchronous event bus. Move to shared infrastructure - when the workload needs coordination or durability. + A memory cache read takes 70 ns. Synchronous queue dispatch returns in 283 ns. A + synchronous event publish plus handler round trip takes 372 ns. An in-memory small-object + read takes 753 ns. These are focused local measurements, not end-to-end App timings. Their + practical value is simple: your App can use real cache, queue, event, and storage contracts + before it needs a broker, database, or cloud service.

@@ -114,13 +156,14 @@ const libraryLink = (repo) => `/${repo === 'str' ? 'strings' : repo}`
-

One API. Different physics.

-

Choose the semantics. See the cost.

+

Same contract. Different boundary.

+

See the cost. Know what it buys.

- These logarithmic charts compare the same operation inside each library. The distance is - the story: an in-process driver is nearly free, while production-capable drivers pay for - transport, serialization, durability, or coordination. + Each chart compares one operation inside a GoForj library. Latency changes because the + work changes: serialization, filesystem access, broker delivery, database commits, + emulator behavior, and network round trips all have a cost. Choose the guarantees the + workload needs, then see the local overhead of that choice.

@@ -138,6 +181,25 @@ const libraryLink = (repo) => `/${repo === 'str' ? 'strings' : repo}` +
+
+ Start here + {{ driverNarrative.local }} + {{ driverNarrative.localDriver }} +
+
+ same library contract + + configuration + wiring +
+
+ Cross the boundary when needed + {{ driverNarrative.external }} + {{ driverNarrative.externalDriver }} +

{{ driverNarrative.buys }}

+
+
+
@@ -146,8 +208,8 @@ const libraryLink = (repo) => `/${repo === 'str' ? 'strings' : repo}`
In process - Local durable - Service-backed + Host-local + External boundary
@@ -167,23 +229,32 @@ const libraryLink = (repo) => `/${repo === 'str' ? 'strings' : repo}` {{ formatLatency(row.value) }}
-

Logarithmic latency scale · lower is faster

+
+

{{ driverNarrative.measurement }}

+

{{ driverNarrative.caveat }}

+ Logarithmic latency scale. Compare rows within this suite only. External fixtures run locally in containers or emulators; they are not managed-service forecasts. +
-

HTTP without the tax

-

Routing overhead stays out of your way.

+

Framework overhead

+

The App stays close to net/http.

- The shared net/http suite measures complete route and handler dispatch. On - the path-and-JSON case, GoForj Web clears {{ formatRate(goforjWeb.throughput) }} + In the shared HTTP/1.1 loopback case, GoForj Web recorded + {{ formatRate(liveGoforjWeb.throughput) }} requests per second versus + {{ formatRate(liveNetHTTP.throughput) }} for net/http. In the + in-process path-and-JSON case, it recorded {{ formatRate(goforjWeb.throughput) }} operations per second with {{ goforjWeb.allocs }} allocations per operation. + The useful result is not a benchmark trophy: GoForj's routing and App abstractions remain + competitive with the underlying Go stack under the same harness.

- Each value is the median of {{ performance.web.metadata.sample_count }} samples at + Values are medians of {{ performance.web.metadata.sample_count }} samples at {{ performance.web.metadata.benchmark_time }} with GOMAXPROCS={{ performance.web.metadata.gomaxprocs }}. - This is an in-process ceiling, not a production capacity forecast. + Small differences are not rankings. The loopback case is a warm single-connection ceiling; + the other cases measure in-process dispatch. Neither predicts production capacity.

@@ -221,16 +292,19 @@ const libraryLink = (repo) => `/${repo === 'str' ? 'strings' : repo}`
-

Read the receipts

-

The numbers are committed, not typed into this page.

+

Traceable measurements

+

Every number points back to a versioned result.

- Library benchmark suites emit JSON. The docs normalize those files into the charts above, - record their source revisions and hashes, and reject stale output during the docs build. + This page is generated from five benchmark snapshots committed in the Cache, Queue, + Events, Storage, and Web repositories. The collector records each source revision and + SHA-256 hash, normalizes the selected rows, and makes the docs build fail when the + checked-in page data drifts from those sources.

- Service-backed rows generally run against local containers or emulators. They are useful - for comparing driver overhead under controlled conditions. Real production latency also - includes your network, topology, service configuration, contention, and payloads. + Container- and emulator-backed results describe those local fixtures on the recorded + machine. Production latency depends on network topology, service configuration, + contention, payloads, and delivery guarantees. Use these results to compare implementation + boundaries and detect regressions, not to forecast deployed throughput.

@@ -249,7 +323,7 @@ const libraryLink = (repo) => `/${repo === 'str' ? 'strings' : repo}`
- Benchmark functions in source + {{ performance.totals.benchmarks }} benchmark functions across {{ performance.benchmarkLibraries.length }} libraries
-

Start local. Scale deliberately.

-

Keep the App shape.
Swap the physics.

+

Change the boundary, not the service

+

Keep the contract.
Add infrastructure deliberately.

- GoForj drivers let you optimize for a fast local loop today and choose shared, - durable infrastructure when the system actually needs it. + Run in process or on local storage while those semantics fit. Move to shared, durable, or + distributed drivers when coordination, persistence, and scale require them. GoForj keeps + that decision in wiring and configuration instead of spreading it through business logic.

@@ -636,6 +711,75 @@ const libraryLink = (repo) => `/${repo === 'str' ? 'strings' : repo}` color: var(--gf-ink); } +.gf-performance-boundary { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(170px, 0.38fr) minmax(0, 1fr); + gap: 18px; + align-items: stretch; + margin-bottom: 18px; +} + +.gf-performance-boundary > div:not(.gf-performance-boundary__bridge) { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 7px 18px; + padding: 20px 22px; + border: 1px solid rgba(166, 156, 176, 0.22); + border-radius: 10px; + background: rgba(255, 255, 255, 0.025); +} + +.gf-performance-boundary > div:last-child { + border-color: rgba(103, 199, 255, 0.3); + background: linear-gradient(135deg, rgba(103, 199, 255, 0.08), rgba(255, 255, 255, 0.02)); +} + +.gf-performance-boundary span { + color: var(--gf-ink-2); + font-size: 0.7rem; + font-weight: 800; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.gf-performance-boundary code { + justify-self: end; + padding: 0; + background: transparent; + color: var(--gf-reference); + font-size: 0.72rem; +} + +.gf-performance-boundary strong { + color: var(--gf-ink); + font-size: 1.16rem; +} + +.gf-performance-boundary p { + grid-column: 1 / -1; + margin: 2px 0 0; + color: var(--gf-ink-2); + font-size: 0.8rem; +} + +.gf-performance-boundary__bridge { + display: grid; + place-content: center; + color: var(--gf-ink-2); + text-align: center; +} + +.gf-performance-boundary__bridge i { + color: var(--gf-accent-hi); + font-size: 1.7rem; + font-style: normal; + line-height: 1; +} + +.gf-performance-boundary__bridge small { + font-size: 0.66rem; +} + .gf-performance-chart, .gf-performance-web__panel { padding: clamp(22px, 3.2vw, 42px); @@ -665,10 +809,14 @@ const libraryLink = (repo) => `/${repo === 'str' ? 'strings' : repo}` .gf-performance-driver-row__bar.is-local { background: linear-gradient(90deg, #7869d8, var(--perf-violet)); box-shadow: 0 0 24px rgba(183, 164, 255, 0.18); } .gf-performance-driver-row__bar.is-service { background: linear-gradient(90deg, #277da8, var(--perf-cyan)); box-shadow: 0 0 24px rgba(103, 199, 255, 0.18); } .gf-performance-driver-row__value { color: var(--gf-ink); font-family: var(--vp-font-family-mono); font-size: 0.78rem; text-align: right; } -.gf-performance-chart__scale { margin: 22px 0 0; color: var(--gf-ink-2); font-size: 0.7rem; text-align: right; } +.gf-performance-chart__notes { display: grid; gap: 7px; margin-top: 24px; padding-top: 20px; border-top: 1px solid rgba(166, 156, 176, 0.16); } +.gf-performance-chart__notes p { max-width: 920px; margin: 0; color: var(--gf-ink); font-size: 0.76rem; line-height: 1.55; } +.gf-performance-chart__notes p + p { color: var(--gf-ink-2); } +.gf-performance-chart__notes small { color: var(--gf-ink-2); font-size: 0.68rem; line-height: 1.55; } .gf-performance-web { display: grid; grid-template-columns: minmax(360px, 0.65fr) minmax(560px, 1.1fr); gap: clamp(44px, 7vw, 110px); align-items: center; } .gf-performance-web__copy code { color: var(--gf-ink); } +.gf-performance-web__copy h2 code { padding: 0; background: transparent; color: var(--gf-reference); font-size: 0.82em; } .gf-performance-web__note { font-size: 0.84rem !important; } .gf-performance-tabs--compact { margin: 0 0 30px; } .gf-performance-tabs--compact button { padding: 8px 12px; font-size: 0.74rem; } @@ -725,6 +873,9 @@ const libraryLink = (repo) => `/${repo === 'str' ? 'strings' : repo}` .gf-performance-highlights { grid-template-columns: 1fr; } .gf-performance-highlights article { min-height: 205px; } .gf-performance-drivers__intro { grid-template-columns: 1fr; gap: 8px; } + .gf-performance-boundary { grid-template-columns: 1fr; } + .gf-performance-boundary__bridge { min-height: 82px; } + .gf-performance-boundary__bridge i { transform: rotate(90deg); } .gf-performance-chart__topline { display: grid; align-items: start; } .gf-performance-driver-row { grid-template-columns: 90px minmax(100px, 1fr) 70px; gap: 9px; } .gf-performance-driver-row__label span { display: none; } From 0bbd9b7fe71772f0a0767b3ed9a6ed4965b2e375 Mon Sep 17 00:00:00 2001 From: Chris Miles Date: Fri, 7 Aug 2026 06:37:17 +0000 Subject: [PATCH 3/3] docs: compare local and production driver paths --- .../theme/components/PerformanceStory.vue | 342 +++++++++--------- 1 file changed, 166 insertions(+), 176 deletions(-) diff --git a/docs/.vitepress/theme/components/PerformanceStory.vue b/docs/.vitepress/theme/components/PerformanceStory.vue index 9193b0e..5a12ac0 100644 --- a/docs/.vitepress/theme/components/PerformanceStory.vue +++ b/docs/.vitepress/theme/components/PerformanceStory.vue @@ -48,12 +48,50 @@ const driverRows = computed(() => [...driverStory.value.rows].sort((left, right) return kindDifference || left.value - right.value })) const webScenario = computed(() => performance.web.scenarios.find((scenario) => scenario.id === selectedWebScenarioID.value)) -const webRows = computed(() => [...webScenario.value.rows].sort((left, right) => right.throughput - left.throughput)) +const webRows = computed(() => webScenario.value.rows + .filter((row) => row.framework !== 'httprouter') + .sort((left, right) => right.throughput - left.throughput)) const goforjWeb = computed(() => webScenario.value.rows.find((row) => row.framework === 'goforj_web')) const liveHTTP = computed(() => performance.web.scenarios.find((scenario) => scenario.id === 'live_plain_text')) const liveGoforjWeb = computed(() => liveHTTP.value.rows.find((row) => row.framework === 'goforj_web')) const liveNetHTTP = computed(() => liveHTTP.value.rows.find((row) => row.framework === 'net_http')) -const highlight = (library) => performance.highlights.find((item) => item.library === library) +const driverRow = (storyID, label) => performance.driverStories + .find((story) => story.id === storyID) + .rows.find((row) => row.label === label) +const performancePairs = [ + { + id: 'cache', + title: 'Cache', + operation: 'Seeded read', + local: driverRow('cache', 'Memory'), + production: driverRow('cache', 'Redis'), + buys: 'Shared cache state across App instances.' + }, + { + id: 'queue', + title: 'Queue', + operation: 'Dispatch return', + local: driverRow('queue', 'Worker pool'), + production: driverRow('queue', 'RabbitMQ'), + buys: 'Broker-backed delivery to independent workers.' + }, + { + id: 'events', + title: 'Events', + operation: 'Publish + handle', + local: driverRow('events', 'Synchronous'), + production: driverRow('events', 'NATS JetStream'), + buys: 'Durable event delivery across processes.' + }, + { + id: 'storage', + title: 'Storage', + operation: 'Small-object read', + local: driverRow('storage', 'Local'), + production: driverRow('storage', 'S3'), + buys: 'Object storage shared beyond one host.' + } +] const driverWidth = (value) => { const values = driverRows.value.map((row) => row.value) @@ -74,7 +112,6 @@ const formatRate = (value) => { if (value >= 1_000) return `${Math.round(value / 1_000)}K` return Math.round(value).toLocaleString() } -const formatHighlight = (item) => item.unit === 'ops/s' ? formatRate(item.value) : formatLatency(item.value).replace(' ', '') const kindLabel = (kind) => ({ process: 'In process', local: 'Host-local', service: 'External boundary' })[kind] const scenarioLabel = (id) => ({ live_plain_text: 'HTTP loopback', @@ -103,22 +140,29 @@ const libraryLink = (repo) => `/${repo === 'str' ? 'strings' : repo}` -
- -
- {{ highlight('Cache').label }} - {{ formatHighlight(highlight('Cache')).replace('ns', '') }}ns -
-
- Queue{{ formatLatency(highlight('Queue').value) }} +
+
+
Start localFast feedback
+ +
Cross the boundaryProduction capability
-
- Events{{ formatLatency(highlight('Events').value) }} -
-
- Storage{{ formatLatency(highlight('Storage').value) }} -
- +
+
+ {{ pair.title }} + {{ pair.operation }} +
+
+ {{ pair.local.label }} + {{ formatLatency(pair.local.value) }} +
+ +
+ {{ pair.production.label }} + {{ formatLatency(pair.production.value) }} +
+

{{ pair.buys }}

+
+ Measured locally. Each row compares the same library operation; lower is faster.
@@ -129,27 +173,46 @@ const libraryLink = (repo) => `/${repo === 'str' ? 'strings' : repo}`
SHArevision and content hash recorded
-
+
-

The cheapest useful boundary

-

Local work stays local.

+

Performance without a fork in the road

+

Your service code stays put.

- A memory cache read takes 70 ns. Synchronous queue dispatch returns in 283 ns. A - synchronous event publish plus handler round trip takes 372 ns. An in-memory small-object - read takes 753 ns. These are focused local measurements, not end-to-end App timings. Their - practical value is simple: your App can use real cache, queue, event, and storage contracts - before it needs a broker, database, or cloud service. + Local drivers are not toy substitutes. They implement the contracts your App uses in + production. Start with fewer processes and faster feedback, then change configuration and + wiring when a workload needs shared state, independent workers, durable delivery, or + storage beyond one host.

-
-
- -

{{ item.library }}

- {{ formatHighlight(item) }} - {{ item.label }} - {{ item.unit }} -
+
+
+ Application service + Business logic + cache.Get(ctx, key) + queue.Dispatch(ctx, job) + events.Publish(ctx, event) + storage.Get(ctx, path) + unchanged +
+
+ GoForj contracts + + Cache · Queue · Events · Storage + +
+
+
+ Local profile + Memory · Worker pool
Synchronous · Disk
+ one command, no supporting services +
+
+ Production profile + Redis · RabbitMQ
JetStream · S3
+ coordination, durability, distribution +
+
@@ -399,7 +462,7 @@ const libraryLink = (repo) => `/${repo === 'str' ? 'strings' : repo}` .gf-performance-hero { position: relative; display: grid; - grid-template-columns: minmax(0, 0.9fr) minmax(520px, 1.1fr); + grid-template-columns: minmax(0, 0.78fr) minmax(660px, 1.22fr); gap: clamp(30px, 6vw, 110px); align-items: center; min-height: calc(100vh - var(--vp-nav-height)); @@ -494,112 +557,53 @@ const libraryLink = (repo) => `/${repo === 'str' ? 'strings' : repo}` border-color: var(--perf-gold); } -.gf-performance-radar { - position: relative; - aspect-ratio: 1; - width: min(100%, 720px); - margin: auto; - border: 1px solid rgba(255, 130, 87, 0.17); - border-radius: 50%; - background: - radial-gradient(circle, rgba(255, 106, 61, 0.13) 0 1px, transparent 2px), - radial-gradient(circle, transparent 0 24%, var(--perf-faint) 24.2% 24.5%, transparent 24.8% 49%, var(--perf-faint) 49.2% 49.5%, transparent 49.8% 74%, var(--perf-faint) 74.2% 74.5%, transparent 74.8%), - var(--gf-code-well); - background-size: 18px 18px, 100% 100%, 100% 100%; - box-shadow: inset 0 0 100px rgba(255, 106, 61, 0.06), 0 40px 120px rgba(0, 0, 0, 0.22); -} - -.gf-performance-radar__grid { - position: absolute; - inset: 8%; - border: 1px solid var(--perf-faint); - border-radius: 50%; +.gf-performance-pairs { + padding: clamp(20px, 2.6vw, 34px); + border: 1px solid rgba(255, 130, 87, 0.22); + border-radius: 14px; + background: linear-gradient(145deg, rgba(255, 106, 61, 0.07), var(--perf-panel) 35%); + box-shadow: 0 40px 120px rgba(0, 0, 0, 0.25); } -.gf-performance-radar__grid::before, -.gf-performance-radar__grid::after { - position: absolute; - background: var(--perf-faint); - content: ""; -} - -.gf-performance-radar__grid::before { top: 50%; left: 0; width: 100%; height: 1px; } -.gf-performance-radar__grid::after { top: 0; left: 50%; width: 1px; height: 100%; } - -.gf-performance-radar__sweep { - position: absolute; - inset: 8%; - overflow: hidden; - border-radius: 50%; - animation: gf-performance-sweep 8s linear infinite; -} - -.gf-performance-radar__sweep::before { - position: absolute; - inset: 0 50% 50% 0; - background: conic-gradient(from 270deg at 100% 100%, transparent 0deg, rgba(255, 106, 61, 0.18) 52deg, transparent 54deg); - content: ""; - transform-origin: 100% 100%; -} - -.gf-performance-radar__core { - position: absolute; - z-index: 3; - top: 50%; - left: 50%; +.gf-performance-pairs__heading, +.gf-performance-pairs article { display: grid; - width: 38%; - aspect-ratio: 1; - place-content: center; - border: 1px solid rgba(255, 130, 87, 0.44); - border-radius: 50%; - background: radial-gradient(circle at 50% 38%, rgba(255, 106, 61, 0.28), var(--gf-code-well) 68%); - box-shadow: 0 0 80px rgba(255, 106, 61, 0.18), inset 0 1px rgba(255, 255, 255, 0.08); - text-align: center; - transform: translate(-50%, -50%); + grid-template-columns: minmax(72px, 0.45fr) minmax(126px, 0.8fr) minmax(74px, 0.48fr) minmax(126px, 0.8fr) minmax(160px, 1.2fr); + gap: 14px; + align-items: center; } -.gf-performance-radar__core span, -.gf-performance-radar__node span { - color: var(--gf-ink-2); - font-size: 0.72rem; - font-weight: 750; - letter-spacing: 0.08em; - text-transform: uppercase; +.gf-performance-pairs__heading { + padding: 0 14px 15px; + border-bottom: 1px solid rgba(166, 156, 176, 0.16); } -.gf-performance-radar__core strong { - color: var(--gf-ink); - font-size: clamp(2.8rem, 5vw, 5.8rem); - letter-spacing: -0.08em; - line-height: 0.95; -} +.gf-performance-pairs__heading > div:first-child { grid-column: 2; } +.gf-performance-pairs__heading > i { grid-column: 3; color: var(--gf-ink-2); font-size: 0.64rem; font-style: normal; text-align: center; } +.gf-performance-pairs__heading > div:last-child { grid-column: 4 / 6; } +.gf-performance-pairs__heading div { display: grid; gap: 2px; } +.gf-performance-pairs__heading span, +.gf-performance-pairs > small { color: var(--gf-ink-2); font-size: 0.65rem; font-weight: 800; letter-spacing: 0.08em; text-transform: uppercase; } +.gf-performance-pairs__heading strong { color: var(--gf-ink); font-size: 0.82rem; } -.gf-performance-radar__core small { - margin-left: 4px; - color: var(--perf-coral); - font-size: 0.34em; - letter-spacing: -0.03em; +.gf-performance-pairs article { + padding: 17px 14px; + border-bottom: 1px solid rgba(166, 156, 176, 0.13); } -.gf-performance-radar__node { - position: absolute; - z-index: 4; - display: grid; - gap: 2px; - min-width: 112px; - padding: 11px 14px; - border: 1px solid rgba(255, 255, 255, 0.12); - border-radius: 7px; - background: var(--gf-code-chrome); - box-shadow: 0 14px 42px rgba(0, 0, 0, 0.28); - backdrop-filter: blur(14px); -} - -.gf-performance-radar__node strong { color: var(--gf-ink); font-size: 1rem; } -.gf-performance-radar__node--queue { top: 16%; right: 4%; border-color: rgba(103, 199, 255, 0.38); } -.gf-performance-radar__node--events { right: 0; bottom: 22%; border-color: rgba(183, 164, 255, 0.42); } -.gf-performance-radar__node--web { bottom: 6%; left: 10%; border-color: rgba(115, 226, 196, 0.4); } +.gf-performance-pairs__label, +.gf-performance-pairs__driver { display: grid; gap: 3px; } +.gf-performance-pairs__label strong { color: var(--gf-ink); font-size: 0.88rem; } +.gf-performance-pairs__label span { color: var(--gf-ink-2); font-size: 0.65rem; } +.gf-performance-pairs__driver { padding: 11px 12px; border: 1px solid rgba(183, 164, 255, 0.3); border-radius: 7px; background: rgba(183, 164, 255, 0.07); } +.gf-performance-pairs__driver.is-production { border-color: rgba(103, 199, 255, 0.34); background: rgba(103, 199, 255, 0.07); } +.gf-performance-pairs__driver span { color: var(--gf-ink-2); font-size: 0.66rem; font-weight: 800; text-transform: uppercase; } +.gf-performance-pairs__driver strong { color: var(--gf-ink); font-family: var(--vp-font-family-mono); font-size: 1rem; } +.gf-performance-pairs__bridge { display: flex; align-items: center; color: var(--gf-accent-hi); } +.gf-performance-pairs__bridge span { flex: 1; height: 1px; background: linear-gradient(90deg, rgba(183, 164, 255, 0.5), rgba(103, 199, 255, 0.5)); } +.gf-performance-pairs__bridge i { font-style: normal; } +.gf-performance-pairs article > p { margin: 0; color: var(--gf-ink-2); font-size: 0.72rem; line-height: 1.45; } +.gf-performance-pairs > small { display: block; padding: 15px 14px 0; line-height: 1.5; text-transform: none; } .gf-performance-proof { display: grid; @@ -645,41 +649,25 @@ const libraryLink = (repo) => `/${repo === 'str' ? 'strings' : repo}` line-height: 1.72; } -.gf-performance-highlights { - display: grid; - grid-template-columns: repeat(4, minmax(0, 1fr)); - gap: 15px; - margin-top: 54px; -} - -.gf-performance-highlights article { - position: relative; - display: grid; - min-height: 250px; - overflow: hidden; - padding: clamp(22px, 2.8vw, 34px); - border: 1px solid rgba(166, 156, 176, 0.2); - border-radius: 10px; - background: linear-gradient(155deg, rgba(255, 255, 255, 0.065), rgba(255, 255, 255, 0.018)); -} - -.gf-performance-highlights__beam { - position: absolute; - right: -30%; - bottom: -50%; - width: 110%; - aspect-ratio: 1; - border-radius: 50%; - background: radial-gradient(circle, rgba(255, 106, 61, 0.18), transparent 68%); -} - -.gf-performance-highlights article:nth-child(2) .gf-performance-highlights__beam { background: radial-gradient(circle, rgba(103, 199, 255, 0.16), transparent 68%); } -.gf-performance-highlights article:nth-child(3) .gf-performance-highlights__beam { background: radial-gradient(circle, rgba(183, 164, 255, 0.17), transparent 68%); } -.gf-performance-highlights article:nth-child(4) .gf-performance-highlights__beam { background: radial-gradient(circle, rgba(115, 226, 196, 0.17), transparent 68%); } -.gf-performance-highlights p { position: relative; margin: 0; color: var(--gf-ink-2); font-size: 0.78rem; font-weight: 800; letter-spacing: 0.1em; text-transform: uppercase; } -.gf-performance-highlights strong { position: relative; align-self: end; color: var(--gf-ink); font-size: clamp(2.7rem, 4vw, 5rem); letter-spacing: -0.075em; line-height: 1; } -.gf-performance-highlights span { position: relative; margin-top: 12px; color: var(--gf-ink); font-weight: 750; } -.gf-performance-highlights small { position: relative; margin-top: 4px; color: var(--gf-ink-2); } +.gf-performance-contract { display: grid; grid-template-columns: minmax(320px, 0.6fr) minmax(660px, 1.4fr); gap: clamp(44px, 7vw, 110px); align-items: center; } +.gf-performance-contract h2 { font-size: clamp(2.8rem, 4.7vw, 5.7rem); } +.gf-performance-contract-map { display: grid; grid-template-columns: minmax(190px, 0.8fr) minmax(180px, 0.7fr) minmax(240px, 1fr); gap: 16px; align-items: stretch; } +.gf-performance-contract-map__service, +.gf-performance-contract-map__modes > div { display: grid; gap: 9px; padding: 24px; border: 1px solid rgba(166, 156, 176, 0.2); border-radius: 10px; background: var(--perf-panel); } +.gf-performance-contract-map__service { position: relative; align-content: center; border-color: rgba(255, 130, 87, 0.38); box-shadow: 0 24px 70px rgba(255, 106, 61, 0.08); } +.gf-performance-contract-map span { color: var(--gf-ink-2); font-size: 0.68rem; font-weight: 800; letter-spacing: 0.08em; text-transform: uppercase; } +.gf-performance-contract-map strong { color: var(--gf-ink); } +.gf-performance-contract-map__service > strong { margin-bottom: 8px; font-size: 1.25rem; } +.gf-performance-contract-map__service code { padding: 5px 7px; background: rgba(255, 255, 255, 0.04); color: var(--gf-reference); font-size: 0.7rem; white-space: nowrap; } +.gf-performance-contract-map__service small { position: absolute; top: 14px; right: 14px; padding: 3px 7px; border: 1px solid rgba(115, 226, 196, 0.3); border-radius: 999px; color: var(--perf-mint); font-size: 0.62rem; font-weight: 800; text-transform: uppercase; } +.gf-performance-contract-map__contracts { display: grid; place-content: center; gap: 11px; color: var(--gf-ink); text-align: center; } +.gf-performance-contract-map__contracts strong { font-size: 0.8rem; line-height: 1.7; } +.gf-performance-contract-map__contracts i { color: var(--gf-accent-hi); font-size: 1.6rem; font-style: normal; line-height: 1; } +.gf-performance-contract-map__modes { display: grid; gap: 12px; } +.gf-performance-contract-map__modes > div:first-child { border-color: rgba(183, 164, 255, 0.3); background: linear-gradient(135deg, rgba(183, 164, 255, 0.08), var(--perf-panel)); } +.gf-performance-contract-map__modes > div:last-child { border-color: rgba(103, 199, 255, 0.34); background: linear-gradient(135deg, rgba(103, 199, 255, 0.08), var(--perf-panel)); } +.gf-performance-contract-map__modes strong { font-size: 0.9rem; line-height: 1.55; } +.gf-performance-contract-map__modes small { color: var(--gf-ink-2); font-size: 0.68rem; } .gf-performance-drivers { background: rgba(255, 255, 255, 0.012); } .gf-performance-drivers__intro { display: grid; grid-template-columns: minmax(0, 1.15fr) minmax(360px, 0.65fr); gap: 48px; align-items: end; } @@ -851,14 +839,10 @@ const libraryLink = (repo) => `/${repo === 'str' ? 'strings' : repo}` .gf-performance-closing > p:not(.gf-performance-eyebrow) { margin-right: auto; margin-left: auto; } .gf-performance-actions--center { justify-content: center; } -@keyframes gf-performance-sweep { - to { transform: rotate(360deg); } -} - @media (max-width: 1100px) { .gf-performance-hero { grid-template-columns: 1fr; min-height: auto; } - .gf-performance-radar { width: min(100%, 620px); } - .gf-performance-highlights { grid-template-columns: repeat(2, minmax(0, 1fr)); } + .gf-performance-pairs { width: 100%; } + .gf-performance-contract { grid-template-columns: 1fr; } .gf-performance-web, .gf-performance-methodology { grid-template-columns: 1fr; } } @@ -866,12 +850,19 @@ const libraryLink = (repo) => `/${repo === 'str' ? 'strings' : repo}` @media (max-width: 760px) { .gf-performance-hero { padding-top: 56px; } .gf-performance-hero h1 { font-size: clamp(3.5rem, 17vw, 5.4rem); } - .gf-performance-radar__node { min-width: 96px; padding: 8px 10px; } + .gf-performance-pairs__heading { display: none; } + .gf-performance-pairs article { grid-template-columns: minmax(80px, 0.65fr) minmax(102px, 1fr) 28px minmax(102px, 1fr); gap: 8px; padding-right: 0; padding-left: 0; } + .gf-performance-pairs article > p { grid-column: 2 / -1; } + .gf-performance-pairs__driver { padding: 9px; } + .gf-performance-pairs__driver strong { font-size: 0.82rem; } + .gf-performance-pairs__bridge span { display: none; } + .gf-performance-pairs__bridge { justify-content: center; } .gf-performance-proof { grid-template-columns: repeat(2, minmax(0, 1fr)); } .gf-performance-proof > div:nth-child(2) { border-right: 0; } .gf-performance-proof > div:nth-child(-n + 2) { border-bottom: 1px solid rgba(166, 156, 176, 0.16); } - .gf-performance-highlights { grid-template-columns: 1fr; } - .gf-performance-highlights article { min-height: 205px; } + .gf-performance-contract-map { grid-template-columns: 1fr; } + .gf-performance-contract-map__contracts { min-height: 110px; } + .gf-performance-contract-map__contracts i { transform: rotate(90deg); } .gf-performance-drivers__intro { grid-template-columns: 1fr; gap: 8px; } .gf-performance-boundary { grid-template-columns: 1fr; } .gf-performance-boundary__bridge { min-height: 82px; } @@ -885,7 +876,6 @@ const libraryLink = (repo) => `/${repo === 'str' ? 'strings' : repo}` } @media (prefers-reduced-motion: reduce) { - .gf-performance-radar__sweep { animation: none; } .gf-performance-driver-row__bar, .gf-performance-web-row__track span { transition: none; } }