From 0ce4ffe388a5a3a76eaa4c7e3cb82af3148c60d7 Mon Sep 17 00:00:00 2001 From: Kiko Beats Date: Tue, 27 Oct 2026 19:15:38 +0100 Subject: [PATCH 1/3] ci: add benchmark workflow Cases: - sortHeaders chrome/firefox/safari: 60000 calls each on a fixed, realistic 16-17 header set (benchmark/fixtures/headers.json), asserting the output key order - getBrowser top 100 user agents: 50000 passes over a fixed UA list, asserting 77 chrome / 4 firefox / 19 safari per pass - tls options top 100 user agents: 20000 passes of tls(ua), asserting every result carries ciphers and signature algorithms Removes benchmark/sort-headers.mjs (fetched vercel.com at runtime and compared two inline copies of sortHeaders) and benchmark/package.json, which only existed to install tinybench for it. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/benchmark.yml | 52 ++++++++++ .gitignore | 6 ++ benchmark/fixtures/headers.json | 57 +++++++++++ benchmark/fixtures/user-agents.json | 102 +++++++++++++++++++ benchmark/index.js | 150 ++++++++++++++++++++++++++++ benchmark/package.json | 8 -- benchmark/sort-headers.mjs | 101 ------------------- package.json | 1 + 8 files changed, 368 insertions(+), 109 deletions(-) create mode 100644 .github/workflows/benchmark.yml create mode 100644 benchmark/fixtures/headers.json create mode 100644 benchmark/fixtures/user-agents.json create mode 100644 benchmark/index.js delete mode 100644 benchmark/package.json delete mode 100644 benchmark/sort-headers.mjs diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml new file mode 100644 index 0000000..017729e --- /dev/null +++ b/.github/workflows/benchmark.yml @@ -0,0 +1,52 @@ +name: benchmark + +on: + push: + branches: + - master + pull_request: + +permissions: + actions: write + contents: write + pull-requests: write + +jobs: + benchmark: + name: Benchmark + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v7 + - name: Setup Node.js + uses: actions/setup-node@v7 + with: + node-version: lts/* + - name: Restore previous benchmark + uses: actions/cache/restore@v6 + with: + path: cache/benchmark-data.json + key: benchmark-${{ github.run_id }}-${{ github.run_attempt }} + restore-keys: benchmark- + - name: Run benchmark + run: node benchmark/index.js + - name: Continuous Benchmark + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository + uses: benchmark-action/github-action-benchmark@v1.22.2 + with: + name: https-tls + tool: customSmallerIsBetter + output-file-path: benchmark-output.json + external-data-json-path: cache/benchmark-data.json + github-token: ${{ secrets.GITHUB_TOKEN }} + auto-push: false + comment-always: true + comment-on-alert: false + fail-on-alert: false + summary-always: false + - name: Save benchmark + if: github.ref == 'refs/heads/master' && github.event_name == 'push' + uses: actions/cache/save@v6 + with: + path: cache/benchmark-data.json + key: benchmark-${{ github.run_id }}-${{ github.run_attempt }} diff --git a/.gitignore b/.gitignore index e1d102d..29cf063 100755 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,9 @@ coverage ############################ .env .envrc + +############################ +# Benchmark +############################ +benchmark-output.json +cache/ diff --git a/benchmark/fixtures/headers.json b/benchmark/fixtures/headers.json new file mode 100644 index 0000000..cd5fdf0 --- /dev/null +++ b/benchmark/fixtures/headers.json @@ -0,0 +1,57 @@ +{ + "chrome": { + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", + "Accept-Encoding": "gzip, deflate, br, zstd", + "Accept-Language": "en-US,en;q=0.9", + "Cache-Control": "max-age=0", + "Connection": "keep-alive", + "Cookie": "session=4f6c1e2a9b; theme=dark", + "Host": "example.com", + "Sec-Fetch-Dest": "document", + "Sec-Fetch-Mode": "navigate", + "Sec-Fetch-Site": "none", + "Sec-Fetch-User": "?1", + "Upgrade-Insecure-Requests": "1", + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/155.0.0.0 Safari/537.36", + "X-Request-Id": "0b5e7f3c-2d1a-4c8e-9f60-7a3b1c2d4e5f", + "sec-ch-ua": "\"Google Chrome\";v=\"155\", \"Chromium\";v=\"155\", \"Not(A:Brand\";v=\"24\"", + "sec-ch-ua-mobile": "?0", + "sec-ch-ua-platform": "\"Windows\"" + }, + "firefox": { + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", + "Accept-Encoding": "gzip, deflate, br, zstd", + "Accept-Language": "en-US,en;q=0.5", + "Connection": "keep-alive", + "Cookie": "session=4f6c1e2a9b; theme=dark", + "Host": "example.com", + "Priority": "u=0, i", + "Referer": "https://example.com/", + "Sec-Fetch-Dest": "document", + "Sec-Fetch-Mode": "navigate", + "Sec-Fetch-Site": "same-origin", + "Sec-Fetch-User": "?1", + "Upgrade-Insecure-Requests": "1", + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:158.0) Gecko/20100101 Firefox/158.0", + "X-Request-Id": "0b5e7f3c-2d1a-4c8e-9f60-7a3b1c2d4e5f", + "te": "trailers" + }, + "safari": { + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", + "Accept-Encoding": "gzip, deflate, br", + "Accept-Language": "en-US,en;q=0.9", + "Connection": "keep-alive", + "Content-Length": "27", + "Content-Type": "application/x-www-form-urlencoded", + "Cookie": "session=4f6c1e2a9b; theme=dark", + "Host": "example.com", + "Origin": "https://example.com", + "Referer": "https://example.com/login", + "Sec-Fetch-Dest": "document", + "Sec-Fetch-Mode": "navigate", + "Sec-Fetch-Site": "same-origin", + "Upgrade-Insecure-Requests": "1", + "User-Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 18_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.6.1 Mobile/15E148 Safari/604.1", + "X-Request-Id": "0b5e7f3c-2d1a-4c8e-9f60-7a3b1c2d4e5f" + } +} diff --git a/benchmark/fixtures/user-agents.json b/benchmark/fixtures/user-agents.json new file mode 100644 index 0000000..7d7f056 --- /dev/null +++ b/benchmark/fixtures/user-agents.json @@ -0,0 +1,102 @@ +[ + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/155.0.0.0 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/155.0.0.0 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/155.0.0.0 Safari/537.36 Edg/155.0.0.0", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.0.0 Safari/537.36", + "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Mobile Safari/537.36", + "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/155.0.0.0 Mobile Safari/537.36", + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.0.0 Safari/537.36", + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/155.0.0.0 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:158.0) Gecko/20100101 Firefox/158.0", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/154.0.0.0 Safari/537.36", + "Mozilla/5.0 (iPhone; CPU iPhone OS 18_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.6.1 Mobile/15E148 Safari/604.1", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.0.0 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/154.0.0.0 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.0.0 Safari/537.36 Edg/156.0.0.0", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.0.0 Safari/537.36 OPR/137.0.0.0", + "Mozilla/5.0 (iPhone; CPU iPhone OS 18_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/27.0.1 Mobile/15E148 Safari/604.1", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.6.2 Safari/605.1.15", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/27.0.1 Safari/605.1.15", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.0.0 Safari/537.36", + "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/537.36 (KHTML, like Gecko) Chrome/152.0.0.0 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/151.0.0.0 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.7827.55 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36", + "Mozilla/5.0 (iPhone; CPU iPhone OS 18_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.0.0 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.0.0 Safari/537.36 OPR/136.0.0.0", + "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/154.0.0.0 Mobile Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:158.0) Gecko/20100101 Firefox/158.0", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36", + "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.0.0 Mobile Safari/537.36", + "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/30.0 Chrome/143.0.0.0 Mobile Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.0.0 Safari/537.36", + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/154.0.0.0 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.0.0 Safari/537.36 OPR/137.0.0.0", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/151.0.0.0 Safari/537.36", + "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 Edg/124.0.0.0", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/27.0 Safari/605.1.15", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 YaBrowser/26.8.0.0 Safari/537.36", + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.0.0 Safari/537.36 OPR/136.0.0.0", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36", + "Mozilla/5.0 (X11; CrOS x86_64 14541.0.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/154.0.0.0 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/151.0.7922.34 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.6 Safari/605.1.15", + "Mozilla/5.0 (iPhone; CPU iPhone OS 18_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.6 Mobile/15E148 Safari/604.1", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.6.1 Safari/605.1.15", + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/124 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.5.2 Safari/605.1.15", + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/116.0.0.0 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.51 Safari/537.36", + "Mozilla/5.0 (X11; Linux x86_64; rv:158.0) Gecko/20100101 Firefox/158.0", + "Mozilla/5.0 (iPhone; CPU iPhone OS 18_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.5.2 Mobile/15E148 Safari/604.1", + "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) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/154.0.0.0 Safari/537.36 Edg/154.0.0.0", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36", + "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/151.0.0.0 Mobile Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/155.0.0.0 Safari/537.36 Edg/155.0.0.0", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.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/26.5 Safari/605.1.15", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36", + "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/155.0.0.0 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36", + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.0.0 Safari/537.36", + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36", + "Mozilla/5.0 (iPhone; CPU iPhone OS 18_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/27.0 Mobile/15E148 Safari/604.1", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36", + "Mozilla/5.0 (iPhone; CPU iPhone OS 18_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.5 Mobile/15E148 Safari/604.1", + "Mozilla/5.0 (iPhone; CPU iPhone OS 18_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.6.2 Mobile/15E148 Safari/604.1", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.120 OBS/32.2.2 Safari/537.36", + "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.0.0 Mobile Safari/537.36", + "Mozilla/5.0 (iPhone; CPU iPhone OS 27_0_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/155.0.8059.37 Mobile/15E148 Safari/604.1", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36", + "Mozilla/5.0 (iPhone; CPU iPhone OS 26_6_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/155.0.8059.37 Mobile/15E148 Safari/604.1", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/151.0.0.0 Safari/537.36 FlexCDN", + "Mozilla/5.0 (iPhone; CPU iPhone OS 18_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.7.5 Mobile/15E148 Safari/604.1", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36", + "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/151.0.0.0 Safari/537.36", + "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.0.0 Mobile Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:157.0) Gecko/20100101 Firefox/157.0" +] diff --git a/benchmark/index.js b/benchmark/index.js new file mode 100644 index 0000000..dae720b --- /dev/null +++ b/benchmark/index.js @@ -0,0 +1,150 @@ +'use strict' + +const { writeFileSync } = require('fs') +const { join } = require('path') + +const tls = require('..') +const HEADERS_ORDER = require('../src/headers-order.json') +const HEADER_SETS = require('./fixtures/headers.json') +const USER_AGENTS = require('./fixtures/user-agents.json') + +const { sortHeaders, getBrowser } = tls + +const RUNS = 7 +const SORT_CALLS = 60000 +const BROWSER_PASSES = 50000 +const TLS_PASSES = 20000 + +const EXPECTED_BROWSERS = { chrome: 77, firefox: 4, safari: 19 } +const CHROME_MIN_VERSION = 'TLSv1' + +const median = values => { + const sorted = values.slice().sort((a, b) => a - b) + return sorted[Math.floor(sorted.length / 2)] +} + +const now = () => process.hrtime.bigint() + +const elapsed = started => Number(now() - started) / 1e6 + +const expectedOrder = (browser, headers) => { + const order = HEADERS_ORDER[browser] + const known = [...new Set(order)].filter(key => key in headers) + const rest = Object.keys(headers).filter(key => !order.includes(key)) + return known.concat(rest).join('\n') +} + +const sortCase = browser => { + const headers = HEADER_SETS[browser] + const expected = expectedOrder(browser, headers) + return { + name: `sortHeaders ${browser}`, + extra: `${SORT_CALLS} calls on ${Object.keys(headers).length} headers`, + run: () => { + let sorted + for (let call = 0; call < SORT_CALLS; call++) sorted = sortHeaders(headers) + return Object.keys(sorted).join('\n') === expected + } + } +} + +const browserCase = { + name: 'getBrowser top 100 user agents', + extra: `${BROWSER_PASSES} passes x ${USER_AGENTS.length} user agents`, + run: () => { + const counts = { chrome: 0, firefox: 0, safari: 0 } + for (let pass = 0; pass < BROWSER_PASSES; pass++) { + for (const userAgent of USER_AGENTS) counts[getBrowser(userAgent)]++ + } + return Object.keys(EXPECTED_BROWSERS).every( + browser => counts[browser] === EXPECTED_BROWSERS[browser] * BROWSER_PASSES + ) + } +} + +const tlsCase = { + name: 'tls options top 100 user agents', + extra: `${TLS_PASSES} passes x ${USER_AGENTS.length} user agents`, + run: () => { + let configured = 0 + let chrome = 0 + for (let pass = 0; pass < TLS_PASSES; pass++) { + for (const userAgent of USER_AGENTS) { + const https = tls(userAgent) + if (typeof https.ciphers === 'string' && https.signatureAlgorithms) { + configured++ + } + if (https.minVersion === CHROME_MIN_VERSION) chrome++ + } + } + return ( + configured === USER_AGENTS.length * TLS_PASSES && + chrome === EXPECTED_BROWSERS.chrome * TLS_PASSES + ) + } +} + +const CASES = [ + sortCase('chrome'), + sortCase('firefox'), + sortCase('safari'), + browserCase, + tlsCase +] + +const fmt = value => value.toFixed(1) + +const report = slot => { + const med = slot.walls.length ? median(slot.walls) : Number.NaN + const min = slot.walls.length ? Math.min(...slot.walls) : Number.NaN + const max = slot.walls.length ? Math.max(...slot.walls) : Number.NaN + const extra = `${slot.entry.extra}, errors ${slot.errors}, runs ${RUNS}` + console.log( + `${slot.entry.name} median ${fmt(med)}ms range ${fmt(min)}-${fmt(max)} ${extra}` + ) + return { + name: slot.entry.name, + unit: 'ms', + value: Number(med.toFixed(1)), + range: `${fmt(min)}-${fmt(max)}`, + extra + } +} + +const takeSample = (slot, run) => { + try { + const started = now() + const ok = slot.entry.run() + const wall = elapsed(started) + if (!ok) { + console.error(`${slot.entry.name} run ${run} produced unexpected output`) + slot.errors++ + return + } + slot.walls.push(wall) + } catch (error) { + console.error(error) + slot.errors++ + } +} + +const main = () => { + const slots = CASES.map(entry => { + entry.run() + return { entry, walls: [], errors: 0 } + }) + for (let run = 0; run < RUNS; run++) { + for (const slot of slots) takeSample(slot, run) + } + const results = slots.map(report) + writeFileSync( + join(__dirname, '..', 'benchmark-output.json'), + JSON.stringify(results, null, 2) + '\n' + ) + const errors = slots.reduce((total, slot) => total + slot.errors, 0) + if (errors > 0 || results.some(result => !Number.isFinite(result.value))) { + process.exitCode = 1 + } +} + +main() diff --git a/benchmark/package.json b/benchmark/package.json deleted file mode 100644 index 412f398..0000000 --- a/benchmark/package.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "@https-tls/benchmark", - "version": "1.0.0", - "devDependencies": { - "tinybench": "latest" - }, - "private": true -} diff --git a/benchmark/sort-headers.mjs b/benchmark/sort-headers.mjs deleted file mode 100644 index 6d62038..0000000 --- a/benchmark/sort-headers.mjs +++ /dev/null @@ -1,101 +0,0 @@ -'use strict' - -import { Bench } from 'tinybench' -import { readFileSync } from 'fs' - -import { getHeader } from '../src/headers.js' - -const getBrowser = () => 'chrome' - -const HEADERS_ORDER = JSON.parse( - readFileSync('../src/headers-order.json', 'utf8') -) - -const headers = await fetch('https://vercel.com').then(res => - Object.fromEntries(res.headers) -) - -const implementations = { - 'v0 (for loop)': headers => { - const userAgent = getHeader('user-agent', headers) - const browser = getBrowser(userAgent) - const order = HEADERS_ORDER[browser] || [] - const orderedHeaders = {} - - // First, add ordered headers - for (let i = 0; i < order.length; i++) { - const key = order[i] - if (key in headers) orderedHeaders[key] = headers[key] - } - - // Then, add remaining headers - const headerKeys = Object.keys(headers) - for (let i = 0; i < headerKeys.length; i++) { - const key = headerKeys[i] - if (!order.includes(key)) orderedHeaders[key] = headers[key] - } - - return orderedHeaders - }, - 'v1 (set)': headers => { - const userAgent = getHeader('user-agent', headers) - const browser = getBrowser(userAgent) - const order = HEADERS_ORDER[browser] || [] - const orderedHeaders = {} - - // Pre-calculate order as a Set for O(1) lookup - const orderSet = new Set(order) - const headerKeys = Object.keys(headers) - const headersLength = headerKeys.length - const orderLength = order.length - let i = 0 - let key - - // Add ordered headers - for (; i < orderLength; i++) { - key = order[i] - if (key in headers) orderedHeaders[key] = headers[key] - } - - // Add remaining headers - for (i = 0; i < headersLength; i++) { - key = headerKeys[i] - if (!orderSet.has(key)) orderedHeaders[key] = headers[key] - } - - return orderedHeaders - } -} - -const cases = Object.entries(implementations) - -// Verify all implementations produce the same result -const reference = JSON.stringify( - implementations[Object.keys(implementations)[0]](headers) -) -cases.slice(1).forEach(([name, implementation]) => { - const result = JSON.stringify(implementation(headers)) - if (result !== reference) { - throw new Error(`Different output for ${name}: ${result}`) - } -}) - -const bench = new Bench({ time: 1000 }) - -for (const [name, implementation] of cases) { - bench.add(name, () => implementation(headers)) -} - -await bench.run() - -const results = bench.tasks.map(task => ({ - name: task.name, - value: task.result.latency.mean -})) - -const fastest = results.reduce((prev, current) => - prev.value < current.value ? prev : current -) - -console.table(bench.table()) -console.log(`Fastest implementation: ${fastest.name}`) diff --git a/package.json b/package.json index 058e1dd..4c9658a 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,7 @@ "src" ], "scripts": { + "benchmark": "node benchmark/index.js", "clean": "rm -rf node_modules", "contributors": "(npx git-authors-cli && npx finepack && git add package.json && git commit -m 'build: contributors' --no-verify) || true", "coverage": "nyc report --reporter=text-lcov | coveralls", From 841d8843ff80d40eafb29a8ba694bde6d1116fb3 Mon Sep 17 00:00:00 2001 From: Kiko Beats Date: Tue, 27 Oct 2026 19:30:25 +0100 Subject: [PATCH 2/3] ci: narrow benchmark workflow token Drop actions: write (actions/cache needs no token scope) and stop persisting the checkout credential, so dependency install scripts can't read it. contents: write stays for the commit comment on master pushes, and pull-requests: write for the PR review comment. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/benchmark.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index 017729e..51f15e9 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -7,7 +7,6 @@ on: pull_request: permissions: - actions: write contents: write pull-requests: write @@ -18,6 +17,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@v7 + with: + persist-credentials: false - name: Setup Node.js uses: actions/setup-node@v7 with: From 6e4bd16d43309f6d970604f78fadbdfc239b5f88 Mon Sep 17 00:00:00 2001 From: Kiko Beats Date: Tue, 27 Oct 2026 19:53:33 +0100 Subject: [PATCH 3/3] test: import ava default export ava 8 no longer returns the test function from require('ava'). Co-Authored-By: Claude Opus 5.5 --- test/browser.js | 2 +- test/headers.js | 2 +- test/hook.js | 2 +- test/index.js | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/test/browser.js b/test/browser.js index 9f83695..621bdae 100644 --- a/test/browser.js +++ b/test/browser.js @@ -1,6 +1,6 @@ 'use strict' -const test = require('ava') +const test = require('ava').default const { getBrowser } = require('..') diff --git a/test/headers.js b/test/headers.js index 769cc51..33fe34c 100644 --- a/test/headers.js +++ b/test/headers.js @@ -1,6 +1,6 @@ 'use strict' -const test = require('ava') +const test = require('ava').default const { sortHeaders, getHeader } = require('..') diff --git a/test/hook.js b/test/hook.js index 75e75ce..92ff940 100644 --- a/test/hook.js +++ b/test/hook.js @@ -2,7 +2,7 @@ const uniqueRandomArray = require('unique-random-array') const userAgents = require('top-user-agents') -const test = require('ava') +const test = require('ava').default const decorate = require('../hook') diff --git a/test/index.js b/test/index.js index e7c9478..1faea30 100644 --- a/test/index.js +++ b/test/index.js @@ -2,7 +2,7 @@ const uniqueRandomArray = require('unique-random-array') const userAgents = require('top-user-agents') -const test = require('ava') +const test = require('ava').default const tls = require('..')