From 2e68bfc42620e9c8732e35492e174eac86772082 Mon Sep 17 00:00:00 2001 From: Garrison Snelling Date: Thu, 25 Jun 2026 19:35:31 +0000 Subject: [PATCH 1/7] feat: add sandbox reliability benchmark --- .github/workflows/sandbox-reliability.yml | 147 +++++ package.json | 1 + src/merge-results.ts | 70 +++ src/run.ts | 69 ++- src/sandbox/reliability.ts | 700 ++++++++++++++++++++++ 5 files changed, 986 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/sandbox-reliability.yml create mode 100644 src/sandbox/reliability.ts diff --git a/.github/workflows/sandbox-reliability.yml b/.github/workflows/sandbox-reliability.yml new file mode 100644 index 00000000..5ffaa342 --- /dev/null +++ b/.github/workflows/sandbox-reliability.yml @@ -0,0 +1,147 @@ +name: Sandbox Reliability Benchmark + +on: + schedule: + - cron: '*/10 * * * *' + workflow_dispatch: + inputs: + duration-seconds: + description: 'How long each provider job should sample' + required: false + default: '600' + interval-seconds: + description: 'Seconds between samples' + required: false + default: '30' + samples: + description: 'Optional fixed sample count; overrides duration when set' + required: false + default: '' + +permissions: + contents: write + +jobs: + bench: + name: Reliability ${{ matrix.provider }} + runs-on: namespace-profile-default + timeout-minutes: 25 + concurrency: + group: sandbox-reliability-${{ matrix.provider }} + cancel-in-progress: true + strategy: + fail-fast: false + matrix: + provider: + - archil + - blaxel + - cloudflare + - codesandbox + - daytona + - declaw + - e2b + - hopx + - isorun + - modal + - northflank + - runloop + - tensorlake + - upstash + - vercel + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 24 + cache: 'npm' + - name: Install dependencies + run: npm ci + - name: Clear stale results from checkout + run: rm -rf results/ + - name: Run reliability benchmark + env: + COMPUTESDK_API_KEY: ${{ secrets.COMPUTESDK_API_KEY }} + ARCHIL_API_KEY: ${{ secrets.ARCHIL_API_KEY }} + ARCHIL_REGION: ${{ secrets.ARCHIL_REGION }} + ARCHIL_DISK_ID: ${{ secrets.ARCHIL_DISK_ID }} + BL_API_KEY: ${{ secrets.BL_API_KEY }} + BL_WORKSPACE: ${{ secrets.BL_WORKSPACE }} + CLOUDFLARE_SANDBOX_URL: ${{ secrets.CLOUDFLARE_SANDBOX_URL }} + CLOUDFLARE_SANDBOX_SECRET: ${{ secrets.CLOUDFLARE_SANDBOX_SECRET }} + CSB_API_KEY: ${{ secrets.CSB_API_KEY }} + DAYTONA_API_KEY: ${{ secrets.DAYTONA_API_KEY }} + DECLAW_API_KEY: ${{ secrets.DECLAW_API_KEY }} + E2B_API_KEY: ${{ secrets.E2B_API_KEY }} + HOPX_API_KEY: ${{ secrets.HOPX_API_KEY }} + ISORUN_API_KEY: ${{ secrets.ISORUN_API_KEY }} + MODAL_TOKEN_ID: ${{ secrets.MODAL_TOKEN_ID }} + MODAL_TOKEN_SECRET: ${{ secrets.MODAL_TOKEN_SECRET }} + NORTHFLANK_TOKEN: ${{ secrets.NORTHFLANK_TOKEN }} + NORTHFLANK_PROJECT_ID: ${{ secrets.NORTHFLANK_PROJECT_ID }} + RUNLOOP_API_KEY: ${{ secrets.RUNLOOP_API_KEY }} + TENSORLAKE_API_KEY: ${{ secrets.TENSORLAKE_API_KEY }} + UPSTASH_BOX_API_KEY: ${{ secrets.UPSTASH_BOX_API_KEY }} + VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} + VERCEL_TEAM_ID: ${{ secrets.VERCEL_TEAM_ID }} + VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} + run: | + SAMPLE_FLAG="" + if [ -n "${{ github.event.inputs.samples }}" ]; then + SAMPLE_FLAG="--samples ${{ github.event.inputs.samples }}" + fi + npm run bench:reliability -- \ + --provider ${{ matrix.provider }} \ + --duration-seconds ${{ github.event.inputs['duration-seconds'] || '600' }} \ + --interval-seconds ${{ github.event.inputs['interval-seconds'] || '30' }} \ + $SAMPLE_FLAG + - name: Upload results + if: always() + uses: actions/upload-artifact@v4 + with: + name: sandbox-reliability-${{ matrix.provider }} + path: results/ + if-no-files-found: ignore + retention-days: 7 + + collect: + name: Collect Reliability Results + runs-on: namespace-profile-default + needs: bench + if: always() + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: actions/setup-node@v4 + with: + node-version: 24 + cache: 'npm' + - name: Install dependencies + run: npm ci + - name: Download all artifacts + uses: actions/download-artifact@v4 + with: + path: artifacts/ + pattern: sandbox-reliability-* + - name: Merge results + run: npx tsx src/merge-results.ts --input artifacts --mode sandbox-reliability + - name: Commit and push + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add results/sandbox-reliability/ + git diff --cached --quiet && echo "No changes to commit" && exit 0 + git commit -m "chore: update sandbox reliability results [skip ci]" + + branch="${GITHUB_REF#refs/heads/}" + for attempt in 1 2 3 4 5; do + git fetch origin "${branch}" + git rebase "origin/${branch}" || { git rebase --abort; exit 1; } + if git push origin "HEAD:${branch}"; then + echo "Pushed on attempt ${attempt}" + exit 0 + fi + echo "Push rejected (attempt ${attempt}); will rebase and retry" + done + echo "Failed to push after multiple attempts" >&2 + exit 1 diff --git a/package.json b/package.json index 181a4e49..5a77c2fe 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "bench:sequential": "tsx src/run.ts --mode sequential", "bench:staggered": "tsx src/run.ts --mode staggered", "bench:burst": "tsx src/run.ts --mode burst", + "bench:reliability": "tsx src/run.ts --mode reliability", "bench:blaxel": "tsx src/run.ts --provider blaxel", "bench:codesandbox": "tsx src/run.ts --provider codesandbox", "bench:daytona": "tsx src/run.ts --provider daytona", diff --git a/src/merge-results.ts b/src/merge-results.ts index cd1bc777..e5ebd847 100644 --- a/src/merge-results.ts +++ b/src/merge-results.ts @@ -29,7 +29,9 @@ import { sortThroughputByCompositeScore, } from './browser/throughput-scoring.js'; import { printResultsTable, writeResultsJson } from './sandbox/table.js'; +import { printReliabilityResultsTable, writeReliabilityResultsJson } from './sandbox/reliability.js'; import type { BenchmarkResult } from './sandbox/types.js'; +import type { ReliabilityBenchmarkResult } from './sandbox/reliability.js'; import type { StorageBenchmarkResult } from './storage/types.js'; import type { BrowserBenchmarkResult } from './browser/types.js'; import type { ThroughputBenchmarkResult } from './browser/throughput-types.js'; @@ -37,6 +39,10 @@ import type { ThroughputBenchmarkResult } from './browser/throughput-types.js'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const ROOT = path.resolve(__dirname, '..'); +function timestampForFilename(): string { + return new Date().toISOString().replace(/[-:]/g, '').replace(/\.\d{3}Z$/, 'Z'); +} + const args = process.argv.slice(2); function getArgValue(flag: string): string | undefined { const idx = args.indexOf(flag); @@ -66,6 +72,14 @@ interface StorageResultFile { results: StorageBenchmarkResult[]; } +interface ReliabilityResultFile { + version: string; + timestamp: string; + environment: Record; + config: Record; + results: ReliabilityBenchmarkResult[]; +} + /** Map mode to results subdirectory name, matching run.ts logic */ function modeToDir(mode: string): string { switch (mode) { @@ -471,12 +485,68 @@ async function mainBrowserThroughput() { console.log(`Copied latest: ${latestPath}`); } +async function mainSandboxReliability() { + 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`); + + const seen = new Map(); + let mergedConfig: Record = {}; + + for (const file of jsonFiles) { + const dirName = path.basename(path.dirname(file)); + if (dirName !== 'sandbox-reliability') continue; + + const raw: ReliabilityResultFile = JSON.parse(fs.readFileSync(file, 'utf-8')); + mergedConfig = { ...mergedConfig, ...raw.config }; + 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: sandbox-reliability`); + + printReliabilityResultsTable(deduped); + + const timestamp = timestampForFilename(); + const resultsDir = path.resolve(ROOT, 'results/sandbox-reliability'); + fs.mkdirSync(resultsDir, { recursive: true }); + + const outPath = path.join(resultsDir, `${timestamp}.json`); + await writeReliabilityResultsJson(deduped, outPath, mergedConfig); + + const latestPath = path.join(resultsDir, 'latest.json'); + fs.copyFileSync(outPath, latestPath); + console.log(`Copied latest: ${latestPath}`); +} + const runner = mergeMode === 'storage' ? mainStorage : mergeMode === 'browser' ? mainBrowser : mergeMode === 'browser-throughput' ? mainBrowserThroughput + : mergeMode === 'sandbox-reliability' + ? mainSandboxReliability : main; runner().catch(err => { console.error('Merge failed:', err); diff --git a/src/run.ts b/src/run.ts index 28a01f6c..2a86a4fb 100644 --- a/src/run.ts +++ b/src/run.ts @@ -7,6 +7,7 @@ import { fileURLToPath } from 'url'; import { runBenchmark } from './sandbox/benchmark.js'; import { runConcurrentBenchmark } from './sandbox/concurrent.js'; import { runStaggeredBenchmark } from './sandbox/staggered.js'; +import { printReliabilityResultsTable, runReliabilityBenchmark, writeReliabilityResultsJson } from './sandbox/reliability.js'; import { runStorageBenchmark, writeStorageResultsJson } from './storage/benchmark.js'; import { runBrowserBenchmark, writeBrowserResultsJson } from './browser/benchmark.js'; import { runThroughputBenchmark, writeThroughputResultsJson } from './browser/throughput-benchmark.js'; @@ -20,6 +21,7 @@ import { computeStorageCompositeScores } from './storage/scoring.js'; import { computeBrowserCompositeScores } from './browser/scoring.js'; import { computeThroughputCompositeScores } from './browser/throughput-scoring.js'; import type { BenchmarkResult, BenchmarkMode } from './sandbox/types.js'; +import type { ReliabilityBenchmarkResult } from './sandbox/reliability.js'; import type { StorageBenchmarkResult } from './storage/types.js'; import type { BrowserBenchmarkResult } from './browser/types.js'; import type { ThroughputBenchmarkResult } from './browser/throughput-types.js'; @@ -36,18 +38,26 @@ const concurrency = parseInt(getArgValue(args, '--concurrency') || '100', 10); const storageConcurrency = parseInt(getArgValue(args, '--storage-concurrency') || '1', 10); const staggerDelay = parseInt(getArgValue(args, '--stagger-delay') || '200', 10); const fileSizeArg = getArgValue(args, '--file-size') || '10MB'; +const samplesArg = getArgValue(args, '--samples'); +const durationSecondsArg = getArgValue(args, '--duration-seconds'); +const intervalSecondsArg = getArgValue(args, '--interval-seconds'); function getArgValue(args: string[], flag: string): string | undefined { const idx = args.indexOf(flag); return idx !== -1 && idx + 1 < args.length ? args[idx + 1] : undefined; } +function timestampForFilename(): string { + return new Date().toISOString().replace(/[-:]/g, '').replace(/\.\d{3}Z$/, 'Z'); +} + /** Resolve which modes to run */ -function getModesToRun(): BenchmarkMode[] | ['storage'] | ['browser'] | ['browser-throughput'] { +function getModesToRun(): BenchmarkMode[] | ['storage'] | ['browser'] | ['browser-throughput'] | ['reliability'] { if (!rawMode) return ['sequential', 'staggered', 'burst']; if (rawMode === 'storage') return ['storage']; if (rawMode === 'browser') return ['browser']; if (rawMode === 'browser-throughput') return ['browser-throughput']; + if (rawMode === 'reliability') return ['reliability']; const m = rawMode === 'concurrent' ? 'burst' : rawMode as BenchmarkMode; return [m]; } @@ -285,6 +295,44 @@ async function runBrowserThroughput(toRun: typeof throughputProviders): Promise< console.log(`Copied latest: ${latestPath}`); } +async function runReliability(toRun: typeof providers): Promise { + const samples = samplesArg ? parseInt(samplesArg, 10) : undefined; + const durationMs = !samples && durationSecondsArg ? parseInt(durationSecondsArg, 10) * 1000 : undefined; + const intervalMs = intervalSecondsArg ? parseInt(intervalSecondsArg, 10) * 1000 : undefined; + + console.log('\n' + '='.repeat(70)); + console.log(' MODE: SANDBOX RELIABILITY'); + console.log(` Samples: ${samples ?? (durationMs ? 'duration-based' : 1)}`); + console.log(` Duration: ${durationMs ? `${durationMs / 1000}s` : 'n/a'}`); + console.log(` Interval: ${(intervalMs ?? 30_000) / 1000}s`); + console.log('='.repeat(70)); + + const results: ReliabilityBenchmarkResult[] = []; + + for (const providerConfig of toRun) { + const result = await runReliabilityBenchmark(providerConfig, { samples, durationMs, intervalMs }); + results.push(result); + } + + printReliabilityResultsTable(results); + + const timestamp = timestampForFilename(); + const resultsDir = path.resolve(__dirname, '../results/sandbox-reliability'); + fs.mkdirSync(resultsDir, { recursive: true }); + + const outPath = path.join(resultsDir, `${timestamp}.json`); + await writeReliabilityResultsJson(results, outPath, { + samples: samples ?? null, + durationMs: durationMs ?? null, + intervalMs: intervalMs ?? 30_000, + timeoutMs: toRun.reduce((max, p) => Math.max(max, p.timeout ?? 120_000), 0) || 120_000, + }); + + const latestPath = path.join(resultsDir, 'latest.json'); + fs.copyFileSync(outPath, latestPath); + console.log(`Copied latest: ${latestPath}`); +} + async function main() { const modes = getModesToRun(); @@ -312,6 +360,25 @@ async function main() { return; } + if (modes[0] === 'reliability') { + console.log('ComputeSDK Sandbox Reliability Benchmarks'); + console.log(`Date: ${new Date().toISOString()}\n`); + + const toRun = providerFilter + ? providers.filter(p => p.name === providerFilter) + : providers; + + if (toRun.length === 0) { + console.error(`Unknown provider: ${providerFilter}`); + console.error(`Available: ${providers.map(p => p.name).join(', ')}`); + process.exit(1); + } + + await runReliability(toRun); + console.log('\nAll reliability tests complete.'); + return; + } + // Handle browser mode separately if (modes[0] === 'browser') { console.log('ComputeSDK Browser Provider Benchmarks'); diff --git a/src/sandbox/reliability.ts b/src/sandbox/reliability.ts new file mode 100644 index 00000000..bc26792b --- /dev/null +++ b/src/sandbox/reliability.ts @@ -0,0 +1,700 @@ +import fs from 'fs'; +import os from 'os'; +import type { ProviderConfig, Stats } from './types.js'; +import { computeStats } from '../util/stats.js'; +import { withTimeout } from '../util/timeout.js'; +import { randomUUID } from 'node:crypto'; + +export interface ReliabilityOptions { + samples?: number; + durationMs?: number; + intervalMs?: number; +} + +export interface ReliabilityProbeResult { + [key: string]: string | number | boolean | undefined; +} + +export interface ReliabilityIteration { + startedAt: string; + createMs: number; + probeMs: number; + totalMs: number; + probes: ReliabilityProbeResult; + error?: string; +} + +export interface ReliabilityBenchmarkResult { + provider: string; + mode: 'reliability'; + iterations: ReliabilityIteration[]; + summary: { + availability: number; + status: 'healthy' | 'degraded' | 'outage'; + failures: number; + longestFailureStreak: number; + outageEvents: ReliabilityOutageEvent[]; + featureMatrix: Record; + totalMs: Stats; + createMs: Stats; + probeMs: Stats; + fsIsolationFailures: number; + processIsolationFailures: number; + }; + skipped?: boolean; + skipReason?: string; +} + +export interface ReliabilityFeatureSummary { + supportedSamples: number; + totalSamples: number; + supportRate: number; +} + +const FEATURE_KEYS = [ + 'shell', + 'hasNode', + 'hasPython', + 'hasGit', + 'hasCurl', + 'hasWget', + 'hasNpm', + 'hasPnpm', + 'hasYarn', + 'hasPip', + 'hasUv', + 'hasApt', + 'hasApk', + 'hasDnf', + 'hasCargo', + 'hasGo', + 'hasDocker', + 'dockerUsable', + 'tmpWrite', + 'tmpRead', + 'varTmpWrite', + 'homeWrite', + 'cwdWrite', + 'devShmWrite', + 'dnsExample', + 'httpExample', + 'tcpExample', + 'udpDns', + 'localhostTcp', + 'perfProbeOk', + 'fsIsolationOk', + 'processIsolationOk', +] as const; + +export interface ReliabilityOutageEvent { + startedAt: string; + endedAt: string; + samples: number; + errors: string[]; +} + +type RunCommandResult = { exitCode: number; stdout?: string; stderr?: string }; + +const DEFAULT_INTERVAL_MS = 30_000; +const DEFAULT_SAMPLES = 1; +const PROBE_TIMEOUT_MS = 30_000; + +export async function runReliabilityBenchmark( + config: ProviderConfig, + options: ReliabilityOptions = {}, +): Promise { + const { name, timeout = 120_000, requiredEnvVars, sandboxOptions, destroyTimeoutMs } = config; + const missingVars = requiredEnvVars.filter(v => !process.env[v]); + if (missingVars.length > 0) { + return { + provider: name, + mode: 'reliability', + iterations: [], + summary: emptySummary(), + skipped: true, + skipReason: `Missing: ${missingVars.join(', ')}`, + }; + } + + const compute = config.createCompute(); + const samples = options.samples ?? DEFAULT_SAMPLES; + const intervalMs = options.intervalMs ?? DEFAULT_INTERVAL_MS; + const deadline = options.durationMs ? Date.now() + options.durationMs : undefined; + const runId = randomUUID().split('-').join(''); + const iterations: ReliabilityIteration[] = []; + + console.log(`\n--- Reliability benchmarking: ${name} ---`); + console.log(` Samples: ${deadline ? `until ${new Date(deadline).toISOString()}` : samples}`); + console.log(` Interval: ${(intervalMs / 1000).toFixed(1)}s`); + + for (let i = 0; shouldRunSample(i, samples, deadline); i++) { + if (i > 0 && intervalMs > 0) { + await sleepUntilNextSample(intervalMs, deadline); + if (deadline && Date.now() >= deadline) break; + } + + console.log(` Sample ${i + 1}...`); + const result = await runReliabilityIteration( + compute, + timeout, + sandboxOptions, + destroyTimeoutMs, + `${runId}_${i}`, + i > 0 ? `${runId}_${i - 1}` : undefined, + ); + iterations.push(result); + + if (result.error) { + console.log(` FAILED: ${result.error}`); + } else { + console.log(` OK: ${(result.totalMs / 1000).toFixed(2)}s`); + } + } + + return { + provider: name, + mode: 'reliability', + iterations, + summary: summarize(iterations), + }; +} + +async function runReliabilityIteration( + compute: any, + timeout: number, + sandboxOptions: Record | undefined, + destroyTimeoutMs: number = 15_000, + markerToken: string, + previousMarkerToken?: string, +): Promise { + const startedAt = new Date().toISOString(); + const start = performance.now(); + let sandbox: any = null; + let createMs = 0; + let probeMs = 0; + let probes: ReliabilityProbeResult = {}; + + try { + const createStart = performance.now(); + sandbox = await withTimeout(compute.sandbox.create(sandboxOptions), timeout, 'Sandbox creation timed out'); + createMs = performance.now() - createStart; + + const probeStart = performance.now(); + const probe = await withTimeout( + sandbox.runCommand(buildProbeCommand(markerToken, previousMarkerToken)), + PROBE_TIMEOUT_MS, + 'Reliability probe timed out', + ) as RunCommandResult; + probeMs = performance.now() - probeStart; + + probes = parseProbeOutput(probe.stdout || ''); + const totalMs = performance.now() - start; + + if (probe.exitCode !== 0) { + throw new Error(`Reliability probe failed with exit code ${probe.exitCode}: ${probe.stderr || 'Unknown error'}`); + } + if (probes.fsIsolationOk === false) { + throw new Error(`Filesystem isolation failed: previous marker at ${probes.previousFsMarkerPath}`); + } + if (probes.processIsolationOk === false) { + throw new Error('Process isolation failed: previous in-memory marker process is visible'); + } + + return { startedAt, createMs, probeMs, totalMs, probes }; + } catch (err) { + return { + startedAt, + createMs, + probeMs, + totalMs: performance.now() - start, + probes, + error: err instanceof Error ? err.message : String(err), + }; + } finally { + if (sandbox) { + let timer: ReturnType | undefined; + try { + await Promise.race([ + sandbox.destroy(), + new Promise((_, reject) => { + timer = setTimeout(() => reject(new Error('Destroy timeout')), destroyTimeoutMs); + }), + ]); + } catch (err) { + console.warn(` [cleanup] destroy failed: ${err instanceof Error ? err.message : String(err)}`); + } finally { + if (timer) clearTimeout(timer); + } + } + } +} + +function buildProbeCommand(markerToken: string, previousMarkerToken?: string): string { + const fsMarker = '/tmp/.computesdk_reliability_fs_marker'; + const varTmpMarker = '/var/tmp/.computesdk_reliability_fs_marker'; + const processMarker = `computesdk_reliability_process_${markerToken}`; + const previousProcessMarker = previousMarkerToken ? `computesdk_reliability_process_${previousMarkerToken}` : ''; + const token = shellQuote(markerToken); + const processToken = shellQuote(processMarker); + const previousProcessToken = shellQuote(previousProcessMarker); + const perfScript = shellQuote(buildPerfScript()); + + return [ + 'set +e', + `token=${token}`, + `process_marker=${processToken}`, + `previous_process_marker=${previousProcessToken}`, + `fs_marker=${shellQuote(fsMarker)}`, + `var_tmp_marker=${shellQuote(varTmpMarker)}`, + 'previous_fs_marker_path=', + 'previous_fs_marker_value=', + 'for p in "$fs_marker" "$var_tmp_marker"; do if [ -f "$p" ]; then previous_fs_marker_path=$p; previous_fs_marker_value=$(tr -d "\\n" < "$p" 2>/dev/null || true); break; fi; done', + 'process_marker_seen=false', + 'if [ -n "$previous_process_marker" ] && command -v ps >/dev/null 2>&1; then ps_output=$(ps axww 2>/dev/null || ps auxww 2>/dev/null || true); case "$ps_output" in *"$previous_process_marker"*) process_marker_seen=true;; esac; fi', + 'tmp_write=false; tmp_read=false; tmp_file=/tmp/computesdk_reliability_probe_$$; printf "%s" "$token" > "$tmp_file" 2>/dev/null && tmp_write=true; [ "$(cat "$tmp_file" 2>/dev/null)" = "$token" ] && tmp_read=true; rm -f "$tmp_file" 2>/dev/null', + 'var_tmp_write=false; var_tmp_file=/var/tmp/computesdk_reliability_probe_$$; printf "%s" "$token" > "$var_tmp_file" 2>/dev/null && var_tmp_write=true; rm -f "$var_tmp_file" 2>/dev/null', + 'home_write=false; home_file="$HOME/.computesdk_reliability_probe_$$"; if [ -n "$HOME" ]; then printf "%s" "$token" > "$home_file" 2>/dev/null && home_write=true; rm -f "$home_file" 2>/dev/null; fi', + 'cwd_write=false; cwd_file="./.computesdk_reliability_probe_$$"; printf "%s" "$token" > "$cwd_file" 2>/dev/null && cwd_write=true; rm -f "$cwd_file" 2>/dev/null', + 'dev_shm_write=false; if [ -d /dev/shm ]; then shm_file=/dev/shm/computesdk_reliability_probe_$$; printf "%s" "$token" > "$shm_file" 2>/dev/null && dev_shm_write=true; rm -f "$shm_file" 2>/dev/null; fi', + 'dns_example=false; (getent hosts example.com >/dev/null 2>&1 || nslookup example.com >/dev/null 2>&1 || host example.com >/dev/null 2>&1) && dns_example=true', + 'http_example=false; (curl -fsS --max-time 5 https://example.com >/dev/null 2>&1 || wget -q -T 5 -O /dev/null https://example.com >/dev/null 2>&1) && http_example=true', + 'mem_total_kb=$(awk "/MemTotal/ {print \\$2}" /proc/meminfo 2>/dev/null || true)', + 'cpu_count=$(getconf _NPROCESSORS_ONLN 2>/dev/null || nproc 2>/dev/null || true)', + 'disk_free_kb=$(df -Pk /tmp 2>/dev/null | awk "NR==2 {print \\$4}" || true)', + 'uname_value=$(uname -a 2>/dev/null || true)', + 'node_version=$(node -v 2>/dev/null || true)', + 'python_version=$(python3 --version 2>/dev/null || python --version 2>/dev/null || true)', + 'git_version=$(git --version 2>/dev/null || true)', + 'npm_version=$(npm --version 2>/dev/null || true)', + 'pnpm_version=$(pnpm --version 2>/dev/null || true)', + 'yarn_version=$(yarn --version 2>/dev/null || true)', + 'pip_version=$(pip3 --version 2>/dev/null || pip --version 2>/dev/null || true)', + 'uv_version=$(uv --version 2>/dev/null || true)', + 'apt_version=$(apt-get --version 2>/dev/null | sed -n "1p" || apt --version 2>/dev/null | sed -n "1p" || true)', + 'apk_version=$(apk --version 2>/dev/null | sed -n "1p" || true)', + 'dnf_version=$(dnf --version 2>/dev/null | sed -n "1p" || true)', + 'cargo_version=$(cargo --version 2>/dev/null || true)', + 'go_version=$(go version 2>/dev/null || true)', + 'docker_version=$(docker --version 2>/dev/null || true)', + 'docker_usable=false; if [ -n "$docker_version" ] && docker version >/dev/null 2>&1; then docker_usable=true; fi', + `if [ -n "$node_version" ]; then node -e ${perfScript} 2>/dev/null || printf "perfProbeOk=false\\n"; else printf "perfProbeOk=false\\n"; fi`, + 'printf "%s" "$token" > "$fs_marker" 2>/dev/null || true', + 'printf "%s" "$token" > "$var_tmp_marker" 2>/dev/null || true', + '(sh -c "while true; do sleep 300; done" "$process_marker" >/dev/null 2>&1 &)', + 'printf "shell=true\\n"', + 'printf "hasNode=%s\\n" "$([ -n "$node_version" ] && printf true || printf false)"', + 'printf "hasPython=%s\\n" "$([ -n "$python_version" ] && printf true || printf false)"', + 'printf "hasGit=%s\\n" "$([ -n "$git_version" ] && printf true || printf false)"', + 'printf "hasCurl=%s\\n" "$(command -v curl >/dev/null 2>&1 && printf true || printf false)"', + 'printf "hasWget=%s\\n" "$(command -v wget >/dev/null 2>&1 && printf true || printf false)"', + 'printf "hasNpm=%s\\n" "$([ -n "$npm_version" ] && printf true || printf false)"', + 'printf "hasPnpm=%s\\n" "$([ -n "$pnpm_version" ] && printf true || printf false)"', + 'printf "hasYarn=%s\\n" "$([ -n "$yarn_version" ] && printf true || printf false)"', + 'printf "hasPip=%s\\n" "$([ -n "$pip_version" ] && printf true || printf false)"', + 'printf "hasUv=%s\\n" "$([ -n "$uv_version" ] && printf true || printf false)"', + 'printf "hasApt=%s\\n" "$([ -n "$apt_version" ] && printf true || printf false)"', + 'printf "hasApk=%s\\n" "$([ -n "$apk_version" ] && printf true || printf false)"', + 'printf "hasDnf=%s\\n" "$([ -n "$dnf_version" ] && printf true || printf false)"', + 'printf "hasCargo=%s\\n" "$([ -n "$cargo_version" ] && printf true || printf false)"', + 'printf "hasGo=%s\\n" "$([ -n "$go_version" ] && printf true || printf false)"', + 'printf "hasDocker=%s\\n" "$([ -n "$docker_version" ] && printf true || printf false)"', + 'printf "dockerUsable=%s\\n" "$docker_usable"', + 'printf "tmpWrite=%s\\n" "$tmp_write"', + 'printf "tmpRead=%s\\n" "$tmp_read"', + 'printf "varTmpWrite=%s\\n" "$var_tmp_write"', + 'printf "homeWrite=%s\\n" "$home_write"', + 'printf "cwdWrite=%s\\n" "$cwd_write"', + 'printf "devShmWrite=%s\\n" "$dev_shm_write"', + 'printf "dnsExample=%s\\n" "$dns_example"', + 'printf "httpExample=%s\\n" "$http_example"', + 'printf "fsIsolationOk=%s\\n" "$([ -z "$previous_fs_marker_path" ] && printf true || printf false)"', + 'printf "processIsolationOk=%s\\n" "$([ "$process_marker_seen" = false ] && printf true || printf false)"', + 'printf "previousFsMarkerPath=%s\\n" "$previous_fs_marker_path"', + 'printf "previousFsMarkerValue=%s\\n" "$previous_fs_marker_value"', + 'printf "memTotalKb=%s\\n" "$mem_total_kb"', + 'printf "cpuCount=%s\\n" "$cpu_count"', + 'printf "diskFreeKb=%s\\n" "$disk_free_kb"', + 'printf "uname=%s\\n" "$uname_value"', + 'printf "nodeVersion=%s\\n" "$node_version"', + 'printf "pythonVersion=%s\\n" "$python_version"', + 'printf "gitVersion=%s\\n" "$git_version"', + 'printf "npmVersion=%s\\n" "$npm_version"', + 'printf "pnpmVersion=%s\\n" "$pnpm_version"', + 'printf "yarnVersion=%s\\n" "$yarn_version"', + 'printf "pipVersion=%s\\n" "$pip_version"', + 'printf "uvVersion=%s\\n" "$uv_version"', + 'printf "aptVersion=%s\\n" "$apt_version"', + 'printf "apkVersion=%s\\n" "$apk_version"', + 'printf "dnfVersion=%s\\n" "$dnf_version"', + 'printf "cargoVersion=%s\\n" "$cargo_version"', + 'printf "goVersion=%s\\n" "$go_version"', + 'printf "dockerVersion=%s\\n" "$docker_version"', + ].join('; '); +} + +function buildPerfScript(): string { + return ` +const fs = require('fs'); +const net = require('net'); +const dgram = require('dgram'); +const crypto = require('crypto'); +const { performance } = require('perf_hooks'); + +function emit(key, value) { + if (typeof value === 'number' && Number.isFinite(value)) value = Math.round(value * 100) / 100; + process.stdout.write(key + '=' + String(value) + '\\n'); +} + +async function tcpConnect(host, port, timeoutMs) { + return new Promise((resolve) => { + const started = performance.now(); + const socket = net.createConnection({ host, port }); + let settled = false; + const done = (ok) => { + if (settled) return; + settled = true; + const ms = performance.now() - started; + socket.destroy(); + resolve({ ok, ms }); + }; + socket.setTimeout(timeoutMs, () => done(false)); + socket.once('connect', () => done(true)); + socket.once('error', () => done(false)); + }); +} + +async function udpDns(timeoutMs) { + return new Promise((resolve) => { + const started = performance.now(); + const socket = dgram.createSocket('udp4'); + const query = Buffer.from('123401000001000000000000076578616d706c6503636f6d0000010001', 'hex'); + let settled = false; + const done = (ok) => { + if (settled) return; + settled = true; + const ms = performance.now() - started; + socket.close(); + resolve({ ok, ms }); + }; + const timer = setTimeout(() => done(false), timeoutMs); + socket.once('message', () => { clearTimeout(timer); done(true); }); + socket.once('error', () => { clearTimeout(timer); done(false); }); + socket.send(query, 53, '1.1.1.1'); + }); +} + +async function localhostTcp(timeoutMs) { + return new Promise((resolve) => { + const started = performance.now(); + const server = net.createServer((socket) => socket.end('ok')); + let settled = false; + const done = (ok) => { + if (settled) return; + settled = true; + const ms = performance.now() - started; + server.close(() => resolve({ ok, ms })); + }; + const timer = setTimeout(() => done(false), timeoutMs); + server.once('error', () => { clearTimeout(timer); resolve({ ok: false, ms: performance.now() - started }); }); + server.listen(0, '127.0.0.1', () => { + const address = server.address(); + const socket = net.createConnection({ host: '127.0.0.1', port: address.port }); + socket.once('connect', () => { clearTimeout(timer); socket.destroy(); done(true); }); + socket.once('error', () => { clearTimeout(timer); done(false); }); + }); + }); +} + +(async () => { + try { + const cpuBuffer = Buffer.alloc(1024, 7); + let started = performance.now(); + for (let i = 0; i < 5000; i++) crypto.createHash('sha256').update(cpuBuffer).digest(); + const cpuMs = performance.now() - started; + emit('cpuSha256Ms', cpuMs); + emit('cpuSha256OpsPerSecond', 5000 / (cpuMs / 1000)); + + started = performance.now(); + const memoryBuffer = Buffer.alloc(8 * 1024 * 1024); + memoryBuffer.fill(3); + let checksum = 0; + for (let i = 0; i < memoryBuffer.length; i += 4096) checksum += memoryBuffer[i]; + emit('memoryFill8MbMs', performance.now() - started); + emit('memoryChecksum', checksum); + + const dir = '/tmp/computesdk_perf_' + process.pid + '_' + Date.now(); + fs.mkdirSync(dir, { recursive: true }); + const oneMb = Buffer.alloc(1024 * 1024, 5); + const file = dir + '/one_mb.bin'; + started = performance.now(); + fs.writeFileSync(file, oneMb); + emit('fsWrite1MbMs', performance.now() - started); + started = performance.now(); + fs.readFileSync(file); + emit('fsRead1MbMs', performance.now() - started); + started = performance.now(); + for (let i = 0; i < 50; i++) fs.writeFileSync(dir + '/small_' + i, 'x'); + for (let i = 0; i < 50; i++) fs.readFileSync(dir + '/small_' + i); + for (let i = 0; i < 50; i++) fs.unlinkSync(dir + '/small_' + i); + emit('fsSmallFiles50Ms', performance.now() - started); + fs.rmSync(dir, { recursive: true, force: true }); + + const tcp = await tcpConnect('example.com', 443, 5000); + emit('tcpExample', tcp.ok); + emit('tcpExampleMs', tcp.ms); + const udp = await udpDns(5000); + emit('udpDns', udp.ok); + emit('udpDnsMs', udp.ms); + const local = await localhostTcp(5000); + emit('localhostTcp', local.ok); + emit('localhostTcpMs', local.ms); + emit('perfProbeOk', true); + } catch (error) { + emit('perfProbeOk', false); + emit('perfProbeError', error && error.message ? error.message : String(error)); + } +})(); +`; +} + +function parseProbeOutput(stdout: string): ReliabilityProbeResult { + const parsed: ReliabilityProbeResult = {}; + for (const line of stdout.split('\n')) { + const index = line.indexOf('='); + if (index <= 0) continue; + const key = line.slice(0, index).trim(); + const raw = line.slice(index + 1).trim(); + if (raw === 'true') parsed[key] = true; + else if (raw === 'false') parsed[key] = false; + else if (/^\d+(\.\d+)?$/.test(raw)) parsed[key] = Number(raw); + else parsed[key] = raw; + } + return parsed; +} + +function summarize(iterations: ReliabilityIteration[]): ReliabilityBenchmarkResult['summary'] { + const successful = iterations.filter(i => !i.error); + const stats = successful.length > 0 + ? { + totalMs: computeStats(successful.map(i => i.totalMs)), + createMs: computeStats(successful.map(i => i.createMs)), + probeMs: computeStats(successful.map(i => i.probeMs)), + } + : { + totalMs: zeroStats(), + createMs: zeroStats(), + probeMs: zeroStats(), + }; + + return { + availability: iterations.length === 0 ? 0 : successful.length / iterations.length, + status: computeStatus(iterations), + failures: iterations.length - successful.length, + longestFailureStreak: computeLongestFailureStreak(iterations), + outageEvents: computeOutageEvents(iterations), + featureMatrix: computeFeatureMatrix(iterations), + ...stats, + fsIsolationFailures: iterations.filter(i => i.probes.fsIsolationOk === false).length, + processIsolationFailures: iterations.filter(i => i.probes.processIsolationOk === false).length, + }; +} + +function emptySummary(): ReliabilityBenchmarkResult['summary'] { + return { + availability: 0, + status: 'outage', + failures: 0, + longestFailureStreak: 0, + outageEvents: [], + featureMatrix: {}, + totalMs: zeroStats(), + createMs: zeroStats(), + probeMs: zeroStats(), + fsIsolationFailures: 0, + processIsolationFailures: 0, + }; +} + +function computeFeatureMatrix(iterations: ReliabilityIteration[]): Record { + const matrix: Record = {}; + + for (const key of FEATURE_KEYS) { + const samples = iterations.filter(i => typeof i.probes[key] === 'boolean'); + const supportedSamples = samples.filter(i => i.probes[key] === true).length; + matrix[key] = { + supportedSamples, + totalSamples: samples.length, + supportRate: samples.length === 0 ? 0 : supportedSamples / samples.length, + }; + } + + return matrix; +} + +function computeStatus(iterations: ReliabilityIteration[]): ReliabilityBenchmarkResult['summary']['status'] { + if (iterations.length === 0) return 'outage'; + const failures = iterations.filter(i => i.error).length; + if (failures === 0) return 'healthy'; + if (failures === iterations.length) return 'outage'; + return 'degraded'; +} + +function computeLongestFailureStreak(iterations: ReliabilityIteration[]): number { + let longest = 0; + let current = 0; + for (const iteration of iterations) { + if (iteration.error) { + current++; + longest = Math.max(longest, current); + } else { + current = 0; + } + } + return longest; +} + +function computeOutageEvents(iterations: ReliabilityIteration[]): ReliabilityOutageEvent[] { + const events: ReliabilityOutageEvent[] = []; + let current: ReliabilityOutageEvent | undefined; + + for (const iteration of iterations) { + if (!iteration.error) { + current = undefined; + continue; + } + + if (!current) { + current = { + startedAt: iteration.startedAt, + endedAt: iteration.startedAt, + samples: 0, + errors: [], + }; + events.push(current); + } + + current.endedAt = iteration.startedAt; + current.samples++; + if (!current.errors.includes(iteration.error)) { + current.errors.push(iteration.error); + } + } + + return events; +} + +function zeroStats(): Stats { + return { median: 0, p95: 0, p99: 0 }; +} + +function shouldRunSample(index: number, samples: number, deadline?: number): boolean { + if (deadline) return Date.now() < deadline; + return index < samples; +} + +async function sleepUntilNextSample(intervalMs: number, deadline?: number): Promise { + const delay = deadline ? Math.min(intervalMs, Math.max(0, deadline - Date.now())) : intervalMs; + if (delay > 0) await new Promise(resolve => setTimeout(resolve, delay)); +} + +function shellQuote(value: string): string { + return `'${value.split("'").join(`'"'"'`)}'`; +} + +function round(n: number): number { + return Math.round(n * 100) / 100; +} + +export function printReliabilityResultsTable(results: ReliabilityBenchmarkResult[]): void { + console.log(`\n${'='.repeat(100)}`); + console.log(' SANDBOX RELIABILITY BENCHMARK RESULTS'); + console.log('='.repeat(100)); + console.log(['Provider', 'State', 'Availability', 'Median Total', 'Median Create', 'Failures', 'Status'] + .map((h, i) => h.padEnd([14, 10, 14, 14, 14, 10, 10][i])) + .join(' | ')); + console.log([14, 10, 14, 14, 14, 10, 10].map(w => '-'.repeat(w)).join('-+-')); + + for (const result of [...results].sort((a, b) => b.summary.availability - a.summary.availability)) { + if (result.skipped) { + console.log([result.provider.padEnd(14), '--'.padEnd(10), '--'.padEnd(14), '--'.padEnd(14), '--'.padEnd(14), '--'.padEnd(10), 'SKIPPED'.padEnd(10)].join(' | ')); + continue; + } + const ok = result.iterations.filter(i => !i.error).length; + const total = result.iterations.length; + console.log([ + result.provider.padEnd(14), + result.summary.status.padEnd(10), + `${(result.summary.availability * 100).toFixed(1)}%`.padEnd(14), + `${(result.summary.totalMs.median / 1000).toFixed(2)}s`.padEnd(14), + `${(result.summary.createMs.median / 1000).toFixed(2)}s`.padEnd(14), + String(result.summary.failures).padEnd(10), + `${ok}/${total} OK`.padEnd(10), + ].join(' | ')); + } + console.log('='.repeat(100)); +} + +export async function writeReliabilityResultsJson( + results: ReliabilityBenchmarkResult[], + outPath: string, + config: Record = {}, +): Promise { + const cleanResults = results.map(r => ({ + provider: r.provider, + mode: r.mode, + iterations: r.iterations.map(i => ({ + startedAt: i.startedAt, + createMs: round(i.createMs), + probeMs: round(i.probeMs), + totalMs: round(i.totalMs), + probes: i.probes, + ...(i.error ? { error: i.error } : {}), + })), + summary: { + availability: round(r.summary.availability), + status: r.summary.status, + failures: r.summary.failures, + longestFailureStreak: r.summary.longestFailureStreak, + outageEvents: r.summary.outageEvents, + featureMatrix: Object.fromEntries( + Object.entries(r.summary.featureMatrix).map(([key, value]) => [ + key, + { + supportedSamples: value.supportedSamples, + totalSamples: value.totalSamples, + supportRate: round(value.supportRate), + }, + ]), + ), + totalMs: roundStats(r.summary.totalMs), + createMs: roundStats(r.summary.createMs), + probeMs: roundStats(r.summary.probeMs), + fsIsolationFailures: r.summary.fsIsolationFailures, + processIsolationFailures: r.summary.processIsolationFailures, + }, + ...(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, + results: cleanResults, + }; + + fs.writeFileSync(outPath, JSON.stringify(output, null, 2)); + console.log(`Results written to ${outPath}`); +} + +function roundStats(stats: Stats): Stats { + return { + median: round(stats.median), + p95: round(stats.p95), + p99: round(stats.p99), + }; +} From 986e96a63ad968bd38e62b86e29c86ec1d91eab4 Mon Sep 17 00:00:00 2001 From: Garrison Snelling Date: Thu, 25 Jun 2026 20:42:27 +0000 Subject: [PATCH 2/7] ci: run sandbox reliability smoke on pull requests --- .github/workflows/sandbox-reliability.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.github/workflows/sandbox-reliability.yml b/.github/workflows/sandbox-reliability.yml index 5ffaa342..302ae20d 100644 --- a/.github/workflows/sandbox-reliability.yml +++ b/.github/workflows/sandbox-reliability.yml @@ -1,6 +1,14 @@ name: Sandbox Reliability Benchmark on: + pull_request: + paths: + - 'src/sandbox/**' + - 'src/util/**' + - 'src/run.ts' + - 'src/merge-results.ts' + - '.github/workflows/sandbox-reliability.yml' + - 'package.json' schedule: - cron: '*/10 * * * *' workflow_dispatch: @@ -89,6 +97,9 @@ jobs: if [ -n "${{ github.event.inputs.samples }}" ]; then SAMPLE_FLAG="--samples ${{ github.event.inputs.samples }}" fi + if [ "${{ github.event_name }}" = "pull_request" ]; then + SAMPLE_FLAG="--samples 1" + fi npm run bench:reliability -- \ --provider ${{ matrix.provider }} \ --duration-seconds ${{ github.event.inputs['duration-seconds'] || '600' }} \ @@ -126,6 +137,7 @@ jobs: - name: Merge results run: npx tsx src/merge-results.ts --input artifacts --mode sandbox-reliability - name: Commit and push + if: github.event_name != 'pull_request' run: | git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" From 4ffb28534144f81a304f01a1e2c34fc865d3e486 Mon Sep 17 00:00:00 2001 From: Garrison Snelling Date: Thu, 25 Jun 2026 20:49:52 +0000 Subject: [PATCH 3/7] ci: post sandbox reliability results on PRs --- .github/workflows/sandbox-reliability.yml | 67 +++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/.github/workflows/sandbox-reliability.yml b/.github/workflows/sandbox-reliability.yml index 302ae20d..14d06ed4 100644 --- a/.github/workflows/sandbox-reliability.yml +++ b/.github/workflows/sandbox-reliability.yml @@ -136,6 +136,73 @@ jobs: pattern: sandbox-reliability-* - name: Merge results run: npx tsx src/merge-results.ts --input artifacts --mode sandbox-reliability + - 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 latestPath = path.join('results', 'sandbox-reliability', 'latest.json'); + const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; + let body = '## Sandbox Reliability Results\n\n'; + + if (!fs.existsSync(latestPath)) { + body += '> No sandbox reliability 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.summary.availability || 0) - (a.summary.availability || 0)); + + if (results.length === 0) { + body += '> No sandbox reliability results were generated.\n\n'; + } else { + body += '| Provider | State | Availability | Median Total | Median Create | Failures | Key Features |\n'; + body += '|----------|-------|--------------|--------------|---------------|----------|--------------|\n'; + + for (const r of results) { + const summary = r.summary || {}; + const features = summary.featureMatrix || {}; + const supported = Object.values(features).filter(f => f.supportRate === 1).length; + const total = Object.keys(features).length; + const availability = `${((summary.availability || 0) * 100).toFixed(1)}%`; + const totalMs = summary.totalMs?.median ? `${(summary.totalMs.median / 1000).toFixed(2)}s` : '--'; + const createMs = summary.createMs?.median ? `${(summary.createMs.median / 1000).toFixed(2)}s` : '--'; + body += `| ${r.provider} | ${summary.status || '--'} | ${availability} | ${totalMs} | ${createMs} | ${summary.failures ?? '--'} | ${supported}/${total} |\n`; + } + + body += '\n'; + } + } + + body += `---\n*[View full run](${runUrl}) · Results available as build artifacts*`; + + const marker = '## Sandbox Reliability 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, + }); + } - name: Commit and push if: github.event_name != 'pull_request' run: | From 8f884c53f45e436927f7e571b12fef897883d69d Mon Sep 17 00:00:00 2001 From: Garrison Snelling Date: Thu, 25 Jun 2026 20:55:03 +0000 Subject: [PATCH 4/7] fix: encode reliability perf probe command --- src/sandbox/reliability.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/sandbox/reliability.ts b/src/sandbox/reliability.ts index bc26792b..7a4af8ab 100644 --- a/src/sandbox/reliability.ts +++ b/src/sandbox/reliability.ts @@ -237,7 +237,8 @@ function buildProbeCommand(markerToken: string, previousMarkerToken?: string): s const token = shellQuote(markerToken); const processToken = shellQuote(processMarker); const previousProcessToken = shellQuote(previousProcessMarker); - const perfScript = shellQuote(buildPerfScript()); + const perfScript = Buffer.from(buildPerfScript()).toString('base64'); + const perfCommand = shellQuote(`eval(Buffer.from('${perfScript}', 'base64').toString())`); return [ 'set +e', @@ -277,7 +278,7 @@ function buildProbeCommand(markerToken: string, previousMarkerToken?: string): s 'go_version=$(go version 2>/dev/null || true)', 'docker_version=$(docker --version 2>/dev/null || true)', 'docker_usable=false; if [ -n "$docker_version" ] && docker version >/dev/null 2>&1; then docker_usable=true; fi', - `if [ -n "$node_version" ]; then node -e ${perfScript} 2>/dev/null || printf "perfProbeOk=false\\n"; else printf "perfProbeOk=false\\n"; fi`, + `if [ -n "$node_version" ]; then node -e ${perfCommand} 2>/dev/null || printf "perfProbeOk=false\\n"; else printf "perfProbeOk=false\\n"; fi`, 'printf "%s" "$token" > "$fs_marker" 2>/dev/null || true', 'printf "%s" "$token" > "$var_tmp_marker" 2>/dev/null || true', '(sh -c "while true; do sleep 300; done" "$process_marker" >/dev/null 2>&1 &)', From 2030a5fbe94475f24e346414ffb4d2939795927f Mon Sep 17 00:00:00 2001 From: Garrison Snelling Date: Thu, 25 Jun 2026 21:01:12 +0000 Subject: [PATCH 5/7] ci: harden sandbox reliability workflow --- .github/workflows/sandbox-reliability.yml | 28 +++++++++++++++++------ 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/.github/workflows/sandbox-reliability.yml b/.github/workflows/sandbox-reliability.yml index 14d06ed4..bc52ce70 100644 --- a/.github/workflows/sandbox-reliability.yml +++ b/.github/workflows/sandbox-reliability.yml @@ -26,14 +26,15 @@ on: required: false default: '' -permissions: - contents: write +permissions: {} jobs: bench: name: Reliability ${{ matrix.provider }} runs-on: namespace-profile-default timeout-minutes: 25 + permissions: + contents: read concurrency: group: sandbox-reliability-${{ matrix.provider }} cancel-in-progress: true @@ -58,10 +59,11 @@ jobs: - vercel steps: - uses: actions/checkout@v4 + with: + persist-credentials: false - uses: actions/setup-node@v4 with: node-version: 24 - cache: 'npm' - name: Install dependencies run: npm ci - name: Clear stale results from checkout @@ -92,18 +94,26 @@ jobs: VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} VERCEL_TEAM_ID: ${{ secrets.VERCEL_TEAM_ID }} VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} + INPUT_SAMPLES: ${{ github.event.inputs.samples }} + INPUT_DURATION_SECONDS: ${{ github.event.inputs['duration-seconds'] || '600' }} + INPUT_INTERVAL_SECONDS: ${{ github.event.inputs['interval-seconds'] || '30' }} run: | SAMPLE_FLAG="" - if [ -n "${{ github.event.inputs.samples }}" ]; then - SAMPLE_FLAG="--samples ${{ github.event.inputs.samples }}" + if [ -n "$INPUT_SAMPLES" ]; then + case "$INPUT_SAMPLES" in + *[!0-9]*) echo "Invalid samples: $INPUT_SAMPLES" >&2; exit 1 ;; + *) SAMPLE_FLAG="--samples $INPUT_SAMPLES" ;; + esac fi if [ "${{ github.event_name }}" = "pull_request" ]; then SAMPLE_FLAG="--samples 1" fi + case "$INPUT_DURATION_SECONDS" in *[!0-9]*) echo "Invalid duration-seconds: $INPUT_DURATION_SECONDS" >&2; exit 1 ;; esac + case "$INPUT_INTERVAL_SECONDS" in *[!0-9]*) echo "Invalid interval-seconds: $INPUT_INTERVAL_SECONDS" >&2; exit 1 ;; esac npm run bench:reliability -- \ --provider ${{ matrix.provider }} \ - --duration-seconds ${{ github.event.inputs['duration-seconds'] || '600' }} \ - --interval-seconds ${{ github.event.inputs['interval-seconds'] || '30' }} \ + --duration-seconds "$INPUT_DURATION_SECONDS" \ + --interval-seconds "$INPUT_INTERVAL_SECONDS" \ $SAMPLE_FLAG - name: Upload results if: always() @@ -119,6 +129,10 @@ jobs: runs-on: namespace-profile-default needs: bench if: always() + permissions: + contents: write + issues: write + pull-requests: write steps: - uses: actions/checkout@v4 with: From 55800154fb6464c95d12548974681c03cf95ddbc Mon Sep 17 00:00:00 2001 From: Garrison Snelling Date: Thu, 25 Jun 2026 21:28:18 +0000 Subject: [PATCH 6/7] refactor: split sandbox reliability and feature probes --- .github/workflows/sandbox-features.yml | 208 ++++++++++++++++++++++ .github/workflows/sandbox-reliability.yml | 9 +- package.json | 1 + src/merge-results.ts | 10 +- src/run.ts | 56 +++++- src/sandbox/reliability.ts | 50 ++++-- 6 files changed, 312 insertions(+), 22 deletions(-) create mode 100644 .github/workflows/sandbox-features.yml diff --git a/.github/workflows/sandbox-features.yml b/.github/workflows/sandbox-features.yml new file mode 100644 index 00000000..91a6520a --- /dev/null +++ b/.github/workflows/sandbox-features.yml @@ -0,0 +1,208 @@ +name: Sandbox Feature Matrix + +on: + pull_request: + paths: + - 'src/sandbox/**' + - 'src/util/**' + - 'src/run.ts' + - 'src/merge-results.ts' + - '.github/workflows/sandbox-features.yml' + - 'package.json' + schedule: + - cron: '0 6 * * *' + workflow_dispatch: + +permissions: {} + +jobs: + bench: + name: Features ${{ matrix.provider }} + runs-on: namespace-profile-default + timeout-minutes: 25 + permissions: + contents: read + concurrency: + group: sandbox-features-${{ matrix.provider }} + cancel-in-progress: true + strategy: + fail-fast: false + matrix: + provider: + - archil + - blaxel + - cloudflare + - codesandbox + - daytona + - declaw + - e2b + - hopx + - isorun + - modal + - northflank + - runloop + - tensorlake + - upstash + - vercel + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + - uses: actions/setup-node@v4 + with: + node-version: 24 + - name: Install dependencies + run: npm ci + - name: Clear stale results from checkout + run: rm -rf results/ + - name: Run feature matrix benchmark + env: + COMPUTESDK_API_KEY: ${{ secrets.COMPUTESDK_API_KEY }} + ARCHIL_API_KEY: ${{ secrets.ARCHIL_API_KEY }} + ARCHIL_REGION: ${{ secrets.ARCHIL_REGION }} + ARCHIL_DISK_ID: ${{ secrets.ARCHIL_DISK_ID }} + BL_API_KEY: ${{ secrets.BL_API_KEY }} + BL_WORKSPACE: ${{ secrets.BL_WORKSPACE }} + CLOUDFLARE_SANDBOX_URL: ${{ secrets.CLOUDFLARE_SANDBOX_URL }} + CLOUDFLARE_SANDBOX_SECRET: ${{ secrets.CLOUDFLARE_SANDBOX_SECRET }} + CSB_API_KEY: ${{ secrets.CSB_API_KEY }} + DAYTONA_API_KEY: ${{ secrets.DAYTONA_API_KEY }} + DECLAW_API_KEY: ${{ secrets.DECLAW_API_KEY }} + E2B_API_KEY: ${{ secrets.E2B_API_KEY }} + HOPX_API_KEY: ${{ secrets.HOPX_API_KEY }} + ISORUN_API_KEY: ${{ secrets.ISORUN_API_KEY }} + MODAL_TOKEN_ID: ${{ secrets.MODAL_TOKEN_ID }} + MODAL_TOKEN_SECRET: ${{ secrets.MODAL_TOKEN_SECRET }} + NORTHFLANK_TOKEN: ${{ secrets.NORTHFLANK_TOKEN }} + NORTHFLANK_PROJECT_ID: ${{ secrets.NORTHFLANK_PROJECT_ID }} + RUNLOOP_API_KEY: ${{ secrets.RUNLOOP_API_KEY }} + TENSORLAKE_API_KEY: ${{ secrets.TENSORLAKE_API_KEY }} + UPSTASH_BOX_API_KEY: ${{ secrets.UPSTASH_BOX_API_KEY }} + VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} + VERCEL_TEAM_ID: ${{ secrets.VERCEL_TEAM_ID }} + VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} + run: | + npm run bench:features -- --provider ${{ matrix.provider }} + - name: Upload results + if: always() + uses: actions/upload-artifact@v4 + with: + name: sandbox-features-${{ matrix.provider }} + path: results/ + if-no-files-found: ignore + retention-days: 7 + + collect: + name: Collect Feature Matrix Results + runs-on: namespace-profile-default + needs: bench + if: always() + permissions: + contents: write + issues: write + pull-requests: write + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: actions/setup-node@v4 + with: + node-version: 24 + cache: 'npm' + - name: Install dependencies + run: npm ci + - name: Download all artifacts + uses: actions/download-artifact@v4 + with: + path: artifacts/ + pattern: sandbox-features-* + - name: Merge results + run: npx tsx src/merge-results.ts --input artifacts --mode sandbox-features + - 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 latestPath = path.join('results', 'sandbox-features', 'latest.json'); + const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; + let body = '## Sandbox Feature Matrix Results\n\n'; + + if (!fs.existsSync(latestPath)) { + body += '> No sandbox feature matrix 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) => a.provider.localeCompare(b.provider)); + + if (results.length === 0) { + body += '> No sandbox feature matrix results were generated.\n\n'; + } else { + body += '| Provider | Features | Node | Python | Docker | TCP | UDP | FS IO | CPU Ops/s |\n'; + body += '|----------|----------|------|--------|--------|-----|-----|-------|-----------|\n'; + + for (const r of results) { + const features = r.summary?.featureMatrix || {}; + const supported = Object.values(features).filter(f => f.supportRate === 1).length; + const total = Object.keys(features).length; + const probes = r.iterations?.[0]?.probes || {}; + const mark = (value) => value === true ? 'yes' : value === false ? 'no' : '--'; + const fsIo = probes.fsWrite1MbMs && probes.fsRead1MbMs ? `${probes.fsWrite1MbMs}/${probes.fsRead1MbMs}ms` : '--'; + const cpu = probes.cpuSha256OpsPerSecond ? Math.round(probes.cpuSha256OpsPerSecond).toLocaleString() : '--'; + body += `| ${r.provider} | ${supported}/${total} | ${mark(probes.hasNode)} | ${mark(probes.hasPython)} | ${mark(probes.dockerUsable)} | ${mark(probes.tcpExample)} | ${mark(probes.udpDns)} | ${fsIo} | ${cpu} |\n`; + } + + body += '\n'; + } + } + + body += `---\n*[View full run](${runUrl}) · Results available as build artifacts*`; + + const marker = '## Sandbox Feature Matrix 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, + }); + } + - name: Commit and push + if: github.event_name != 'pull_request' + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add results/sandbox-features/ + git diff --cached --quiet && echo "No changes to commit" && exit 0 + git commit -m "chore: update sandbox feature matrix results [skip ci]" + + branch="${GITHUB_REF#refs/heads/}" + for attempt in 1 2 3 4 5; do + git fetch origin "${branch}" + git rebase "origin/${branch}" || { git rebase --abort; exit 1; } + if git push origin "HEAD:${branch}"; then + echo "Pushed on attempt ${attempt}" + exit 0 + fi + echo "Push rejected (attempt ${attempt}); will rebase and retry" + done + echo "Failed to push after multiple attempts" >&2 + exit 1 diff --git a/.github/workflows/sandbox-reliability.yml b/.github/workflows/sandbox-reliability.yml index bc52ce70..3a836472 100644 --- a/.github/workflows/sandbox-reliability.yml +++ b/.github/workflows/sandbox-reliability.yml @@ -174,18 +174,15 @@ jobs: if (results.length === 0) { body += '> No sandbox reliability results were generated.\n\n'; } else { - body += '| Provider | State | Availability | Median Total | Median Create | Failures | Key Features |\n'; - body += '|----------|-------|--------------|--------------|---------------|----------|--------------|\n'; + body += '| Provider | State | Availability | Median Total | Median Create | Failures |\n'; + body += '|----------|-------|--------------|--------------|---------------|----------|\n'; for (const r of results) { const summary = r.summary || {}; - const features = summary.featureMatrix || {}; - const supported = Object.values(features).filter(f => f.supportRate === 1).length; - const total = Object.keys(features).length; const availability = `${((summary.availability || 0) * 100).toFixed(1)}%`; const totalMs = summary.totalMs?.median ? `${(summary.totalMs.median / 1000).toFixed(2)}s` : '--'; const createMs = summary.createMs?.median ? `${(summary.createMs.median / 1000).toFixed(2)}s` : '--'; - body += `| ${r.provider} | ${summary.status || '--'} | ${availability} | ${totalMs} | ${createMs} | ${summary.failures ?? '--'} | ${supported}/${total} |\n`; + body += `| ${r.provider} | ${summary.status || '--'} | ${availability} | ${totalMs} | ${createMs} | ${summary.failures ?? '--'} |\n`; } body += '\n'; diff --git a/package.json b/package.json index 5a77c2fe..c787eaa8 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "bench:staggered": "tsx src/run.ts --mode staggered", "bench:burst": "tsx src/run.ts --mode burst", "bench:reliability": "tsx src/run.ts --mode reliability", + "bench:features": "tsx src/run.ts --mode features", "bench:blaxel": "tsx src/run.ts --provider blaxel", "bench:codesandbox": "tsx src/run.ts --provider codesandbox", "bench:daytona": "tsx src/run.ts --provider daytona", diff --git a/src/merge-results.ts b/src/merge-results.ts index e5ebd847..af64db47 100644 --- a/src/merge-results.ts +++ b/src/merge-results.ts @@ -485,7 +485,7 @@ async function mainBrowserThroughput() { console.log(`Copied latest: ${latestPath}`); } -async function mainSandboxReliability() { +async function mainSandboxReliability(resultsSubdir = 'sandbox-reliability') { const jsonFiles: string[] = []; function walk(dir: string) { if (!fs.existsSync(dir)) return; @@ -509,7 +509,7 @@ async function mainSandboxReliability() { for (const file of jsonFiles) { const dirName = path.basename(path.dirname(file)); - if (dirName !== 'sandbox-reliability') continue; + if (dirName !== resultsSubdir) continue; const raw: ReliabilityResultFile = JSON.parse(fs.readFileSync(file, 'utf-8')); mergedConfig = { ...mergedConfig, ...raw.config }; @@ -523,12 +523,12 @@ async function mainSandboxReliability() { } const deduped = Array.from(seen.values()).map(e => e.result); - console.log(`\nMerging ${deduped.length} provider results for mode: sandbox-reliability`); + console.log(`\nMerging ${deduped.length} provider results for mode: ${resultsSubdir}`); printReliabilityResultsTable(deduped); const timestamp = timestampForFilename(); - const resultsDir = path.resolve(ROOT, 'results/sandbox-reliability'); + const resultsDir = path.resolve(ROOT, `results/${resultsSubdir}`); fs.mkdirSync(resultsDir, { recursive: true }); const outPath = path.join(resultsDir, `${timestamp}.json`); @@ -547,6 +547,8 @@ const runner = mergeMode === 'storage' ? mainBrowserThroughput : mergeMode === 'sandbox-reliability' ? mainSandboxReliability + : mergeMode === 'sandbox-features' + ? () => mainSandboxReliability('sandbox-features') : main; runner().catch(err => { console.error('Merge failed:', err); diff --git a/src/run.ts b/src/run.ts index 2a86a4fb..ff215576 100644 --- a/src/run.ts +++ b/src/run.ts @@ -7,7 +7,7 @@ import { fileURLToPath } from 'url'; import { runBenchmark } from './sandbox/benchmark.js'; import { runConcurrentBenchmark } from './sandbox/concurrent.js'; import { runStaggeredBenchmark } from './sandbox/staggered.js'; -import { printReliabilityResultsTable, runReliabilityBenchmark, writeReliabilityResultsJson } from './sandbox/reliability.js'; +import { printReliabilityResultsTable, runFeatureMatrixBenchmark, runReliabilityBenchmark, writeReliabilityResultsJson } from './sandbox/reliability.js'; import { runStorageBenchmark, writeStorageResultsJson } from './storage/benchmark.js'; import { runBrowserBenchmark, writeBrowserResultsJson } from './browser/benchmark.js'; import { runThroughputBenchmark, writeThroughputResultsJson } from './browser/throughput-benchmark.js'; @@ -52,12 +52,13 @@ function timestampForFilename(): string { } /** Resolve which modes to run */ -function getModesToRun(): BenchmarkMode[] | ['storage'] | ['browser'] | ['browser-throughput'] | ['reliability'] { +function getModesToRun(): BenchmarkMode[] | ['storage'] | ['browser'] | ['browser-throughput'] | ['reliability'] | ['features'] { if (!rawMode) return ['sequential', 'staggered', 'burst']; if (rawMode === 'storage') return ['storage']; if (rawMode === 'browser') return ['browser']; if (rawMode === 'browser-throughput') return ['browser-throughput']; if (rawMode === 'reliability') return ['reliability']; + if (rawMode === 'features') return ['features']; const m = rawMode === 'concurrent' ? 'burst' : rawMode as BenchmarkMode; return [m]; } @@ -333,6 +334,38 @@ async function runReliability(toRun: typeof providers): Promise { console.log(`Copied latest: ${latestPath}`); } +async function runFeatures(toRun: typeof providers): Promise { + console.log('\n' + '='.repeat(70)); + console.log(' MODE: SANDBOX FEATURES'); + console.log(' Samples per provider: 1'); + console.log('='.repeat(70)); + + const results: ReliabilityBenchmarkResult[] = []; + + for (const providerConfig of toRun) { + const result = await runFeatureMatrixBenchmark(providerConfig, { samples: 1, intervalMs: 0 }); + results.push(result); + } + + printReliabilityResultsTable(results); + + const timestamp = timestampForFilename(); + const resultsDir = path.resolve(__dirname, '../results/sandbox-features'); + fs.mkdirSync(resultsDir, { recursive: true }); + + const outPath = path.join(resultsDir, `${timestamp}.json`); + await writeReliabilityResultsJson(results, outPath, { + samples: 1, + durationMs: null, + intervalMs: 0, + timeoutMs: toRun.reduce((max, p) => Math.max(max, p.timeout ?? 120_000), 0) || 120_000, + }); + + const latestPath = path.join(resultsDir, 'latest.json'); + fs.copyFileSync(outPath, latestPath); + console.log(`Copied latest: ${latestPath}`); +} + async function main() { const modes = getModesToRun(); @@ -379,6 +412,25 @@ async function main() { return; } + if (modes[0] === 'features') { + console.log('ComputeSDK Sandbox Feature Matrix Benchmarks'); + console.log(`Date: ${new Date().toISOString()}\n`); + + const toRun = providerFilter + ? providers.filter(p => p.name === providerFilter) + : providers; + + if (toRun.length === 0) { + console.error(`Unknown provider: ${providerFilter}`); + console.error(`Available: ${providers.map(p => p.name).join(', ')}`); + process.exit(1); + } + + await runFeatures(toRun); + console.log('\nAll feature matrix tests complete.'); + return; + } + // Handle browser mode separately if (modes[0] === 'browser') { console.log('ComputeSDK Browser Provider Benchmarks'); diff --git a/src/sandbox/reliability.ts b/src/sandbox/reliability.ts index 7a4af8ab..a059df29 100644 --- a/src/sandbox/reliability.ts +++ b/src/sandbox/reliability.ts @@ -9,6 +9,7 @@ export interface ReliabilityOptions { samples?: number; durationMs?: number; intervalMs?: number; + probeProfile?: ProbeProfile; } export interface ReliabilityProbeResult { @@ -26,7 +27,7 @@ export interface ReliabilityIteration { export interface ReliabilityBenchmarkResult { provider: string; - mode: 'reliability'; + mode: ProbeProfile; iterations: ReliabilityIteration[]; summary: { availability: number; @@ -94,6 +95,7 @@ export interface ReliabilityOutageEvent { } type RunCommandResult = { exitCode: number; stdout?: string; stderr?: string }; +type ProbeProfile = 'reliability' | 'features'; const DEFAULT_INTERVAL_MS = 30_000; const DEFAULT_SAMPLES = 1; @@ -104,11 +106,12 @@ export async function runReliabilityBenchmark( options: ReliabilityOptions = {}, ): Promise { const { name, timeout = 120_000, requiredEnvVars, sandboxOptions, destroyTimeoutMs } = config; + const probeProfile = options.probeProfile ?? 'reliability'; const missingVars = requiredEnvVars.filter(v => !process.env[v]); if (missingVars.length > 0) { return { provider: name, - mode: 'reliability', + mode: probeProfile, iterations: [], summary: emptySummary(), skipped: true, @@ -123,7 +126,7 @@ export async function runReliabilityBenchmark( const runId = randomUUID().split('-').join(''); const iterations: ReliabilityIteration[] = []; - console.log(`\n--- Reliability benchmarking: ${name} ---`); + console.log(`\n--- ${probeProfile === 'features' ? 'Feature matrix' : 'Reliability'} benchmarking: ${name} ---`); console.log(` Samples: ${deadline ? `until ${new Date(deadline).toISOString()}` : samples}`); console.log(` Interval: ${(intervalMs / 1000).toFixed(1)}s`); @@ -141,6 +144,7 @@ export async function runReliabilityBenchmark( destroyTimeoutMs, `${runId}_${i}`, i > 0 ? `${runId}_${i - 1}` : undefined, + probeProfile, ); iterations.push(result); @@ -153,12 +157,19 @@ export async function runReliabilityBenchmark( return { provider: name, - mode: 'reliability', + mode: probeProfile, iterations, summary: summarize(iterations), }; } +export async function runFeatureMatrixBenchmark( + config: ProviderConfig, + options: Omit = {}, +): Promise { + return runReliabilityBenchmark(config, { ...options, probeProfile: 'features' }); +} + async function runReliabilityIteration( compute: any, timeout: number, @@ -166,6 +177,7 @@ async function runReliabilityIteration( destroyTimeoutMs: number = 15_000, markerToken: string, previousMarkerToken?: string, + probeProfile: ProbeProfile = 'reliability', ): Promise { const startedAt = new Date().toISOString(); const start = performance.now(); @@ -181,9 +193,9 @@ async function runReliabilityIteration( const probeStart = performance.now(); const probe = await withTimeout( - sandbox.runCommand(buildProbeCommand(markerToken, previousMarkerToken)), + sandbox.runCommand(buildProbeCommand(markerToken, previousMarkerToken, probeProfile)), PROBE_TIMEOUT_MS, - 'Reliability probe timed out', + `${probeProfile === 'features' ? 'Feature matrix' : 'Reliability'} probe timed out`, ) as RunCommandResult; probeMs = performance.now() - probeStart; @@ -193,10 +205,10 @@ async function runReliabilityIteration( if (probe.exitCode !== 0) { throw new Error(`Reliability probe failed with exit code ${probe.exitCode}: ${probe.stderr || 'Unknown error'}`); } - if (probes.fsIsolationOk === false) { + if (probeProfile === 'features' && probes.fsIsolationOk === false) { throw new Error(`Filesystem isolation failed: previous marker at ${probes.previousFsMarkerPath}`); } - if (probes.processIsolationOk === false) { + if (probeProfile === 'features' && probes.processIsolationOk === false) { throw new Error('Process isolation failed: previous in-memory marker process is visible'); } @@ -229,7 +241,9 @@ async function runReliabilityIteration( } } -function buildProbeCommand(markerToken: string, previousMarkerToken?: string): string { +function buildProbeCommand(markerToken: string, previousMarkerToken?: string, probeProfile: ProbeProfile = 'reliability'): string { + if (probeProfile === 'reliability') return buildReliabilityProbeCommand(); + const fsMarker = '/tmp/.computesdk_reliability_fs_marker'; const varTmpMarker = '/var/tmp/.computesdk_reliability_fs_marker'; const processMarker = `computesdk_reliability_process_${markerToken}`; @@ -333,6 +347,19 @@ function buildProbeCommand(markerToken: string, previousMarkerToken?: string): s ].join('; '); } +function buildReliabilityProbeCommand(): string { + return [ + 'set +e', + 'started_at=$(date -u +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || true)', + 'node_version=$(node -v 2>/dev/null || true)', + 'printf "commandOk=true\\n"', + 'printf "shell=true\\n"', + 'printf "hasNode=%s\\n" "$([ -n "$node_version" ] && printf true || printf false)"', + 'printf "nodeVersion=%s\\n" "$node_version"', + 'printf "sandboxTime=%s\\n" "$started_at"', + ].join('; '); +} + function buildPerfScript(): string { return ` const fs = require('fs'); @@ -607,8 +634,11 @@ function round(n: number): number { } export function printReliabilityResultsTable(results: ReliabilityBenchmarkResult[]): void { + const title = results.some(r => r.mode === 'features') + ? 'SANDBOX FEATURE MATRIX RESULTS' + : 'SANDBOX RELIABILITY BENCHMARK RESULTS'; console.log(`\n${'='.repeat(100)}`); - console.log(' SANDBOX RELIABILITY BENCHMARK RESULTS'); + console.log(` ${title}`); console.log('='.repeat(100)); console.log(['Provider', 'State', 'Availability', 'Median Total', 'Median Create', 'Failures', 'Status'] .map((h, i) => h.padEnd([14, 10, 14, 14, 14, 10, 10][i])) From 01c5a1f1704b373bc7847ffe30ad8fdd99c9acd8 Mon Sep 17 00:00:00 2001 From: Garrison Snelling Date: Thu, 25 Jun 2026 21:43:23 +0000 Subject: [PATCH 7/7] fix: address sandbox workflow review feedback --- .github/workflows/sandbox-features.yml | 50 +++++++++++------------ .github/workflows/sandbox-reliability.yml | 50 +++++++++++------------ src/merge-results.ts | 7 +++- src/run.ts | 22 ++++++++-- src/sandbox/reliability.ts | 5 ++- 5 files changed, 78 insertions(+), 56 deletions(-) diff --git a/.github/workflows/sandbox-features.yml b/.github/workflows/sandbox-features.yml index 91a6520a..f869234a 100644 --- a/.github/workflows/sandbox-features.yml +++ b/.github/workflows/sandbox-features.yml @@ -23,7 +23,7 @@ jobs: permissions: contents: read concurrency: - group: sandbox-features-${{ matrix.provider }} + group: sandbox-features-${{ github.event_name }}-${{ github.ref }}-${{ matrix.provider }} cancel-in-progress: true strategy: fail-fast: false @@ -57,30 +57,30 @@ jobs: run: rm -rf results/ - name: Run feature matrix benchmark env: - COMPUTESDK_API_KEY: ${{ secrets.COMPUTESDK_API_KEY }} - ARCHIL_API_KEY: ${{ secrets.ARCHIL_API_KEY }} - ARCHIL_REGION: ${{ secrets.ARCHIL_REGION }} - ARCHIL_DISK_ID: ${{ secrets.ARCHIL_DISK_ID }} - BL_API_KEY: ${{ secrets.BL_API_KEY }} - BL_WORKSPACE: ${{ secrets.BL_WORKSPACE }} - CLOUDFLARE_SANDBOX_URL: ${{ secrets.CLOUDFLARE_SANDBOX_URL }} - CLOUDFLARE_SANDBOX_SECRET: ${{ secrets.CLOUDFLARE_SANDBOX_SECRET }} - CSB_API_KEY: ${{ secrets.CSB_API_KEY }} - DAYTONA_API_KEY: ${{ secrets.DAYTONA_API_KEY }} - DECLAW_API_KEY: ${{ secrets.DECLAW_API_KEY }} - E2B_API_KEY: ${{ secrets.E2B_API_KEY }} - HOPX_API_KEY: ${{ secrets.HOPX_API_KEY }} - ISORUN_API_KEY: ${{ secrets.ISORUN_API_KEY }} - MODAL_TOKEN_ID: ${{ secrets.MODAL_TOKEN_ID }} - MODAL_TOKEN_SECRET: ${{ secrets.MODAL_TOKEN_SECRET }} - NORTHFLANK_TOKEN: ${{ secrets.NORTHFLANK_TOKEN }} - NORTHFLANK_PROJECT_ID: ${{ secrets.NORTHFLANK_PROJECT_ID }} - RUNLOOP_API_KEY: ${{ secrets.RUNLOOP_API_KEY }} - TENSORLAKE_API_KEY: ${{ secrets.TENSORLAKE_API_KEY }} - UPSTASH_BOX_API_KEY: ${{ secrets.UPSTASH_BOX_API_KEY }} - VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} - VERCEL_TEAM_ID: ${{ secrets.VERCEL_TEAM_ID }} - VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} + COMPUTESDK_API_KEY: ${{ github.event_name != 'pull_request' && secrets.COMPUTESDK_API_KEY || '' }} + ARCHIL_API_KEY: ${{ github.event_name != 'pull_request' && secrets.ARCHIL_API_KEY || '' }} + ARCHIL_REGION: ${{ github.event_name != 'pull_request' && secrets.ARCHIL_REGION || '' }} + ARCHIL_DISK_ID: ${{ github.event_name != 'pull_request' && secrets.ARCHIL_DISK_ID || '' }} + BL_API_KEY: ${{ github.event_name != 'pull_request' && secrets.BL_API_KEY || '' }} + BL_WORKSPACE: ${{ github.event_name != 'pull_request' && secrets.BL_WORKSPACE || '' }} + CLOUDFLARE_SANDBOX_URL: ${{ github.event_name != 'pull_request' && secrets.CLOUDFLARE_SANDBOX_URL || '' }} + CLOUDFLARE_SANDBOX_SECRET: ${{ github.event_name != 'pull_request' && secrets.CLOUDFLARE_SANDBOX_SECRET || '' }} + CSB_API_KEY: ${{ github.event_name != 'pull_request' && secrets.CSB_API_KEY || '' }} + DAYTONA_API_KEY: ${{ github.event_name != 'pull_request' && secrets.DAYTONA_API_KEY || '' }} + DECLAW_API_KEY: ${{ github.event_name != 'pull_request' && secrets.DECLAW_API_KEY || '' }} + E2B_API_KEY: ${{ github.event_name != 'pull_request' && secrets.E2B_API_KEY || '' }} + HOPX_API_KEY: ${{ github.event_name != 'pull_request' && secrets.HOPX_API_KEY || '' }} + ISORUN_API_KEY: ${{ github.event_name != 'pull_request' && secrets.ISORUN_API_KEY || '' }} + MODAL_TOKEN_ID: ${{ github.event_name != 'pull_request' && secrets.MODAL_TOKEN_ID || '' }} + MODAL_TOKEN_SECRET: ${{ github.event_name != 'pull_request' && secrets.MODAL_TOKEN_SECRET || '' }} + NORTHFLANK_TOKEN: ${{ github.event_name != 'pull_request' && secrets.NORTHFLANK_TOKEN || '' }} + NORTHFLANK_PROJECT_ID: ${{ github.event_name != 'pull_request' && secrets.NORTHFLANK_PROJECT_ID || '' }} + RUNLOOP_API_KEY: ${{ github.event_name != 'pull_request' && secrets.RUNLOOP_API_KEY || '' }} + TENSORLAKE_API_KEY: ${{ github.event_name != 'pull_request' && secrets.TENSORLAKE_API_KEY || '' }} + UPSTASH_BOX_API_KEY: ${{ github.event_name != 'pull_request' && secrets.UPSTASH_BOX_API_KEY || '' }} + VERCEL_TOKEN: ${{ github.event_name != 'pull_request' && secrets.VERCEL_TOKEN || '' }} + VERCEL_TEAM_ID: ${{ github.event_name != 'pull_request' && secrets.VERCEL_TEAM_ID || '' }} + VERCEL_PROJECT_ID: ${{ github.event_name != 'pull_request' && secrets.VERCEL_PROJECT_ID || '' }} run: | npm run bench:features -- --provider ${{ matrix.provider }} - name: Upload results diff --git a/.github/workflows/sandbox-reliability.yml b/.github/workflows/sandbox-reliability.yml index 3a836472..2b25347f 100644 --- a/.github/workflows/sandbox-reliability.yml +++ b/.github/workflows/sandbox-reliability.yml @@ -36,7 +36,7 @@ jobs: permissions: contents: read concurrency: - group: sandbox-reliability-${{ matrix.provider }} + group: sandbox-reliability-${{ github.event_name }}-${{ github.ref }}-${{ matrix.provider }} cancel-in-progress: true strategy: fail-fast: false @@ -70,30 +70,30 @@ jobs: run: rm -rf results/ - name: Run reliability benchmark env: - COMPUTESDK_API_KEY: ${{ secrets.COMPUTESDK_API_KEY }} - ARCHIL_API_KEY: ${{ secrets.ARCHIL_API_KEY }} - ARCHIL_REGION: ${{ secrets.ARCHIL_REGION }} - ARCHIL_DISK_ID: ${{ secrets.ARCHIL_DISK_ID }} - BL_API_KEY: ${{ secrets.BL_API_KEY }} - BL_WORKSPACE: ${{ secrets.BL_WORKSPACE }} - CLOUDFLARE_SANDBOX_URL: ${{ secrets.CLOUDFLARE_SANDBOX_URL }} - CLOUDFLARE_SANDBOX_SECRET: ${{ secrets.CLOUDFLARE_SANDBOX_SECRET }} - CSB_API_KEY: ${{ secrets.CSB_API_KEY }} - DAYTONA_API_KEY: ${{ secrets.DAYTONA_API_KEY }} - DECLAW_API_KEY: ${{ secrets.DECLAW_API_KEY }} - E2B_API_KEY: ${{ secrets.E2B_API_KEY }} - HOPX_API_KEY: ${{ secrets.HOPX_API_KEY }} - ISORUN_API_KEY: ${{ secrets.ISORUN_API_KEY }} - MODAL_TOKEN_ID: ${{ secrets.MODAL_TOKEN_ID }} - MODAL_TOKEN_SECRET: ${{ secrets.MODAL_TOKEN_SECRET }} - NORTHFLANK_TOKEN: ${{ secrets.NORTHFLANK_TOKEN }} - NORTHFLANK_PROJECT_ID: ${{ secrets.NORTHFLANK_PROJECT_ID }} - RUNLOOP_API_KEY: ${{ secrets.RUNLOOP_API_KEY }} - TENSORLAKE_API_KEY: ${{ secrets.TENSORLAKE_API_KEY }} - UPSTASH_BOX_API_KEY: ${{ secrets.UPSTASH_BOX_API_KEY }} - VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} - VERCEL_TEAM_ID: ${{ secrets.VERCEL_TEAM_ID }} - VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} + COMPUTESDK_API_KEY: ${{ github.event_name != 'pull_request' && secrets.COMPUTESDK_API_KEY || '' }} + ARCHIL_API_KEY: ${{ github.event_name != 'pull_request' && secrets.ARCHIL_API_KEY || '' }} + ARCHIL_REGION: ${{ github.event_name != 'pull_request' && secrets.ARCHIL_REGION || '' }} + ARCHIL_DISK_ID: ${{ github.event_name != 'pull_request' && secrets.ARCHIL_DISK_ID || '' }} + BL_API_KEY: ${{ github.event_name != 'pull_request' && secrets.BL_API_KEY || '' }} + BL_WORKSPACE: ${{ github.event_name != 'pull_request' && secrets.BL_WORKSPACE || '' }} + CLOUDFLARE_SANDBOX_URL: ${{ github.event_name != 'pull_request' && secrets.CLOUDFLARE_SANDBOX_URL || '' }} + CLOUDFLARE_SANDBOX_SECRET: ${{ github.event_name != 'pull_request' && secrets.CLOUDFLARE_SANDBOX_SECRET || '' }} + CSB_API_KEY: ${{ github.event_name != 'pull_request' && secrets.CSB_API_KEY || '' }} + DAYTONA_API_KEY: ${{ github.event_name != 'pull_request' && secrets.DAYTONA_API_KEY || '' }} + DECLAW_API_KEY: ${{ github.event_name != 'pull_request' && secrets.DECLAW_API_KEY || '' }} + E2B_API_KEY: ${{ github.event_name != 'pull_request' && secrets.E2B_API_KEY || '' }} + HOPX_API_KEY: ${{ github.event_name != 'pull_request' && secrets.HOPX_API_KEY || '' }} + ISORUN_API_KEY: ${{ github.event_name != 'pull_request' && secrets.ISORUN_API_KEY || '' }} + MODAL_TOKEN_ID: ${{ github.event_name != 'pull_request' && secrets.MODAL_TOKEN_ID || '' }} + MODAL_TOKEN_SECRET: ${{ github.event_name != 'pull_request' && secrets.MODAL_TOKEN_SECRET || '' }} + NORTHFLANK_TOKEN: ${{ github.event_name != 'pull_request' && secrets.NORTHFLANK_TOKEN || '' }} + NORTHFLANK_PROJECT_ID: ${{ github.event_name != 'pull_request' && secrets.NORTHFLANK_PROJECT_ID || '' }} + RUNLOOP_API_KEY: ${{ github.event_name != 'pull_request' && secrets.RUNLOOP_API_KEY || '' }} + TENSORLAKE_API_KEY: ${{ github.event_name != 'pull_request' && secrets.TENSORLAKE_API_KEY || '' }} + UPSTASH_BOX_API_KEY: ${{ github.event_name != 'pull_request' && secrets.UPSTASH_BOX_API_KEY || '' }} + VERCEL_TOKEN: ${{ github.event_name != 'pull_request' && secrets.VERCEL_TOKEN || '' }} + VERCEL_TEAM_ID: ${{ github.event_name != 'pull_request' && secrets.VERCEL_TEAM_ID || '' }} + VERCEL_PROJECT_ID: ${{ github.event_name != 'pull_request' && secrets.VERCEL_PROJECT_ID || '' }} INPUT_SAMPLES: ${{ github.event.inputs.samples }} INPUT_DURATION_SECONDS: ${{ github.event.inputs['duration-seconds'] || '600' }} INPUT_INTERVAL_SECONDS: ${{ github.event.inputs['interval-seconds'] || '30' }} diff --git a/src/merge-results.ts b/src/merge-results.ts index af64db47..9956ffd9 100644 --- a/src/merge-results.ts +++ b/src/merge-results.ts @@ -52,7 +52,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|browser|browser-throughput]'); + console.error('Usage: tsx src/merge-results.ts --input [--mode storage|browser|browser-throughput|sandbox-reliability|sandbox-features]'); process.exit(1); } @@ -523,6 +523,11 @@ async function mainSandboxReliability(resultsSubdir = 'sandbox-reliability') { } const deduped = Array.from(seen.values()).map(e => e.result); + if (deduped.length === 0) { + console.error(`No ${resultsSubdir} results found in ${inputDir}`); + process.exit(1); + } + console.log(`\nMerging ${deduped.length} provider results for mode: ${resultsSubdir}`); printReliabilityResultsTable(deduped); diff --git a/src/run.ts b/src/run.ts index ff215576..a02db87e 100644 --- a/src/run.ts +++ b/src/run.ts @@ -47,6 +47,20 @@ function getArgValue(args: string[], flag: string): string | undefined { return idx !== -1 && idx + 1 < args.length ? args[idx + 1] : undefined; } +function parsePositiveIntegerArg(value: string | undefined, flag: string): number | undefined { + if (value === undefined) return undefined; + if (!/^\d+$/.test(value)) { + console.error(`Invalid ${flag}: expected a positive integer, got "${value}"`); + process.exit(1); + } + const parsed = Number(value); + if (!Number.isSafeInteger(parsed) || parsed <= 0) { + console.error(`Invalid ${flag}: expected a positive integer, got "${value}"`); + process.exit(1); + } + return parsed; +} + function timestampForFilename(): string { return new Date().toISOString().replace(/[-:]/g, '').replace(/\.\d{3}Z$/, 'Z'); } @@ -297,9 +311,11 @@ async function runBrowserThroughput(toRun: typeof throughputProviders): Promise< } async function runReliability(toRun: typeof providers): Promise { - const samples = samplesArg ? parseInt(samplesArg, 10) : undefined; - const durationMs = !samples && durationSecondsArg ? parseInt(durationSecondsArg, 10) * 1000 : undefined; - const intervalMs = intervalSecondsArg ? parseInt(intervalSecondsArg, 10) * 1000 : undefined; + const samples = parsePositiveIntegerArg(samplesArg, '--samples'); + const durationSeconds = parsePositiveIntegerArg(durationSecondsArg, '--duration-seconds'); + const intervalSeconds = parsePositiveIntegerArg(intervalSecondsArg, '--interval-seconds'); + const durationMs = !samples && durationSeconds ? durationSeconds * 1000 : undefined; + const intervalMs = intervalSeconds ? intervalSeconds * 1000 : undefined; console.log('\n' + '='.repeat(70)); console.log(' MODE: SANDBOX RELIABILITY'); diff --git a/src/sandbox/reliability.ts b/src/sandbox/reliability.ts index a059df29..77141514 100644 --- a/src/sandbox/reliability.ts +++ b/src/sandbox/reliability.ts @@ -265,7 +265,8 @@ function buildProbeCommand(markerToken: string, previousMarkerToken?: string, pr 'previous_fs_marker_value=', 'for p in "$fs_marker" "$var_tmp_marker"; do if [ -f "$p" ]; then previous_fs_marker_path=$p; previous_fs_marker_value=$(tr -d "\\n" < "$p" 2>/dev/null || true); break; fi; done', 'process_marker_seen=false', - 'if [ -n "$previous_process_marker" ] && command -v ps >/dev/null 2>&1; then ps_output=$(ps axww 2>/dev/null || ps auxww 2>/dev/null || true); case "$ps_output" in *"$previous_process_marker"*) process_marker_seen=true;; esac; fi', + 'process_isolation_ok=', + 'if [ -n "$previous_process_marker" ] && command -v ps >/dev/null 2>&1; then ps_output=$(ps axww 2>/dev/null || ps auxww 2>/dev/null || true); case "$ps_output" in *"$previous_process_marker"*) process_marker_seen=true;; esac; if [ "$process_marker_seen" = false ]; then process_isolation_ok=true; else process_isolation_ok=false; fi; fi', 'tmp_write=false; tmp_read=false; tmp_file=/tmp/computesdk_reliability_probe_$$; printf "%s" "$token" > "$tmp_file" 2>/dev/null && tmp_write=true; [ "$(cat "$tmp_file" 2>/dev/null)" = "$token" ] && tmp_read=true; rm -f "$tmp_file" 2>/dev/null', 'var_tmp_write=false; var_tmp_file=/var/tmp/computesdk_reliability_probe_$$; printf "%s" "$token" > "$var_tmp_file" 2>/dev/null && var_tmp_write=true; rm -f "$var_tmp_file" 2>/dev/null', 'home_write=false; home_file="$HOME/.computesdk_reliability_probe_$$"; if [ -n "$HOME" ]; then printf "%s" "$token" > "$home_file" 2>/dev/null && home_write=true; rm -f "$home_file" 2>/dev/null; fi', @@ -323,7 +324,7 @@ function buildProbeCommand(markerToken: string, previousMarkerToken?: string, pr 'printf "dnsExample=%s\\n" "$dns_example"', 'printf "httpExample=%s\\n" "$http_example"', 'printf "fsIsolationOk=%s\\n" "$([ -z "$previous_fs_marker_path" ] && printf true || printf false)"', - 'printf "processIsolationOk=%s\\n" "$([ "$process_marker_seen" = false ] && printf true || printf false)"', + 'if [ -n "$process_isolation_ok" ]; then printf "processIsolationOk=%s\\n" "$process_isolation_ok"; fi', 'printf "previousFsMarkerPath=%s\\n" "$previous_fs_marker_path"', 'printf "previousFsMarkerValue=%s\\n" "$previous_fs_marker_value"', 'printf "memTotalKb=%s\\n" "$mem_total_kb"',