diff --git a/.github/workflows/browser-benchmarks.yml b/.github/workflows/browser-benchmarks.yml index 1413353..f4ede1b 100644 --- a/.github/workflows/browser-benchmarks.yml +++ b/.github/workflows/browser-benchmarks.yml @@ -42,7 +42,13 @@ jobs: with: node-version: 24 cache: 'npm' - - run: npm ci + - name: Install dependencies + run: | + if [ "${{ github.event_name }}" = "schedule" ]; then + npm update + else + npm ci + fi - name: Clear stale results from checkout run: rm -rf results/browser/ - name: Run browser benchmark @@ -75,7 +81,13 @@ jobs: with: node-version: 24 cache: 'npm' - - run: npm ci + - name: Install dependencies + run: | + if [ "${{ github.event_name }}" = "schedule" ]; then + npm update + else + npm ci + fi - name: Download all artifacts uses: actions/download-artifact@v4 with: @@ -83,6 +95,15 @@ jobs: pattern: browser-results-* - name: Merge results run: npx tsx src/merge-results.ts --input artifacts --mode browser + - run: npm run generate-browser-svg + - name: Upload SVG as artifact + if: github.event_name == 'pull_request' + uses: actions/upload-artifact@v4 + with: + name: browser-benchmark-svg + path: browser.svg + if-no-files-found: ignore + retention-days: 7 - name: Post results to PR if: github.event_name == 'pull_request' continue-on-error: true @@ -128,7 +149,7 @@ jobs: } } - body += `---\n*[View full run](${runUrl})*`; + body += `---\n*[View full run](${runUrl}) · SVG available as [build artifact](${runUrl}#artifacts)*`; const marker = '## Browser Benchmark Results'; const { data: comments } = await github.rest.issues.listComments({ @@ -159,7 +180,7 @@ jobs: run: | git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" - git add results/browser/ + git add package.json package-lock.json browser.svg results/browser/ git diff --cached --quiet && echo "No changes to commit" && exit 0 git commit -m "chore: update browser benchmark results [skip ci]" git push diff --git a/README.md b/README.md index b7be592..25350b7 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,14 @@ ![Pricing Comparison](./pricing.svg) +### [Browser Sessions](#browser-sessions) + +![Browser Sessions](./browser.svg) + +### [Object Storage](#object-storage) + +![Object Storage — 10MB](./storage_10mb.svg) + [![Benchmarks](https://github.com/computesdk/benchmarks/actions/workflows/benchmarks.yml/badge.svg)](https://github.com/computesdk/benchmarks/actions/workflows/benchmarks.yml) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](./LICENSE) diff --git a/package.json b/package.json index 43c1527..ab8b634 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "generate-svg:staggered": "tsx src/sandbox/generate-svg.ts --mode staggered", "generate-svg:burst": "tsx src/sandbox/generate-svg.ts --mode burst", "generate-storage-svg": "tsx src/storage/generate-svg.ts", + "generate-browser-svg": "tsx src/browser/generate-svg.ts", "generate-pricing-svg": "tsx src/sandbox/generate-pricing-svg.ts" }, "dependencies": { diff --git a/src/browser/generate-svg.ts b/src/browser/generate-svg.ts new file mode 100644 index 0000000..c274b3b --- /dev/null +++ b/src/browser/generate-svg.ts @@ -0,0 +1,248 @@ +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; +import type { BrowserBenchmarkResult } from './types.js'; +import { sortBrowserByCompositeScore, computeBrowserCompositeScores } from './scoring.js'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const ROOT = path.resolve(__dirname, '../..'); +const RESULTS_DIR = path.join(ROOT, 'results', 'browser'); +const SPONSORS_DIR_TIER1 = path.join(ROOT, 'sponsors', 'tier-1'); +const SPONSORS_DIR_TIER2 = path.join(ROOT, 'sponsors', 'tier-2'); + +function loadSponsorImages(): { dataUri: string; name: string }[] { + const allSponsors: { dataUri: string; name: string }[] = []; + + const mimeTypes: Record = { + '.png': 'image/png', + '.jpg': 'image/jpeg', + '.jpeg': 'image/jpeg', + '.svg': 'image/svg+xml', + }; + + const loadFromDir = (dir: string) => { + if (!fs.existsSync(dir)) return; + + const files = fs.readdirSync(dir) + .filter(f => /\.(png|jpe?g|svg)$/i.test(f)) + .sort(); + + for (const file of files) { + const ext = path.extname(file).toLowerCase(); + const mime = mimeTypes[ext] || 'image/png'; + const raw = fs.readFileSync(path.join(dir, file)); + const b64 = raw.toString('base64'); + const name = path.basename(file, ext); + allSponsors.push({ dataUri: `data:${mime};base64,${b64}`, name }); + } + }; + + loadFromDir(SPONSORS_DIR_TIER1); + loadFromDir(SPONSORS_DIR_TIER2); + + return allSponsors; +} + +const LOGO_C_PATH = `M1036.26,1002.28h237.87l-.93,19.09c-8.38,110.32-49.81,198.3-123.82,262.07-73.09,63.31-170.84,95.43-290.48,95.43-130.81,0-235.55-44.69-311.43-133.6-74.48-87.98-112.65-209.48-112.65-361.23v-60.51c0-96.83,17.7-183.41,51.68-257.43,34.91-74.95,85.19-133.61,149.89-173.63,64.7-40.04,140.12-60.52,225.3-60.52,117.77,0,214.13,32.12,286.29,95.9,72.62,63.3,114.98,153.61,126.15,267.67l1.86,19.08h-238.34l-.93-15.83c-4.65-59.11-20.95-101.94-47.95-127.08-27-25.6-69.83-38.17-127.08-38.17-61.91,0-107.06,20.95-137.33,65.17-31.65,45.15-47.94,117.77-48.87,215.53v74.48c0,102.41,15.36,177.83,45.62,223.91,28.86,44.22,74.01,65.63,137.79,65.63,58.19,0,101.48-12.57,128.95-38.17,26.99-25.14,43.29-66.1,47.48-121.5l.93-16.3Z`; + +interface ResultFile { + timestamp: string; + results: BrowserBenchmarkResult[]; +} + +function formatProviderName(s: string): string { + return s.charAt(0).toUpperCase() + s.slice(1); +} + +function formatSeconds(ms: number): string { + return (ms / 1000).toFixed(2) + 's'; +} + +const sponsorImages = loadSponsorImages(); + +function generateSVG(results: BrowserBenchmarkResult[], timestamp: string): string { + if (!results.every(r => r.compositeScore !== undefined)) { + computeBrowserCompositeScores(results); + } + + const sorted = sortBrowserByCompositeScore(results).filter(r => !r.skipped); + + const rowHeight = 44; + const headerHeight = 110; + const tableHeaderHeight = 44; + const padding = 24; + const width = 1280; + const tableTop = headerHeight + padding; + const tableBottom = tableTop + tableHeaderHeight + (sorted.length * rowHeight); + const footnoteHeight = 20; + + const height = tableBottom + padding + 30 + footnoteHeight; + + const cols = { + rank: 40, + provider: 80, + score: 240, + create: 340, + connect: 480, + navigate: 620, + release: 760, + total: 900, + status: 1060, + }; + + const title = 'Browser Session Benchmarks'; + const subtitle = 'Session creation, connection, navigation, and release latency'; + + let svg = ` + + + + + + + + + + + + + + + + + + + + + ${title} + ${subtitle} +${sponsorImages.length > 0 ? (() => { + const logoW = 100; + const logoH = 32; + const logoGap = 12; + const totalLogosW = sponsorImages.length * logoW + (sponsorImages.length - 1) * logoGap; + const logosStartX = width - padding - totalLogosW; + return ` + + SPONSORED BY + ${sponsorImages.map((img, i) => ``).join('\n ')}`; +})() + : ''} + + + + + # + Provider + Score + Create + Connect + Navigate + Release + Total + Status +`; + + sorted.forEach((r, i) => { + const y = tableTop + tableHeaderHeight + (i * rowHeight) + 30; + const ok = r.iterations.filter(it => !it.error).length; + const total = r.iterations.length; + const rank = i + 1; + const totalMs = r.summary.totalMs.median; + const allFailed = ok === 0; + const score = r.compositeScore !== undefined ? r.compositeScore.toFixed(1) : '--'; + + let speedClass = allFailed ? 'slow' : 'fast'; + if (!allFailed && totalMs > 7000) speedClass = 'slow'; + else if (!allFailed && totalMs > 3500) speedClass = 'medium'; + + let rankClass = 'rank'; + if (rank === 1) rankClass = 'rank rank-1'; + else if (rank === 2) rankClass = 'rank rank-2'; + else if (rank === 3) rankClass = 'rank rank-3'; + + const createDisplay = allFailed ? '--' : formatSeconds(r.summary.createMs.median); + const connectDisplay = allFailed ? '--' : formatSeconds(r.summary.connectMs.median); + const navigateDisplay = allFailed ? '--' : formatSeconds(r.summary.navigateMs.median); + const releaseDisplay = allFailed ? '--' : formatSeconds(r.summary.releaseMs.median); + const totalDisplay = allFailed ? '--' : formatSeconds(totalMs); + + svg += ` + + ${rank} + ${formatProviderName(r.provider)} + ${score} + ${createDisplay} + ${connectDisplay} + ${navigateDisplay} + ${releaseDisplay} + ${totalDisplay} + ${ok}/${total} +`; + + if (i < sorted.length - 1) { + const lineY = tableTop + tableHeaderHeight + ((i + 1) * rowHeight); + svg += ` +`; + } + }); + + const date = new Date(timestamp).toLocaleDateString('en-US', { + year: 'numeric', + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + timeZoneName: 'short' + }); + + svg += ` + + Last updated: ${date} + + + Measures full session lifecycle: create, connect via CDP, navigate to example.com, release. Lower total time is better. + +`; + + return svg; +} + +function main() { + const latestPath = path.join(RESULTS_DIR, 'latest.json'); + + if (!fs.existsSync(latestPath)) { + console.error(`No browser benchmark results found at ${latestPath}`); + process.exit(1); + } + + const raw = fs.readFileSync(latestPath, 'utf-8'); + const data: ResultFile = JSON.parse(raw); + + const svg = generateSVG(data.results, data.timestamp); + const svgPath = path.join(ROOT, 'browser.svg'); + fs.writeFileSync(svgPath, svg); + console.log(`SVG written to ${svgPath}`); +} + +main();