diff --git a/.github/workflows/browser-benchmarks.yml b/.github/workflows/browser-benchmarks.yml new file mode 100644 index 0000000..9932f4a --- /dev/null +++ b/.github/workflows/browser-benchmarks.yml @@ -0,0 +1,152 @@ +name: Browser Benchmark + +on: + pull_request: + paths: + - 'src/browser/**' + - 'src/util/**' + - 'src/run.ts' + - 'src/merge-results.ts' + - 'package.json' + workflow_dispatch: + inputs: + iterations: + description: 'Iterations per provider' + required: false + default: '10' + +concurrency: + group: browser-benchmarks + cancel-in-progress: true + +permissions: + contents: read + pull-requests: write + +jobs: + bench: + name: Bench ${{ matrix.provider }} + runs-on: namespace-profile-default + timeout-minutes: 60 + strategy: + fail-fast: false + matrix: + provider: + - browserbase + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 24 + cache: 'npm' + - run: npm ci + - name: Clear stale results from checkout + run: rm -rf results/browser/ + - name: Run browser benchmark + env: + BROWSERBASE_API_KEY: ${{ secrets.BROWSERBASE_API_KEY }} + BROWSERBASE_PROJECT_ID: ${{ secrets.BROWSERBASE_PROJECT_ID }} + run: | + npm run bench -- \ + --mode browser \ + --provider ${{ matrix.provider }} \ + --iterations ${{ github.event.inputs.iterations || '10' }} + - name: Upload results + if: always() + uses: actions/upload-artifact@v4 + with: + name: browser-results-${{ matrix.provider }} + path: results/browser/ + if-no-files-found: ignore + retention-days: 7 + + collect: + name: Collect Results + runs-on: namespace-profile-default + needs: bench + if: always() + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 24 + cache: 'npm' + - run: npm ci + - name: Download all artifacts + uses: actions/download-artifact@v4 + with: + path: artifacts/ + pattern: browser-results-* + - name: Merge results + run: npx tsx src/merge-results.ts --input artifacts --mode browser + - name: Post results to PR + if: github.event_name == 'pull_request' + continue-on-error: true + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + const path = require('path'); + + const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; + const latestPath = path.join('results', 'browser', 'latest.json'); + + let body = '## Browser Benchmark Results\n\n'; + + if (!fs.existsSync(latestPath)) { + body += '> No browser benchmark results were generated.\n\n'; + } else { + const data = JSON.parse(fs.readFileSync(latestPath, 'utf-8')); + const results = data.results + .filter(r => !r.skipped) + .sort((a, b) => (b.compositeScore || 0) - (a.compositeScore || 0)); + + if (results.length === 0) { + body += '> No browser benchmark results were generated.\n\n'; + } else { + body += '| # | Provider | Score | Create | Connect | Navigate | Release | Total | Status |\n'; + body += '|---|----------|-------|--------|---------|----------|---------|-------|--------|\n'; + + results.forEach((r, i) => { + const name = r.provider.charAt(0).toUpperCase() + r.provider.slice(1); + const score = r.compositeScore !== undefined ? r.compositeScore.toFixed(1) : '--'; + const create = (r.summary.createMs.median / 1000).toFixed(2) + 's'; + const connect = (r.summary.connectMs.median / 1000).toFixed(2) + 's'; + const navigate = (r.summary.navigateMs.median / 1000).toFixed(2) + 's'; + const release = (r.summary.releaseMs.median / 1000).toFixed(2) + 's'; + const total = (r.summary.totalMs.median / 1000).toFixed(2) + 's'; + const ok = r.iterations.filter(it => !it.error).length; + const count = r.iterations.length; + body += `| ${i + 1} | ${name} | ${score} | ${create} | ${connect} | ${navigate} | ${release} | ${total} | ${ok}/${count} |\n`; + }); + + body += '\n'; + } + } + + body += `---\n*[View full run](${runUrl})*`; + + const marker = '## Browser Benchmark Results'; + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + }); + + const existing = comments.find(c => c.body.startsWith(marker)); + + if (existing) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existing.id, + body, + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body, + }); + } diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ece003f..744d0e5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -75,10 +75,10 @@ cp env.example .env ### Running Tests Locally ```bash -# Run all three test modes (sequential → staggered → burst) +# Run all three sandbox test modes (sequential → staggered → burst) npm run bench -# Run individual test modes +# Run individual sandbox test modes npm run bench -- --mode sequential --iterations 10 npm run bench -- --mode staggered --concurrency 10 --stagger-delay 200 npm run bench -- --mode burst --concurrency 10 @@ -88,6 +88,15 @@ npm run bench -- --provider e2b # Combine flags npm run bench -- --provider e2b --mode sequential --iterations 5 + +# Run browser benchmarks +npm run bench -- --mode browser +npm run bench -- --mode browser --provider browserbase + +# Run storage benchmarks +npm run bench -- --mode storage +npm run bench -- --mode storage --provider aws-s3 +npm run bench -- --mode storage --file-size 100MB ``` ### Code Style diff --git a/package-lock.json b/package-lock.json index 90f1319..819ed4d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,27 +9,29 @@ "version": "1.0.0", "dependencies": { "@computesdk/blaxel": "^1.6.7", - "@computesdk/cloudflare": "^1.6.0", - "@computesdk/codesandbox": "^1.5.40", - "@computesdk/daytona": "^1.7.14", - "@computesdk/e2b": "^1.7.34", - "@computesdk/hopx": "^0.2.10", - "@computesdk/just-bash": "^0.3.0", - "@computesdk/modal": "^1.8.29", - "@computesdk/namespace": "^1.5.0", - "@computesdk/r2": "^1.0.0", - "@computesdk/runloop": "^1.3.36", - "@computesdk/s3": "^1.0.0", - "@computesdk/sprites": "^0.1.1", - "@computesdk/tigris": "^1.0.0", - "@computesdk/vercel": "^1.7.13", - "computesdk": "^2.2.1", - "dotenv": "^17.2.1" + "@computesdk/browserbase": "^0.3.0", + "@computesdk/cloudflare": "^1.6.4", + "@computesdk/codesandbox": "^1.5.42", + "@computesdk/daytona": "^1.7.22", + "@computesdk/e2b": "^1.7.42", + "@computesdk/hopx": "^0.2.18", + "@computesdk/just-bash": "^0.4.6", + "@computesdk/modal": "^1.8.37", + "@computesdk/namespace": "^1.6.4", + "@computesdk/r2": "^1.1.1", + "@computesdk/runloop": "^1.3.42", + "@computesdk/s3": "^1.1.1", + "@computesdk/sprites": "^0.1.5", + "@computesdk/tigris": "^1.1.1", + "@computesdk/vercel": "^1.7.21", + "computesdk": "^2.5.4", + "dotenv": "^17.4.0", + "playwright-core": "^1.59.1" }, "devDependencies": { - "@types/node": "^20.0.0", - "tsx": "^4.0.0", - "typescript": "^5.0.0" + "@types/node": "^25.5.0", + "tsx": "^4.21.0", + "typescript": "^6.0.2" } }, "node_modules/@alcalzone/ansi-tokenize": { @@ -248,9 +250,9 @@ } }, "node_modules/@aws-sdk/client-s3": { - "version": "3.1021.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.1021.0.tgz", - "integrity": "sha512-BCfggq8gYSjlKOZlMSVApix3cgKAQIWGeoJFX/AU5HMvqz1BZBEw83jJFL9LYrqTPCocH8NGl++1Xr70ro+jcg==", + "version": "3.1022.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.1022.0.tgz", + "integrity": "sha512-PhdIW0LxjzcMlBiCldRefnyZk84wtYGnEV0sNGOD55DZTvZsibG2XHvQiL1aFliKugfAhuIpNmFkctI2n2I3Dg==", "license": "Apache-2.0", "peer": true, "dependencies": { @@ -510,9 +512,9 @@ } }, "node_modules/@aws-sdk/lib-storage": { - "version": "3.1021.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/lib-storage/-/lib-storage-3.1021.0.tgz", - "integrity": "sha512-DmbaWPd4HSbz/gLS6tFZ6hTjDw3OvgheOqEywT/Z8sxdaSMIkEJjt2CcYKy7YiqG3DrkvZ/te5oGjgIQOUlhnw==", + "version": "3.1022.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/lib-storage/-/lib-storage-3.1022.0.tgz", + "integrity": "sha512-RozYW8R0O+Ld+qLYtIgOSt9iD31rC+G1vGmPkZN9aGf9FqGCwt0Fumsaa8CNLbzcI/O6PyL4g4OJgsCpmSTX4Q==", "license": "Apache-2.0", "dependencies": { "@smithy/middleware-endpoint": "^4.4.28", @@ -528,7 +530,7 @@ "node": ">=20.0.0" }, "peerDependencies": { - "@aws-sdk/client-s3": "^3.1021.0" + "@aws-sdk/client-s3": "^3.1022.0" } }, "node_modules/@aws-sdk/middleware-bucket-endpoint": { @@ -772,9 +774,9 @@ } }, "node_modules/@aws-sdk/s3-request-presigner": { - "version": "3.1021.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/s3-request-presigner/-/s3-request-presigner-3.1021.0.tgz", - "integrity": "sha512-kkIzsIAc7wnG7vVRkZFIwJ3noOyF3S6ozOQ9t2KxzPde1LsmpmPwYbmiB91DzdfuGySdk4Hpb0JmHh4KhGECXQ==", + "version": "3.1022.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/s3-request-presigner/-/s3-request-presigner-3.1022.0.tgz", + "integrity": "sha512-2arKiJswYGEOScAhEeOuy/1A1wScfgbfmU/6NAn0UK0/LDxqsOTc4/bCEuUK+/LtB+Lxu3rODqbY8V5gTGPLaQ==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/signature-v4-multi-region": "^3.996.15", @@ -999,6 +1001,35 @@ "url": "https://github.com/sponsors/Borewit" } }, + "node_modules/@browserbasehq/sdk": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@browserbasehq/sdk/-/sdk-2.9.0.tgz", + "integrity": "sha512-Xzm1+6suzQypXjley4Phqer++pjnYyST6S7CArUn3kWyGA8aruXjAV5wkmqE21lgXo9K3/OQJvCu48bKEZFNDQ==", + "dependencies": { + "@types/node": "^18.11.18", + "@types/node-fetch": "^2.6.4", + "abort-controller": "^3.0.0", + "agentkeepalive": "^4.2.1", + "form-data-encoder": "1.7.2", + "formdata-node": "^4.3.2", + "node-fetch": "^2.6.7" + } + }, + "node_modules/@browserbasehq/sdk/node_modules/@types/node": { + "version": "18.19.130", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz", + "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==", + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@browserbasehq/sdk/node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "license": "MIT" + }, "node_modules/@bufbuild/protobuf": { "version": "2.11.0", "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.11.0.tgz", @@ -1183,6 +1214,31 @@ "computesdk": "2.5.4" } }, + "node_modules/@computesdk/browserbase": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@computesdk/browserbase/-/browserbase-0.3.0.tgz", + "integrity": "sha512-zsZgbn1CQNrFmGRA/Iyxu1xYf4sL5p60neAmR3cdzUrZndxz2WEzWfIQmxFnNazcdxA0mOkdvRNlt352xsG3Nw==", + "license": "MIT", + "dependencies": { + "@browserbasehq/sdk": "^2.0.0", + "@computesdk/provider": "1.1.0", + "computesdk": "2.5.4", + "dotenv": "^16.0.0", + "playwright-core": "^1.40.0" + } + }, + "node_modules/@computesdk/browserbase/node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/@computesdk/cloudflare": { "version": "1.6.4", "resolved": "https://registry.npmjs.org/@computesdk/cloudflare/-/cloudflare-1.6.4.tgz", @@ -1248,37 +1304,18 @@ } }, "node_modules/@computesdk/just-bash": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@computesdk/just-bash/-/just-bash-0.3.0.tgz", - "integrity": "sha512-7Kjzg/Nxge3uU7xUKePddElroBEVi8om4Rqs5DHDrk1Vzz4+EzNPDUCgpGLdB8Qi0f2hB21lzUw7u5NQO4VLVg==", + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/@computesdk/just-bash/-/just-bash-0.4.6.tgz", + "integrity": "sha512-OfACkKbOOQkuutL8qIKpVYQQ6eECHxiSjbK3WB4NUis7YCQ7gaxNLHJJ8c+cI9L0wvL5kPuK53+QpnfSVYhXhQ==", "license": "MIT", "dependencies": { - "@computesdk/provider": "1.0.28", - "computesdk": "2.3.0" + "@computesdk/provider": "1.1.0", + "computesdk": "2.5.4" }, "peerDependencies": { "just-bash": ">=2.0.0" } }, - "node_modules/@computesdk/just-bash/node_modules/@computesdk/provider": { - "version": "1.0.28", - "resolved": "https://registry.npmjs.org/@computesdk/provider/-/provider-1.0.28.tgz", - "integrity": "sha512-ecn/AFiLpr5GCU0r6VkwBrwYnFULIoJ2K1oiJ7TBqj0pAYmQsFgv8i5b4VRbKab1FGG5zCXthaFY/3liTYjzCg==", - "license": "MIT", - "dependencies": { - "@computesdk/cmd": "0.4.1", - "computesdk": "2.3.0" - } - }, - "node_modules/@computesdk/just-bash/node_modules/computesdk": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/computesdk/-/computesdk-2.3.0.tgz", - "integrity": "sha512-4B7CRN2qB6XkuAnN7dZ0aMYqHaFrh2qdSuh02lM+cgMEQ7wZy9v44FAjBGfWebHXuPNA/nZRx7211U6CEiGdTw==", - "license": "MIT", - "dependencies": { - "@computesdk/cmd": "0.4.1" - } - }, "node_modules/@computesdk/modal": { "version": "1.8.37", "resolved": "https://registry.npmjs.org/@computesdk/modal/-/modal-1.8.37.tgz", @@ -1445,9 +1482,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz", - "integrity": "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==", + "version": "0.27.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.5.tgz", + "integrity": "sha512-nGsF/4C7uzUj+Nj/4J+Zt0bYQ6bz33Phz8Lb2N80Mti1HjGclTJdXZ+9APC4kLvONbjxN1zfvYNd8FEcbBK/MQ==", "cpu": [ "ppc64" ], @@ -1462,9 +1499,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.4.tgz", - "integrity": "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==", + "version": "0.27.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.5.tgz", + "integrity": "sha512-Cv781jd0Rfj/paoNrul1/r4G0HLvuFKYh7C9uHZ2Pl8YXstzvCyyeWENTFR9qFnRzNMCjXmsulZuvosDg10Mog==", "cpu": [ "arm" ], @@ -1479,9 +1516,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.4.tgz", - "integrity": "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==", + "version": "0.27.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.5.tgz", + "integrity": "sha512-Oeghq+XFgh1pUGd1YKs4DDoxzxkoUkvko+T/IVKwlghKLvvjbGFB3ek8VEDBmNvqhwuL0CQS3cExdzpmUyIrgA==", "cpu": [ "arm64" ], @@ -1496,9 +1533,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.4.tgz", - "integrity": "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==", + "version": "0.27.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.5.tgz", + "integrity": "sha512-nQD7lspbzerlmtNOxYMFAGmhxgzn8Z7m9jgFkh6kpkjsAhZee1w8tJW3ZlW+N9iRePz0oPUDrYrXidCPSImD0Q==", "cpu": [ "x64" ], @@ -1513,9 +1550,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.4.tgz", - "integrity": "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==", + "version": "0.27.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.5.tgz", + "integrity": "sha512-I+Ya/MgC6rr8oRWGRDF3BXDfP8K1BVUggHqN6VI2lUZLdDi1IM1v2cy0e3lCPbP+pVcK3Tv8cgUhHse1kaNZZw==", "cpu": [ "arm64" ], @@ -1530,9 +1567,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.4.tgz", - "integrity": "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==", + "version": "0.27.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.5.tgz", + "integrity": "sha512-MCjQUtC8wWJn/pIPM7vQaO69BFgwPD1jriEdqwTCKzWjGgkMbcg+M5HzrOhPhuYe1AJjXlHmD142KQf+jnYj8A==", "cpu": [ "x64" ], @@ -1547,9 +1584,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.4.tgz", - "integrity": "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==", + "version": "0.27.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.5.tgz", + "integrity": "sha512-X6xVS+goSH0UelYXnuf4GHLwpOdc8rgK/zai+dKzBMnncw7BTQIwquOodE7EKvY2UVUetSqyAfyZC1D+oqLQtg==", "cpu": [ "arm64" ], @@ -1564,9 +1601,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.4.tgz", - "integrity": "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==", + "version": "0.27.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.5.tgz", + "integrity": "sha512-233X1FGo3a8x1ekLB6XT69LfZ83vqz+9z3TSEQCTYfMNY880A97nr81KbPcAMl9rmOFp11wO0dP+eB18KU/Ucg==", "cpu": [ "x64" ], @@ -1581,9 +1618,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.4.tgz", - "integrity": "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==", + "version": "0.27.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.5.tgz", + "integrity": "sha512-0wkVrYHG4sdCCN/bcwQ7yYMXACkaHc3UFeaEOwSVW6e5RycMageYAFv+JS2bKLwHyeKVUvtoVH+5/RHq0fgeFw==", "cpu": [ "arm" ], @@ -1598,9 +1635,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.4.tgz", - "integrity": "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==", + "version": "0.27.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.5.tgz", + "integrity": "sha512-euKkilsNOv7x/M1NKsx5znyprbpsRFIzTV6lWziqJch7yWYayfLtZzDxDTl+LSQDJYAjd9TVb/Kt5UKIrj2e4A==", "cpu": [ "arm64" ], @@ -1615,9 +1652,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.4.tgz", - "integrity": "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==", + "version": "0.27.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.5.tgz", + "integrity": "sha512-hVRQX4+P3MS36NxOy24v/Cdsimy/5HYePw+tmPqnNN1fxV0bPrFWR6TMqwXPwoTM2VzbkA+4lbHWUKDd5ZDA/w==", "cpu": [ "ia32" ], @@ -1632,9 +1669,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.4.tgz", - "integrity": "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==", + "version": "0.27.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.5.tgz", + "integrity": "sha512-mKqqRuOPALI8nDzhOBmIS0INvZOOFGGg5n1osGIXAx8oersceEbKd4t1ACNTHM3sJBXGFAlEgqM+svzjPot+ZQ==", "cpu": [ "loong64" ], @@ -1649,9 +1686,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.4.tgz", - "integrity": "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==", + "version": "0.27.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.5.tgz", + "integrity": "sha512-EE/QXH9IyaAj1qeuIV5+/GZkBTipgGO782Ff7Um3vPS9cvLhJJeATy4Ggxikz2inZ46KByamMn6GqtqyVjhenA==", "cpu": [ "mips64el" ], @@ -1666,9 +1703,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.4.tgz", - "integrity": "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==", + "version": "0.27.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.5.tgz", + "integrity": "sha512-0V2iF1RGxBf1b7/BjurA5jfkl7PtySjom1r6xOK2q9KWw/XCpAdtB6KNMO+9xx69yYfSCRR9FE0TyKfHA2eQMw==", "cpu": [ "ppc64" ], @@ -1683,9 +1720,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.4.tgz", - "integrity": "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==", + "version": "0.27.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.5.tgz", + "integrity": "sha512-rYxThBx6G9HN6tFNuvB/vykeLi4VDsm5hE5pVwzqbAjZEARQrWu3noZSfbEnPZ/CRXP3271GyFk/49up2W190g==", "cpu": [ "riscv64" ], @@ -1700,9 +1737,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.4.tgz", - "integrity": "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==", + "version": "0.27.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.5.tgz", + "integrity": "sha512-uEP2q/4qgd8goEUc4QIdU/1P2NmEtZ/zX5u3OpLlCGhJIuBIv0s0wr7TB2nBrd3/A5XIdEkkS5ZLF0ULuvaaYQ==", "cpu": [ "s390x" ], @@ -1717,9 +1754,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.4.tgz", - "integrity": "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==", + "version": "0.27.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.5.tgz", + "integrity": "sha512-+Gq47Wqq6PLOOZuBzVSII2//9yyHNKZLuwfzCemqexqOQCSz0zy0O26kIzyp9EMNMK+nZ0tFHBZrCeVUuMs/ew==", "cpu": [ "x64" ], @@ -1734,9 +1771,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.4.tgz", - "integrity": "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==", + "version": "0.27.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.5.tgz", + "integrity": "sha512-3F/5EG8VHfN/I+W5cO1/SV2H9Q/5r7vcHabMnBqhHK2lTWOh3F8vixNzo8lqxrlmBtZVFpW8pmITHnq54+Tq4g==", "cpu": [ "arm64" ], @@ -1751,9 +1788,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.4.tgz", - "integrity": "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==", + "version": "0.27.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.5.tgz", + "integrity": "sha512-28t+Sj3CPN8vkMOlZotOmDgilQwVvxWZl7b8rxpn73Tt/gCnvrHxQUMng4uu3itdFvrtba/1nHejvxqz8xgEMA==", "cpu": [ "x64" ], @@ -1768,9 +1805,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.4.tgz", - "integrity": "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==", + "version": "0.27.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.5.tgz", + "integrity": "sha512-Doz/hKtiuVAi9hMsBMpwBANhIZc8l238U2Onko3t2xUp8xtM0ZKdDYHMnm/qPFVthY8KtxkXaocwmMh6VolzMA==", "cpu": [ "arm64" ], @@ -1785,9 +1822,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.4.tgz", - "integrity": "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==", + "version": "0.27.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.5.tgz", + "integrity": "sha512-WfGVaa1oz5A7+ZFPkERIbIhKT4olvGl1tyzTRaB5yoZRLqC0KwaO95FeZtOdQj/oKkjW57KcVF944m62/0GYtA==", "cpu": [ "x64" ], @@ -1802,9 +1839,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.4.tgz", - "integrity": "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==", + "version": "0.27.5", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.5.tgz", + "integrity": "sha512-Xh+VRuh6OMh3uJ0JkCjI57l+DVe7VRGBYymen8rFPnTVgATBwA6nmToxM2OwTlSvrnWpPKkrQUj93+K9huYC6A==", "cpu": [ "arm64" ], @@ -1819,9 +1856,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.4.tgz", - "integrity": "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==", + "version": "0.27.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.5.tgz", + "integrity": "sha512-aC1gpJkkaUADHuAdQfuVTnqVUTLqqUNhAvEwHwVWcnVVZvNlDPGA0UveZsfXJJ9T6k9Po4eHi3c02gbdwO3g6w==", "cpu": [ "x64" ], @@ -1836,9 +1873,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.4.tgz", - "integrity": "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==", + "version": "0.27.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.5.tgz", + "integrity": "sha512-0UNx2aavV0fk6UpZcwXFLztA2r/k9jTUa7OW7SAea1VYUhkug99MW1uZeXEnPn5+cHOd0n8myQay6TlFnBR07w==", "cpu": [ "arm64" ], @@ -1853,9 +1890,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.4.tgz", - "integrity": "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==", + "version": "0.27.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.5.tgz", + "integrity": "sha512-5nlJ3AeJWCTSzR7AEqVjT/faWyqKU86kCi1lLmxVqmNR+j4HrYdns+eTGjS/vmrzCIe8inGQckUadvS0+JkKdQ==", "cpu": [ "ia32" ], @@ -1870,9 +1907,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.4.tgz", - "integrity": "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==", + "version": "0.27.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.5.tgz", + "integrity": "sha512-PWypQR+d4FLfkhBIV+/kHsUELAnMpx1bRvvsn3p+/sAERbnCzFrtDRG2Xw5n+2zPxBK2+iaP+vetsRl4Ti7WgA==", "cpu": [ "x64" ], @@ -1966,15 +2003,15 @@ } }, "node_modules/@hey-api/openapi-ts": { - "version": "0.94.5", - "resolved": "https://registry.npmjs.org/@hey-api/openapi-ts/-/openapi-ts-0.94.5.tgz", - "integrity": "sha512-fCR/kIexbDarnt/WGKvjJb4K30JaFzO2F/528kHpyWT7vopPS0JeqtRQMjJg+Gk09N/05nbv1OaFOQXcy0BiVQ==", + "version": "0.95.0", + "resolved": "https://registry.npmjs.org/@hey-api/openapi-ts/-/openapi-ts-0.95.0.tgz", + "integrity": "sha512-lk5C+WKl5yqEmliQihEyhX/jNcWlAykTSEqkDeKa9xSq5YDAzOFvx7oos8YTqiIzdc4TemtlEaB8Rns7+8A0qg==", "license": "MIT", "peer": true, "dependencies": { "@hey-api/codegen-core": "0.7.4", "@hey-api/json-schema-ref-parser": "1.3.1", - "@hey-api/shared": "0.2.6", + "@hey-api/shared": "0.3.0", "@hey-api/spec-types": "0.1.0", "@hey-api/types": "0.1.4", "ansi-colors": "4.1.3", @@ -1996,9 +2033,9 @@ } }, "node_modules/@hey-api/shared": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/@hey-api/shared/-/shared-0.2.6.tgz", - "integrity": "sha512-ZZrsWbazJcJO688tJVEBeei03B4miPI7OauW+qLMYP/9KL6NadmA5MjqsIIwgfvb0HKMAR7lt4AINKzv0Zwdgw==", + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@hey-api/shared/-/shared-0.3.0.tgz", + "integrity": "sha512-G+4GPojdLEh9bUwRG88teMPM1HdqMm/IsJ38cbnNxhyDu1FkFGwilkA1EqnULCzfTam/ZoZkaLdmAd8xEh4Xsw==", "license": "MIT", "dependencies": { "@hey-api/codegen-core": "0.7.4", @@ -5283,9 +5320,9 @@ "license": "BSD-3-Clause" }, "node_modules/@runloop/api-client": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@runloop/api-client/-/api-client-1.14.1.tgz", - "integrity": "sha512-DYqNuj/zHTF3IohkSS/ZaDeIdtyUSarTiB+uWDKqTSWNyljEg0RcDzki0s6HTM1AUxS0RL/PkrXRYh95ADtZrw==", + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/@runloop/api-client/-/api-client-1.16.2.tgz", + "integrity": "sha512-BAa0FOpD1xwdritKYlFr6Bi9JCA2f51Y4WBLQa0fTVq7F09Jh9vyULfxFBTh1lML2F20zo8RO+27ndy/12TmWA==", "license": "MIT", "dependencies": { "@types/node": "^18.11.18", @@ -6411,12 +6448,12 @@ } }, "node_modules/@types/node": { - "version": "20.19.37", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.37.tgz", - "integrity": "sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw==", + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz", + "integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==", "license": "MIT", "dependencies": { - "undici-types": "~6.21.0" + "undici-types": "~7.18.0" } }, "node_modules/@types/node-fetch": { @@ -6478,12 +6515,13 @@ } }, "node_modules/@vercel/sandbox": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@vercel/sandbox/-/sandbox-1.9.0.tgz", - "integrity": "sha512-zgr1ad0tkT1xZn/8Vxo60wOUOLqMAVGo4WqJQ8/UDcUtWynNJsBjI2tiMdWZrAo9EKH1MIqEzJNkcclF0UT1EQ==", + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@vercel/sandbox/-/sandbox-1.9.1.tgz", + "integrity": "sha512-Y9nZk19GPXiKihLqa0bACbjBo2R0G1/0NHthn9e6kqI/ypK6z3liAsQ+RncjXIDDiBun6gJYOw73V8JADuUO8g==", "license": "Apache-2.0", "dependencies": { "@vercel/oidc": "3.2.0", + "@workflow/serde": "4.1.0-beta.2", "async-retry": "1.3.3", "jsonlines": "0.1.1", "ms": "2.1.3", @@ -6503,6 +6541,12 @@ "url": "https://github.com/sponsors/colinhacks" } }, + "node_modules/@workflow/serde": { + "version": "4.1.0-beta.2", + "resolved": "https://registry.npmjs.org/@workflow/serde/-/serde-4.1.0-beta.2.tgz", + "integrity": "sha512-8kkeoQKLDaKXefjV5dbhBj2aErfKp1Mc4pb6tj8144cF+Em5SPbyMbyLCHp+BVrFfFVCBluCtMx+jjvaFVZGww==", + "license": "Apache-2.0" + }, "node_modules/@xterm/addon-serialize": { "version": "0.13.0", "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.13.0.tgz", @@ -6954,6 +6998,12 @@ "node": ">=0.8.0" } }, + "node_modules/blessed-contrib/node_modules/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "license": "MIT" + }, "node_modules/blessed-contrib/node_modules/supports-color": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", @@ -7876,9 +7926,9 @@ } }, "node_modules/dotenv": { - "version": "17.3.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.3.1.tgz", - "integrity": "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==", + "version": "17.4.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.0.tgz", + "integrity": "sha512-kCKF62fwtzwYm0IGBNjRUjtJgMfGapII+FslMHIjMR5KTnwEmBmWLDRSnc3XSNP8bNy34tekgQyDT0hr7pERRQ==", "license": "BSD-2-Clause", "engines": { "node": ">=12" @@ -8155,9 +8205,9 @@ ] }, "node_modules/esbuild": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.4.tgz", - "integrity": "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==", + "version": "0.27.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.5.tgz", + "integrity": "sha512-zdQoHBjuDqKsvV5OPaWansOwfSQ0Js+Uj9J85TBvj3bFW1JjWTSULMRwdQAc8qMeIScbClxeMK0jlrtB9linhA==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -8168,32 +8218,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.4", - "@esbuild/android-arm": "0.27.4", - "@esbuild/android-arm64": "0.27.4", - "@esbuild/android-x64": "0.27.4", - "@esbuild/darwin-arm64": "0.27.4", - "@esbuild/darwin-x64": "0.27.4", - "@esbuild/freebsd-arm64": "0.27.4", - "@esbuild/freebsd-x64": "0.27.4", - "@esbuild/linux-arm": "0.27.4", - "@esbuild/linux-arm64": "0.27.4", - "@esbuild/linux-ia32": "0.27.4", - "@esbuild/linux-loong64": "0.27.4", - "@esbuild/linux-mips64el": "0.27.4", - "@esbuild/linux-ppc64": "0.27.4", - "@esbuild/linux-riscv64": "0.27.4", - "@esbuild/linux-s390x": "0.27.4", - "@esbuild/linux-x64": "0.27.4", - "@esbuild/netbsd-arm64": "0.27.4", - "@esbuild/netbsd-x64": "0.27.4", - "@esbuild/openbsd-arm64": "0.27.4", - "@esbuild/openbsd-x64": "0.27.4", - "@esbuild/openharmony-arm64": "0.27.4", - "@esbuild/sunos-x64": "0.27.4", - "@esbuild/win32-arm64": "0.27.4", - "@esbuild/win32-ia32": "0.27.4", - "@esbuild/win32-x64": "0.27.4" + "@esbuild/aix-ppc64": "0.27.5", + "@esbuild/android-arm": "0.27.5", + "@esbuild/android-arm64": "0.27.5", + "@esbuild/android-x64": "0.27.5", + "@esbuild/darwin-arm64": "0.27.5", + "@esbuild/darwin-x64": "0.27.5", + "@esbuild/freebsd-arm64": "0.27.5", + "@esbuild/freebsd-x64": "0.27.5", + "@esbuild/linux-arm": "0.27.5", + "@esbuild/linux-arm64": "0.27.5", + "@esbuild/linux-ia32": "0.27.5", + "@esbuild/linux-loong64": "0.27.5", + "@esbuild/linux-mips64el": "0.27.5", + "@esbuild/linux-ppc64": "0.27.5", + "@esbuild/linux-riscv64": "0.27.5", + "@esbuild/linux-s390x": "0.27.5", + "@esbuild/linux-x64": "0.27.5", + "@esbuild/netbsd-arm64": "0.27.5", + "@esbuild/netbsd-x64": "0.27.5", + "@esbuild/openbsd-arm64": "0.27.5", + "@esbuild/openbsd-x64": "0.27.5", + "@esbuild/openharmony-arm64": "0.27.5", + "@esbuild/sunos-x64": "0.27.5", + "@esbuild/win32-arm64": "0.27.5", + "@esbuild/win32-ia32": "0.27.5", + "@esbuild/win32-x64": "0.27.5" } }, "node_modules/escalade": { @@ -8982,9 +9032,9 @@ } }, "node_modules/hono": { - "version": "4.12.9", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.9.tgz", - "integrity": "sha512-wy3T8Zm2bsEvxKZM5w21VdHDDcwVS1yUFFY6i8UobSsKfFceT7TOwhbhfKsDyx7tYQlmRM5FLpIuYvNFyjctiA==", + "version": "4.12.10", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.10.tgz", + "integrity": "sha512-mx/p18PLy5og9ufies2GOSUqep98Td9q4i/EF6X7yJgAiIopxqdfIO3jbqsi3jRgTgw88jMDEzVKi+V2EF+27w==", "license": "MIT", "peer": true, "engines": { @@ -9684,9 +9734,9 @@ } }, "node_modules/lodash": { - "version": "4.17.23", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", - "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", "license": "MIT" }, "node_modules/lodash.camelcase": { @@ -10232,9 +10282,9 @@ } }, "node_modules/nypm/node_modules/citty": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/citty/-/citty-0.2.1.tgz", - "integrity": "sha512-kEV95lFBhQgtogAPlQfJJ0WGVSokvLr/UEoFPiKKOXF7pl98HfUVUD0ejsuTCld/9xH9vogSywZ5KqHzXrZpqg==", + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.2.2.tgz", + "integrity": "sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w==", "license": "MIT" }, "node_modules/object-assign": { @@ -10581,9 +10631,9 @@ } }, "node_modules/path-to-regexp": { - "version": "8.4.1", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.1.tgz", - "integrity": "sha512-fvU78fIjZ+SBM9YwCknCvKOUKkLVqtWDVctl0s7xIqfmfb38t2TT4ZU2gHm+Z8xGwgW+QWEU3oQSAzIbo89Ggw==", + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", "license": "MIT", "funding": { "type": "opencollective", @@ -10715,6 +10765,18 @@ "integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==", "license": "MIT" }, + "node_modules/playwright-core": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.59.1.tgz", + "integrity": "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==", + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/png-js": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/png-js/-/png-js-0.1.1.tgz", @@ -12183,9 +12245,9 @@ } }, "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.2.tgz", + "integrity": "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==", "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -12217,9 +12279,9 @@ } }, "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", "license": "MIT" }, "node_modules/unpipe": { diff --git a/package.json b/package.json index 2dfb114..1eccef1 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,8 @@ "bench:vercel": "tsx src/run.ts --provider vercel", "bench:just-bash": "tsx src/run.ts --provider just-bash", "bench:sprites": "tsx src/run.ts --provider sprites", + "bench:browser": "tsx src/run.ts --mode browser", + "bench:browser:browserbase": "tsx src/run.ts --mode browser --provider browserbase", "bench:storage": "tsx src/run.ts --mode storage", "bench:storage:s3": "tsx src/run.ts --mode storage --provider aws-s3", "bench:storage:r2": "tsx src/run.ts --mode storage --provider cloudflare-r2", @@ -38,26 +40,28 @@ }, "dependencies": { "@computesdk/blaxel": "^1.6.7", - "@computesdk/cloudflare": "^1.6.0", - "@computesdk/codesandbox": "^1.5.40", - "@computesdk/daytona": "^1.7.14", - "@computesdk/e2b": "^1.7.34", - "@computesdk/hopx": "^0.2.10", - "@computesdk/just-bash": "^0.3.0", - "@computesdk/modal": "^1.8.29", - "@computesdk/namespace": "^1.5.0", - "@computesdk/r2": "^1.0.0", - "@computesdk/runloop": "^1.3.36", - "@computesdk/s3": "^1.0.0", - "@computesdk/sprites": "^0.1.1", - "@computesdk/tigris": "^1.0.0", - "@computesdk/vercel": "^1.7.13", - "computesdk": "^2.2.1", - "dotenv": "^17.2.1" + "@computesdk/browserbase": "^0.3.0", + "@computesdk/cloudflare": "^1.6.4", + "@computesdk/codesandbox": "^1.5.42", + "@computesdk/daytona": "^1.7.22", + "@computesdk/e2b": "^1.7.42", + "@computesdk/hopx": "^0.2.18", + "@computesdk/just-bash": "^0.4.6", + "@computesdk/modal": "^1.8.37", + "@computesdk/namespace": "^1.6.4", + "@computesdk/r2": "^1.1.1", + "@computesdk/runloop": "^1.3.42", + "@computesdk/s3": "^1.1.1", + "@computesdk/sprites": "^0.1.5", + "@computesdk/tigris": "^1.1.1", + "@computesdk/vercel": "^1.7.21", + "computesdk": "^2.5.4", + "dotenv": "^17.4.0", + "playwright-core": "^1.59.1" }, "devDependencies": { - "@types/node": "^20.0.0", - "tsx": "^4.0.0", - "typescript": "^5.0.0" + "@types/node": "^25.5.0", + "tsx": "^4.21.0", + "typescript": "^6.0.2" } } diff --git a/src/browser/benchmark.ts b/src/browser/benchmark.ts new file mode 100644 index 0000000..006ff96 --- /dev/null +++ b/src/browser/benchmark.ts @@ -0,0 +1,218 @@ +import { chromium } from 'playwright-core'; +import { withTimeout } from '../util/timeout.js'; +import type { BrowserProviderConfig, BrowserBenchmarkResult, BrowserTimingResult } from './types.js'; + +function round(n: number): number { + return Math.round(n * 100) / 100; +} + +function percentile(sorted: number[], p: number): number { + const idx = Math.ceil((p / 100) * sorted.length) - 1; + return sorted[Math.min(idx, sorted.length - 1)]; +} + +function computeBrowserStats(values: number[]): { median: number; p95: number; p99: number } { + if (values.length === 0) return { median: 0, p95: 0, p99: 0 }; + + const sorted = [...values].sort((a, b) => a - b); + const trimCount = Math.floor(sorted.length * 0.05); + const trimmed = trimCount > 0 && sorted.length - 2 * trimCount > 0 + ? sorted.slice(trimCount, sorted.length - trimCount) + : sorted; + + const mid = Math.floor(trimmed.length / 2); + const median = trimmed.length % 2 === 0 + ? (trimmed[mid - 1] + trimmed[mid]) / 2 + : trimmed[mid]; + + return { + median, + p95: percentile(trimmed, 95), + p99: percentile(trimmed, 99), + }; +} + +async function runBrowserIteration( + provider: any, + timeout: number, +): Promise { + const timings = { createMs: 0, connectMs: 0, navigateMs: 0, releaseMs: 0, totalMs: 0 }; + const totalStart = performance.now(); + + try { + // 1. Create session + const createStart = performance.now(); + const session = await withTimeout( + provider.session.create(), + timeout, + 'Session creation timed out', + ) as { sessionId: string; connectUrl: string }; + timings.createMs = performance.now() - createStart; + + let browser; + try { + // 2. Connect over CDP + const connectStart = performance.now(); + browser = await withTimeout( + chromium.connectOverCDP(session.connectUrl), + 30_000, + 'CDP connection timed out', + ); + const context = await browser.newContext(); + const page = await context.newPage(); + timings.connectMs = performance.now() - connectStart; + + // 3. Navigate + const navStart = performance.now(); + await withTimeout( + page.goto('https://www.example.com', { waitUntil: 'load' }), + 30_000, + 'Navigation timed out', + ); + timings.navigateMs = performance.now() - navStart; + } finally { + // 4. Close browser and release session + if (browser) { + await browser.close().catch(() => {}); + } + const releaseStart = performance.now(); + await withTimeout( + provider.session.destroy(session.sessionId), + 15_000, + 'Session destroy timed out', + ); + timings.releaseMs = performance.now() - releaseStart; + } + + timings.totalMs = performance.now() - totalStart; + return { ...timings }; + } catch (err) { + timings.totalMs = performance.now() - totalStart; + const error = err instanceof Error ? err.message : String(err); + return { ...timings, error }; + } +} + +export async function runBrowserBenchmark(config: BrowserProviderConfig): Promise { + const { name, iterations = 25, timeout = 120_000, requiredEnvVars } = config; + + // Check if all required credentials are available + const missingVars = requiredEnvVars.filter(v => !process.env[v]); + if (missingVars.length > 0) { + return { + provider: name, + mode: 'browser', + iterations: [], + summary: { + createMs: { median: 0, p95: 0, p99: 0 }, + connectMs: { median: 0, p95: 0, p99: 0 }, + navigateMs: { median: 0, p95: 0, p99: 0 }, + releaseMs: { median: 0, p95: 0, p99: 0 }, + totalMs: { median: 0, p95: 0, p99: 0 }, + }, + skipped: true, + skipReason: `Missing: ${missingVars.join(', ')}`, + }; + } + + const provider = config.createBrowserProvider(); + const results: BrowserTimingResult[] = []; + + console.log(`\n--- Browser Benchmarking: ${name} (${iterations} iterations) ---`); + console.log('Run Create Connect Navigate Release Total Status'); + console.log('─── ─────── ─────── ──────── ─────── ─────── ──────'); + + for (let i = 0; i < iterations; i++) { + const result = await runBrowserIteration(provider, timeout); + results.push(result); + + const pad = (n: number) => `${Math.round(n)}ms`.padStart(7); + const status = result.error ? `✗ ${result.error.slice(0, 40)}` : '✓'; + console.log( + `${String(i + 1).padStart(3)} ${pad(result.createMs)} ${pad(result.connectMs)} ${pad(result.navigateMs)} ${pad(result.releaseMs)} ${pad(result.totalMs)} ${status}` + ); + } + + const successful = results.filter(r => !r.error); + + // If every iteration failed, mark as skipped + if (successful.length === 0) { + return { + provider: name, + mode: 'browser', + iterations: results, + summary: { + createMs: { median: 0, p95: 0, p99: 0 }, + connectMs: { median: 0, p95: 0, p99: 0 }, + navigateMs: { median: 0, p95: 0, p99: 0 }, + releaseMs: { median: 0, p95: 0, p99: 0 }, + totalMs: { median: 0, p95: 0, p99: 0 }, + }, + skipped: true, + skipReason: 'All iterations failed', + }; + } + + return { + provider: name, + mode: 'browser', + iterations: results, + summary: { + createMs: computeBrowserStats(successful.map(r => r.createMs)), + connectMs: computeBrowserStats(successful.map(r => r.connectMs)), + navigateMs: computeBrowserStats(successful.map(r => r.navigateMs)), + releaseMs: computeBrowserStats(successful.map(r => r.releaseMs)), + totalMs: computeBrowserStats(successful.map(r => r.totalMs)), + }, + }; +} + +function roundStats(s: { median: number; p95: number; p99: number }) { + return { median: round(s.median), p95: round(s.p95), p99: round(s.p99) }; +} + +export async function writeBrowserResultsJson(results: BrowserBenchmarkResult[], outPath: string): Promise { + const fs = await import('fs'); + const os = await import('os'); + + const cleanResults = results.map(r => ({ + provider: r.provider, + mode: r.mode, + iterations: r.iterations.map(i => ({ + createMs: round(i.createMs), + connectMs: round(i.connectMs), + navigateMs: round(i.navigateMs), + releaseMs: round(i.releaseMs), + totalMs: round(i.totalMs), + ...(i.error ? { error: i.error } : {}), + })), + summary: { + createMs: roundStats(r.summary.createMs), + connectMs: roundStats(r.summary.connectMs), + navigateMs: roundStats(r.summary.navigateMs), + releaseMs: roundStats(r.summary.releaseMs), + totalMs: roundStats(r.summary.totalMs), + }, + ...(r.compositeScore !== undefined ? { compositeScore: round(r.compositeScore) } : {}), + ...(r.successRate !== undefined ? { successRate: round(r.successRate) } : {}), + ...(r.skipped ? { skipped: r.skipped, skipReason: r.skipReason } : {}), + })); + + const output = { + version: '1.0', + timestamp: new Date().toISOString(), + environment: { + node: process.version, + platform: os.platform(), + arch: os.arch(), + }, + config: { + iterations: results[0]?.iterations.length || 0, + timeoutMs: 120000, + }, + results: cleanResults, + }; + + fs.writeFileSync(outPath, JSON.stringify(output, null, 2)); + console.log(`Results written to ${outPath}`); +} diff --git a/src/browser/providers.ts b/src/browser/providers.ts new file mode 100644 index 0000000..9c61610 --- /dev/null +++ b/src/browser/providers.ts @@ -0,0 +1,12 @@ +import { browserbase } from '@computesdk/browserbase'; +import type { BrowserProviderConfig } from './types.js'; + +/** + * Browser provider benchmark configurations. + * + * All providers use ComputeSDK's browser packages directly (no ComputeSDK API key). + */ +export const browserProviders: BrowserProviderConfig[] = [ + // + // add more browser providers above +]; diff --git a/src/browser/scoring.ts b/src/browser/scoring.ts new file mode 100644 index 0000000..10c6ce5 --- /dev/null +++ b/src/browser/scoring.ts @@ -0,0 +1,93 @@ +import type { BrowserBenchmarkResult } from './types.js'; + +/** + * Weight configuration for browser composite scoring. + * Total time is weighted highest since it reflects the full user experience. + */ +export interface BrowserScoringWeights { + totalMedian: number; + totalP95: number; + totalP99: number; + createMedian: number; +} + +export const DEFAULT_BROWSER_WEIGHTS: BrowserScoringWeights = { + totalMedian: 0.40, // 40% - overall latency matters most + totalP95: 0.20, // 20% - tail latency + totalP99: 0.10, // 10% - worst case + createMedian: 0.30, // 30% - session provisioning speed +}; + +/** Absolute ceiling for latency in ms. Anything at or above this scores 0. */ +const LATENCY_CEILING_MS = 60000; // 60 seconds + +/** + * Score a latency value (lower is better). + * 0ms = 100, LATENCY_CEILING_MS = 0, values above ceiling are clamped to 0. + */ +function scoreLatency(valueMs: number): number { + return Math.max(0, 100 * (1 - valueMs / LATENCY_CEILING_MS)); +} + +/** + * Compute the success rate for a browser benchmark result (0 to 1). + */ +export function computeBrowserSuccessRate(result: BrowserBenchmarkResult): number { + if (result.skipped || result.iterations.length === 0) return 0; + const successful = result.iterations.filter(i => !i.error).length; + return successful / result.iterations.length; +} + +/** + * Compute a weighted browser score (0-100, higher = better). + */ +function computeBrowserScore( + result: BrowserBenchmarkResult, + weights: BrowserScoringWeights = DEFAULT_BROWSER_WEIGHTS, +): number { + return ( + weights.totalMedian * scoreLatency(result.summary.totalMs.median) + + weights.totalP95 * scoreLatency(result.summary.totalMs.p95) + + weights.totalP99 * scoreLatency(result.summary.totalMs.p99) + + weights.createMedian * scoreLatency(result.summary.createMs.median) + ); +} + +/** + * Compute composite scores for all browser results and attach them. + * + * Formula: compositeScore = browserScore × successRate + * + * Lower latency = better score. + * successRate (0-1) acts as a linear multiplier. + */ +export function computeBrowserCompositeScores( + results: BrowserBenchmarkResult[], + weights: BrowserScoringWeights = DEFAULT_BROWSER_WEIGHTS, +): void { + for (const result of results) { + const successRate = computeBrowserSuccessRate(result); + result.successRate = successRate; + + if (result.skipped || successRate === 0) { + result.compositeScore = 0; + continue; + } + + const browserScore = computeBrowserScore(result, weights); + result.compositeScore = Math.round(browserScore * successRate * 100) / 100; + } +} + +/** + * Sort browser benchmark results by composite score (highest first). + * Skipped providers are always last. + */ +export function sortBrowserByCompositeScore(results: BrowserBenchmarkResult[]): BrowserBenchmarkResult[] { + return [...results].sort((a, b) => { + if (a.skipped && !b.skipped) return 1; + if (!a.skipped && b.skipped) return -1; + if (a.skipped && b.skipped) return 0; + return (b.compositeScore ?? 0) - (a.compositeScore ?? 0); + }); +} diff --git a/src/browser/types.ts b/src/browser/types.ts new file mode 100644 index 0000000..70f59a7 --- /dev/null +++ b/src/browser/types.ts @@ -0,0 +1,48 @@ +export interface BrowserProviderConfig { + /** Provider name */ + name: string; + /** Number of iterations (default: 25) */ + iterations?: number; + /** Timeout for session creation in ms (default: 120000) */ + timeout?: number; + /** Environment variables that must all be set to run this benchmark */ + requiredEnvVars: string[]; + /** Creates a browser provider instance */ + createBrowserProvider: () => any; +} + +export interface BrowserTimingResult { + /** Time to create a browser session in ms */ + createMs: number; + /** Time to connect over CDP in ms */ + connectMs: number; + /** Time to navigate to example.com in ms */ + navigateMs: number; + /** Time to release/destroy the session in ms */ + releaseMs: number; + /** Total time for the full lifecycle in ms */ + totalMs: number; + /** Error message if this iteration failed */ + error?: string; +} + +export interface BrowserStats { + createMs: { median: number; p95: number; p99: number }; + connectMs: { median: number; p95: number; p99: number }; + navigateMs: { median: number; p95: number; p99: number }; + releaseMs: { median: number; p95: number; p99: number }; + totalMs: { median: number; p95: number; p99: number }; +} + +export interface BrowserBenchmarkResult { + provider: string; + mode: 'browser'; + iterations: BrowserTimingResult[]; + summary: BrowserStats; + /** Composite weighted score (0-100, higher = better). Computed post-benchmark. */ + compositeScore?: number; + /** Success rate as a fraction (0 to 1). Computed post-benchmark. */ + successRate?: number; + skipped?: boolean; + skipReason?: string; +} diff --git a/src/merge-results.ts b/src/merge-results.ts index 8fc47cf..18bbe32 100644 --- a/src/merge-results.ts +++ b/src/merge-results.ts @@ -1,7 +1,7 @@ /** * Merge per-provider benchmark results into combined result files. * - * Usage: tsx src/merge-results.ts --input [--mode storage] + * Usage: tsx src/merge-results.ts --input [--mode storage|browser] * * By default, merges sandbox benchmark results: reads latest.json files from * the input directory, groups by mode (sequential/staggered/burst), computes @@ -10,15 +10,21 @@ * With --mode storage, merges storage benchmark results instead: groups by * file size (1mb/10mb/100mb), computes storage-specific composite scores, * and writes combined files to results/storage//latest.json. + * + * With --mode browser, merges browser benchmark results: deduplicates by + * provider, computes browser-specific composite scores, and writes combined + * files to results/browser/latest.json. */ import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; import { computeCompositeScores } from './sandbox/scoring.js'; import { computeStorageCompositeScores, sortStorageByCompositeScore } from './storage/scoring.js'; +import { computeBrowserCompositeScores, sortBrowserByCompositeScore } from './browser/scoring.js'; import { printResultsTable, writeResultsJson } from './sandbox/table.js'; import type { BenchmarkResult } from './sandbox/types.js'; import type { StorageBenchmarkResult } from './storage/types.js'; +import type { BrowserBenchmarkResult } from './browser/types.js'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const ROOT = path.resolve(__dirname, '..'); @@ -32,7 +38,7 @@ function getArgValue(flag: string): string | undefined { const inputDir = getArgValue('--input'); const mergeMode = getArgValue('--mode'); if (!inputDir) { - console.error('Usage: tsx src/merge-results.ts --input [--mode storage]'); + console.error('Usage: tsx src/merge-results.ts --input [--mode storage|browser]'); process.exit(1); } @@ -269,7 +275,107 @@ async function mainStorage() { } } -(mergeMode === 'storage' ? mainStorage() : main()).catch(err => { +/** + * Print a browser results table to stdout. + */ +function printBrowserResultsTable(results: BrowserBenchmarkResult[]): void { + const sorted = sortBrowserByCompositeScore(results); + + console.log(`\n${'='.repeat(110)}`); + console.log(' BROWSER PROVIDER BENCHMARK RESULTS'); + console.log('='.repeat(110)); + console.log( + ['Provider', 'Score', 'Create', 'Connect', 'Navigate', 'Release', 'Total', 'Status'] + .map((h, i) => h.padEnd([14, 8, 12, 12, 12, 12, 12, 10][i])) + .join(' | ') + ); + console.log( + [14, 8, 12, 12, 12, 12, 12, 10].map(w => '-'.repeat(w)).join('-+-') + ); + + for (const r of sorted) { + if (r.skipped) { + console.log([r.provider.padEnd(14), '--'.padEnd(8), '--'.padEnd(12), '--'.padEnd(12), '--'.padEnd(12), '--'.padEnd(12), '--'.padEnd(12), 'SKIPPED'.padEnd(10)].join(' | ')); + continue; + } + const ok = r.iterations.filter(i => !i.error).length; + const total = r.iterations.length; + if (ok === 0 && total > 0) { + console.log([r.provider.padEnd(14), '--'.padEnd(8), '--'.padEnd(12), '--'.padEnd(12), '--'.padEnd(12), '--'.padEnd(12), '--'.padEnd(12), 'FAILED'.padEnd(10)].join(' | ')); + continue; + } + const score = r.compositeScore !== undefined ? r.compositeScore.toFixed(1) : '--'; + const create = (r.summary.createMs.median / 1000).toFixed(2) + 's'; + const connect = (r.summary.connectMs.median / 1000).toFixed(2) + 's'; + const navigate = (r.summary.navigateMs.median / 1000).toFixed(2) + 's'; + const release = (r.summary.releaseMs.median / 1000).toFixed(2) + 's'; + const tot = (r.summary.totalMs.median / 1000).toFixed(2) + 's'; + console.log([r.provider.padEnd(14), score.padEnd(8), create.padEnd(12), connect.padEnd(12), navigate.padEnd(12), release.padEnd(12), tot.padEnd(12), `${ok}/${total} OK`.padEnd(10)].join(' | ')); + } + console.log('='.repeat(110)); +} + +/** + * Merge browser benchmark results. + */ +async function mainBrowser() { + const jsonFiles: string[] = []; + function walk(dir: string) { + if (!fs.existsSync(dir)) return; + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + const full = path.join(dir, entry.name); + if (entry.isDirectory()) walk(full); + else if (entry.name === 'latest.json') jsonFiles.push(full); + } + } + walk(inputDir!); + + if (jsonFiles.length === 0) { + console.error(`No latest.json files found in ${inputDir}`); + process.exit(1); + } + + console.log(`Found ${jsonFiles.length} result files`); + + // Collect all results, deduplicating by provider + const seen = new Map(); + + for (const file of jsonFiles) { + const raw = JSON.parse(fs.readFileSync(file, 'utf-8')) as { results: BrowserBenchmarkResult[] }; + const fromSingleProvider = raw.results.length === 1; + for (const result of raw.results) { + const existing = seen.get(result.provider); + if (!existing || (fromSingleProvider && !existing.fromSingleProvider)) { + seen.set(result.provider, { result, fromSingleProvider }); + } + } + } + + const deduped = Array.from(seen.values()).map(e => e.result); + console.log(`\nMerging ${deduped.length} provider results for mode: browser`); + + // Compute composite scores + computeBrowserCompositeScores(deduped); + + // Print table + printBrowserResultsTable(deduped); + + // Write combined results + const { writeBrowserResultsJson } = await import('./browser/benchmark.js'); + const timestamp = new Date().toISOString().slice(0, 10); + const resultsDir = path.resolve(ROOT, 'results/browser'); + fs.mkdirSync(resultsDir, { recursive: true }); + + const outPath = path.join(resultsDir, `${timestamp}.json`); + await writeBrowserResultsJson(deduped, outPath); + + const latestPath = path.join(resultsDir, 'latest.json'); + fs.copyFileSync(outPath, latestPath); + console.log(`Copied latest: ${latestPath}`); +} + +const runner = mergeMode === 'storage' ? mainStorage : mergeMode === 'browser' ? mainBrowser : main; +runner().catch(err => { console.error('Merge failed:', err); process.exit(1); }); diff --git a/src/run.ts b/src/run.ts index f3f75c8..67c1263 100644 --- a/src/run.ts +++ b/src/run.ts @@ -6,13 +6,17 @@ import { runBenchmark } from './sandbox/benchmark.js'; import { runConcurrentBenchmark } from './sandbox/concurrent.js'; import { runStaggeredBenchmark } from './sandbox/staggered.js'; import { runStorageBenchmark, writeStorageResultsJson } from './storage/benchmark.js'; +import { runBrowserBenchmark, writeBrowserResultsJson } from './browser/benchmark.js'; import { printResultsTable, writeResultsJson } from './sandbox/table.js'; import { providers } from './sandbox/providers.js'; import { storageProviders } from './storage/providers.js'; +import { browserProviders } from './browser/providers.js'; import { computeCompositeScores } from './sandbox/scoring.js'; import { computeStorageCompositeScores } from './storage/scoring.js'; +import { computeBrowserCompositeScores } from './browser/scoring.js'; import type { BenchmarkResult, BenchmarkMode } from './sandbox/types.js'; import type { StorageBenchmarkResult } from './storage/types.js'; +import type { BrowserBenchmarkResult } from './browser/types.js'; // Load .env from the benchmarking root const __dirname = path.dirname(fileURLToPath(import.meta.url)); @@ -33,9 +37,10 @@ function getArgValue(args: string[], flag: string): string | undefined { } /** Resolve which modes to run */ -function getModesToRun(): BenchmarkMode[] | ['storage'] { +function getModesToRun(): BenchmarkMode[] | ['storage'] | ['browser'] { if (!rawMode) return ['sequential', 'staggered', 'burst']; if (rawMode === 'storage') return ['storage']; + if (rawMode === 'browser') return ['browser']; const m = rawMode === 'concurrent' ? 'burst' : rawMode as BenchmarkMode; return [m]; } @@ -165,9 +170,76 @@ async function runStorage(toRun: typeof storageProviders, fileSizeLabel: string) console.log(`Copied latest: ${latestPath}`); } +async function runBrowser(toRun: typeof browserProviders): Promise { + console.log('\n' + '='.repeat(70)); + console.log(' MODE: BROWSER'); + console.log(` Iterations per provider: ${iterations}`); + console.log('='.repeat(70)); + + const results: BrowserBenchmarkResult[] = []; + + for (const providerConfig of toRun) { + const result = await runBrowserBenchmark({ ...providerConfig, iterations }); + results.push(result); + } + + // Compute composite scores + computeBrowserCompositeScores(results); + + // Print summary + console.log('\n--- Browser Benchmark Results ---'); + for (const r of results) { + if (r.skipped) { + console.log(`${r.provider}: SKIPPED (${r.skipReason})`); + continue; + } + console.log(`${r.provider}:`); + console.log(` Total: ${(r.summary.totalMs.median / 1000).toFixed(2)}s (median) — create ${(r.summary.createMs.median / 1000).toFixed(2)}s + connect ${(r.summary.connectMs.median / 1000).toFixed(2)}s + navigate ${(r.summary.navigateMs.median / 1000).toFixed(2)}s + release ${(r.summary.releaseMs.median / 1000).toFixed(2)}s`); + console.log(` Score: ${r.compositeScore?.toFixed(1) || '--'}`); + } + + // Write JSON results to browser subdirectory + const timestamp = new Date().toISOString().slice(0, 10); + const resultsDir = path.resolve(__dirname, '../results/browser'); + fs.mkdirSync(resultsDir, { recursive: true }); + + const outPath = path.join(resultsDir, `${timestamp}.json`); + await writeBrowserResultsJson(results, outPath); + + // Copy results to latest.json + const latestPath = path.join(resultsDir, 'latest.json'); + fs.copyFileSync(outPath, latestPath); + console.log(`Copied latest: ${latestPath}`); +} + async function main() { const modes = getModesToRun(); + // Handle browser mode separately + if (modes[0] === 'browser') { + console.log('ComputeSDK Browser Provider Benchmarks'); + console.log(`Date: ${new Date().toISOString()}\n`); + + // Filter browser providers + const toRun = providerFilter + ? browserProviders.filter(p => p.name === providerFilter) + : browserProviders; + + if (toRun.length === 0) { + if (providerFilter) { + console.error(`Unknown browser provider: ${providerFilter}`); + console.error(`Available: ${browserProviders.map(p => p.name).join(', ')}`); + } else { + console.error('No browser providers configured. Add entries to src/browser/providers.ts.'); + } + process.exit(1); + } + + await runBrowser(toRun); + console.log('\nAll browser tests complete.'); + return; + } + // Handle storage mode separately if (modes[0] === 'storage') { console.log('ComputeSDK Storage Provider Benchmarks'); diff --git a/src/storage/providers.ts b/src/storage/providers.ts index e9f4b92..fbb695e 100644 --- a/src/storage/providers.ts +++ b/src/storage/providers.ts @@ -42,5 +42,5 @@ export const storageProviders: StorageProviderConfig[] = [ fileSizes: [1024 * 1024, 10 * 1024 * 1024], }, // - // add more providers above + // add providers above ];