From dff436d3d59acf3b6b4200a8601031b49e19aeeb Mon Sep 17 00:00:00 2001 From: Tom Beckenham <34339192+tombeckenham@users.noreply.github.com> Date: Thu, 13 Aug 2026 15:54:51 +1000 Subject: [PATCH] feat(producer): fan out heygen-promo capture across Cloudflare Sandboxes Isolated remote Chromes beat the 25% wall-clock goal; extra local workers share one SwiftShader and get slower. --- .fallowrc.jsonc | 7 + .../producer/cloudflare-sandbox/.gitignore | 6 + .../producer/cloudflare-sandbox/Dockerfile | 62 ++++ packages/producer/cloudflare-sandbox/bun.lock | 208 +++++++++++ .../producer/cloudflare-sandbox/package.json | 16 + .../producer/cloudflare-sandbox/src/worker.ts | 150 ++++++++ .../cloudflare-sandbox/wrangler.jsonc | 33 ++ packages/producer/package.json | 1 + .../producer/src/cloudflareSandboxBench.ts | 342 ++++++++++++++++++ packages/producer/src/distributed.ts | 14 + packages/producer/src/renderRequest.ts | 1 + .../distributed/cloudflareSandbox.test.ts | 184 ++++++++++ .../services/distributed/cloudflareSandbox.ts | 265 ++++++++++++++ .../producer/src/services/distributed/plan.ts | 2 +- .../distributed/publicExports.test.ts | 3 + .../services/distributed/renderChunkCli.ts | 38 ++ .../distributed/renderConfigValidation.ts | 9 +- packages/producer/tests/perf/README.md | 3 + packages/producer/tests/perf/goal.md | 156 ++++++++ 19 files changed, 1498 insertions(+), 2 deletions(-) create mode 100644 packages/producer/cloudflare-sandbox/.gitignore create mode 100644 packages/producer/cloudflare-sandbox/Dockerfile create mode 100644 packages/producer/cloudflare-sandbox/bun.lock create mode 100644 packages/producer/cloudflare-sandbox/package.json create mode 100644 packages/producer/cloudflare-sandbox/src/worker.ts create mode 100644 packages/producer/cloudflare-sandbox/wrangler.jsonc create mode 100644 packages/producer/src/cloudflareSandboxBench.ts create mode 100644 packages/producer/src/services/distributed/cloudflareSandbox.test.ts create mode 100644 packages/producer/src/services/distributed/cloudflareSandbox.ts create mode 100644 packages/producer/src/services/distributed/renderChunkCli.ts create mode 100644 packages/producer/tests/perf/goal.md diff --git a/.fallowrc.jsonc b/.fallowrc.jsonc index 2e47f6d433..628a54934d 100644 --- a/.fallowrc.jsonc +++ b/.fallowrc.jsonc @@ -13,6 +13,8 @@ "packages/producer/src/perf-gate.ts", "packages/producer/src/runtime-conformance.ts", "packages/producer/src/benchmark.ts", + "packages/producer/src/cloudflareSandboxBench.ts", + "packages/producer/src/services/distributed/renderChunkCli.ts", "packages/producer/scripts/generate-font-data.ts", "packages/producer/we-render.mjs", "packages/producer/scripts/validate-fast-video.ts", @@ -62,6 +64,8 @@ "packages/engine/spikes/**", "packages/producer/de-*.mjs", "packages/producer/tests/**", + // Nested Cloudflare worker + generated sandbox image artifacts. + "packages/producer/cloudflare-sandbox/**", "packages/player/tests/**", "packages/engine/tests/**", "skills/**/test-corpus/**", @@ -790,6 +794,9 @@ // stage's runCompileStage under the cyclo/cognitive thresholds. "packages/producer/src/server.ts", "packages/producer/src/services/distributed/plan.ts", + // Standalone sandbox bench harness: CLI parse + retry loops trip CRAP + // without coverage mapping. Not library code. + "packages/producer/src/cloudflareSandboxBench.ts", // Sibling-surface fix (PR #2529 R2): lambda.ts's top-level `run` // (cyclo 39, CRAP 1560) is the big subcommand switch that pre-dates // this PR. The change threads two additional variables through the diff --git a/packages/producer/cloudflare-sandbox/.gitignore b/packages/producer/cloudflare-sandbox/.gitignore new file mode 100644 index 0000000000..3a06ee273c --- /dev/null +++ b/packages/producer/cloudflare-sandbox/.gitignore @@ -0,0 +1,6 @@ +node_modules/ +chunk-worker.mjs +.wrangler/ +dist/ +.dev.vars +runtime/ diff --git a/packages/producer/cloudflare-sandbox/Dockerfile b/packages/producer/cloudflare-sandbox/Dockerfile new file mode 100644 index 0000000000..eae04b33c2 --- /dev/null +++ b/packages/producer/cloudflare-sandbox/Dockerfile @@ -0,0 +1,62 @@ +FROM docker.io/cloudflare/sandbox:0.12.5 + +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates \ + curl \ + ffmpeg \ + libgbm1 \ + libnss3 \ + libatk-bridge2.0-0 \ + libdrm2 \ + libxcomposite1 \ + libxdamage1 \ + libxrandr2 \ + libasound2 \ + libpangocairo-1.0-0 \ + libxshmfence1 \ + libgtk-3-0 \ + fonts-liberation \ + fonts-dejavu-core \ + fontconfig \ + && rm -rf /var/lib/apt/lists/* \ + && fc-cache -fv + +# Ubuntu jammy has no `chromium` apt package. Install the same +# chrome-headless-shell the producer regression image uses. +RUN npx --yes @puppeteer/browsers install chrome-headless-shell@148.0.7778.167 \ + --path /opt/hf/chrome \ + && find /opt/hf/chrome -name "chrome-headless-shell" -type f + +ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true +ENV CONTAINER=true +ENV CHROME_PATH=/opt/hf/chrome +# resolveHeadlessShellPath also searches ~/.cache/puppeteer; keep a copy there. +ENV PRODUCER_HEADLESS_SHELL_PATH=/opt/hf/chrome + +RUN mkdir -p /opt/hf /workspace /root/.cache \ + && ln -sfn /opt/hf/chrome /root/.cache/puppeteer + +COPY chunk-worker.mjs /opt/hf/chunk-worker.mjs +COPY runtime/hyperframe.manifest.json /opt/hf/runtime/hyperframe.manifest.json +COPY runtime/hyperframe.runtime.iife.js /opt/hf/runtime/hyperframe.runtime.iife.js +ENV PRODUCER_HYPERFRAME_MANIFEST_PATH=/opt/hf/runtime/hyperframe.manifest.json +WORKDIR /opt/hf +RUN printf '%s\n' '{"name":"hf-chunk-worker","type":"module"}' > /opt/hf/package.json \ + && npm install --omit=dev ws \ + && test -d /opt/hf/node_modules/ws + +# Wrapper so `chrome-headless-shell --version` works in /smoke. +RUN printf '%s\n' '#!/bin/sh' 'exec "$(find /opt/hf/chrome -name chrome-headless-shell -type f | head -n1)" "$@"' \ + > /usr/local/bin/chrome-headless-shell \ + && chmod +x /usr/local/bin/chrome-headless-shell \ + && printf '%s\n' '#!/bin/sh' 'exec /usr/local/bin/chrome-headless-shell "$@"' \ + > /usr/local/bin/chromium \ + && chmod +x /usr/local/bin/chromium + +ENV PUPPETEER_EXECUTABLE_PATH=/usr/local/bin/chrome-headless-shell +ENV PRODUCER_HEADLESS_SHELL_PATH=/usr/local/bin/chrome-headless-shell +ENV NODE_PATH=/opt/hf/node_modules + +RUN curl -fsSL https://bun.sh/install | BUN_INSTALL=/usr/local bash -s "bun-v1.3.14" + +EXPOSE 8080 diff --git a/packages/producer/cloudflare-sandbox/bun.lock b/packages/producer/cloudflare-sandbox/bun.lock new file mode 100644 index 0000000000..c65b819e8c --- /dev/null +++ b/packages/producer/cloudflare-sandbox/bun.lock @@ -0,0 +1,208 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "hf-render-sandbox", + "dependencies": { + "@cloudflare/sandbox": "^0.12.5", + }, + "devDependencies": { + "wrangler": "^4.83.0", + }, + }, + }, + "packages": { + "@cloudflare/containers": ["@cloudflare/containers@0.3.7", "", {}, "sha512-DM9dm3FnIBSyiSJ1FLavKwl/lk3oAmTaynCzZQ9pZR0ncRPquSxkxd8Nu2MFILxmDDsPkxKsSNEh9mHHMty4Fw=="], + + "@cloudflare/kv-asset-handler": ["@cloudflare/kv-asset-handler@0.5.0", "", {}, "sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg=="], + + "@cloudflare/sandbox": ["@cloudflare/sandbox@0.12.5", "", { "dependencies": { "@cloudflare/containers": "^0.3.5", "aws4fetch": "^1.0.20", "capnweb": "^0.8.0", "hono": "^4.13.0" }, "peerDependencies": { "@openai/agents": "^0.3.3", "@opencode-ai/sdk": "^1.1.40", "@xterm/xterm": ">=5.0.0" }, "optionalPeers": ["@openai/agents", "@opencode-ai/sdk", "@xterm/xterm"] }, "sha512-D+EvqrpnJHmEaIbFMf5HefavBLMEbUcz7XdhbBfaJzFMma0sjKoN8JuQ5VNlMmPvwKx+Rj7mw5djSg9YMZVbeA=="], + + "@cloudflare/unenv-preset": ["@cloudflare/unenv-preset@2.16.1", "", { "peerDependencies": { "unenv": "2.0.0-rc.24", "workerd": ">1.20260305.0 <2.0.0-0" }, "optionalPeers": ["workerd"] }, "sha512-ECxObrMfyTl5bhQf/lZCXwo5G6xX9IAUo+nDMKK4SZ8m4Jvvxp52vilxyySSWh2YTZz8+HQ07qGH/2rEom1vDw=="], + + "@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20260811.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-i5jqz+ywtOefr0AJbiAc8qxBLfSim/B0WJG7aW3B+pWnoVfMJdUQvi+BWcFKZJ0MoCci3KadTx6g31VfuEEqpQ=="], + + "@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20260811.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-NoOUM/nvaDdm2Onlnz33FikWjtatzulNtvwvy4xs0IrHaTCHwC0c8NwIt6s+AI13FkDs02/vm2I3GTPLCT9+hQ=="], + + "@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20260811.1", "", { "os": "linux", "cpu": "x64" }, "sha512-sdYq2jL1AD1supa3fsi5O4zTB28wSjvTHj7Migh6/ts8EROPdvrSwv+rdGHhv8HJNAz/wbIAY3wZsi1Rw4uUIg=="], + + "@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20260811.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-RIRv4shbu1kg05sD+DHTpSFCNnb5Dl2SkPDMUykqZa508tkPqe7VVw7gO0Q5msTBGyL0FfFrLuRxwwfA8u5Sow=="], + + "@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20260811.1", "", { "os": "win32", "cpu": "x64" }, "sha512-g6VquwjASlYAibcNW/0E6Zszht4qLkmnXOGwIjjRHl2A0Qz48kVeMcGvyH6eA0G9U3OzZojjYFpP+YeyQmmdjw=="], + + "@cspotcode/source-map-support": ["@cspotcode/source-map-support@0.8.1", "", { "dependencies": { "@jridgewell/trace-mapping": "0.3.9" } }, "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw=="], + + "@emnapi/runtime": ["@emnapi/runtime@1.11.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA=="], + + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.28.1", "", { "os": "android", "cpu": "arm" }, "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.28.1", "", { "os": "android", "cpu": "arm64" }, "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.28.1", "", { "os": "android", "cpu": "x64" }, "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.28.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.28.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.28.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.28.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.28.1", "", { "os": "linux", "cpu": "arm" }, "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.28.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.28.1", "", { "os": "linux", "cpu": "ia32" }, "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.28.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.28.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.28.1", "", { "os": "linux", "cpu": "x64" }, "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA=="], + + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.28.1", "", { "os": "none", "cpu": "x64" }, "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg=="], + + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.28.1", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.28.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw=="], + + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.28.1", "", { "os": "sunos", "cpu": "x64" }, "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.28.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.28.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.1", "", { "os": "win32", "cpu": "x64" }, "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A=="], + + "@img/colour": ["@img/colour@1.1.0", "", {}, "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ=="], + + "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.35.2", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.3.1" }, "os": "darwin", "cpu": "arm64" }, "sha512-eEieHsMksAW4IiO5NzauESRl2D2qz3J/kwUxUrSfV06A93eEaRfMpHXyUb1mAqrR7i8U9A0GRqE9pjn6u1Jjpg=="], + + "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.35.2", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.3.1" }, "os": "darwin", "cpu": "x64" }, "sha512-BaktuGPCeHJMARpodR8jK4uKiZrPAy9WrfQW0sdI37clracq8Bp01AYS3SZgi5FS/y5twa9t4+LIuuxQjqRrWw=="], + + "@img/sharp-freebsd-wasm32": ["@img/sharp-freebsd-wasm32@0.35.2", "", { "dependencies": { "@img/sharp-wasm32": "0.35.2" }, "os": "freebsd" }, "sha512-YoAxdnd8hPUkvLHd3bWY+YA8nw3xM/RyRopYucNsWHVSan8NLVM3X2volsfoRDcXdUJPg6tXahSd7HXPK7lRnw=="], + + "@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.3.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-4V/M3roRMTYjiwZY9IOVQOE8OyeCxFAkYmyZDrZl51uOKjibm3oeEJ4WAmLxutAfzFbC9jqUiPs2gbnGflH+7g=="], + + "@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.3.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-c0/DxItpJv2+dGhgycJBBgotdqruGYDvA79drdh0MD1dFpy7JzJ/PlXwi1H4rFf0eTy8tgbI91aHDnZIceY3jQ=="], + + "@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.3.1", "", { "os": "linux", "cpu": "arm" }, "sha512-aGGy9aWzXgHBG7HNyQPWorZthlp7+x6fDRoPAQbGO3ThcttuTyKIx3NuSHb6zb4gBNq6/yNn9f1cy9nFKS/Vmg=="], + + "@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.3.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-JznefmcK9j1JKPz8AkQDh89kjojubyfOasWBPKfzMIhPwsgDy9evpE/naJTXXXmghS1iFwR8u/kTwh/I2/+GCw=="], + + "@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.3.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-1EkwGNCZk6iWNCMWqrvdJ+r1j0PT1zIz60CNPhYnJlK/zyeWqlsPZIe+ocBVqPF8k/Ssee/NCk+tE9Ryrko6ng=="], + + "@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.3.1", "", { "os": "linux", "cpu": "none" }, "sha512-Ilays+w2bXdnxzxtQdmXR62u8o8GYa3eL4+Gr+1KiE4xperMZUslRaVPJwwPkzlHEjGfXAfRVAa/7CYCtSqsBw=="], + + "@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.3.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-VfBwVHQTbRoj4XlpA/KLZ7ltgMpz+4WSejFzQ+GnoImjo1PtEJ59QB2qR1xQEeRPYIkNrPIm2L4cICMvz4C2ew=="], + + "@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.3.1", "", { "os": "linux", "cpu": "x64" }, "sha512-+c8ukgwU62DS54nCAjw7keOfHUkmr0B5QHEdcOqRnodF/MNXJbVI8Eopoj4B/0H8Asr65I+A4Amrn7a85/md6A=="], + + "@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.3.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-qlKb/pwbkAi1WMsJrYHk7CuDrd12s27U2QnRhFYUoJNrRCmkosMTttuRFat/DDB3IlDm5qE1TJgZ4JDnHX8Ldw=="], + + "@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.3.1", "", { "os": "linux", "cpu": "x64" }, "sha512-yO21HwoUVLN8Qa+/SBjQLMYwBWAVJjeGPNe+hc0OUeMeifEtJqu5a1c4HayE1nNpDih9y3/KkoltfkDodmKAlg=="], + + "@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.35.2", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.3.1" }, "os": "linux", "cpu": "arm" }, "sha512-SE4kzF2mepn6z+6E7L6lsV8FzuLL6IPQdyX8ZiwROAG/G8td+hP/m7FsFPwidtrF19gvajuC9l6TxAVcsA4S7A=="], + + "@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.35.2", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.3.1" }, "os": "linux", "cpu": "arm64" }, "sha512-af12Pnd0ZGu2HfP8NayB0kk6eC/lrfbQE6HlR4jD+34wdJ1Vw9TF6TMn6ZvffT+WgqVsl0hRbmNvz2u/23VmwA=="], + + "@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.35.2", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.3.1" }, "os": "linux", "cpu": "ppc64" }, "sha512-hYSBm7zcNtDCozCxQHYZJiu63b/bXsgRZuOxCIBZsStMM9Vap47iFHdbX4kCvQsblPB/k+clhELpdQJHQLSHvg=="], + + "@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.35.2", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.3.1" }, "os": "linux", "cpu": "none" }, "sha512-qQt0Kc13+Hoan/Awq/qMSQw3L+RI1NCRPgD5cUJ/1WSSmIoysLOc72jlRM3E0OHN9Yr313jgeQ2T+zW+F03QFA=="], + + "@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.35.2", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.3.1" }, "os": "linux", "cpu": "s390x" }, "sha512-E4fLLfRPzDLlEeDaTzI98OFLcv++WL5ChLLMwPoVd0CIoZQqupBSNbOisPL5am9XsbQ9T84+iiMpUvbFtkunbA=="], + + "@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.35.2", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.3.1" }, "os": "linux", "cpu": "x64" }, "sha512-gi0zFJJRLswfCZmHtJdikXPOc5u7qamSOS3NHedLqLd4W8Q0NqjdBr6TTRIgsfFjqfTsHFgdfvJ9LwqSgcHiAA=="], + + "@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.35.2", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.3.1" }, "os": "linux", "cpu": "arm64" }, "sha512-siWbOW1u6HFnFLrp0waKyW7VEf7jYvcDWdrXEFa8AkdAQgEvuu5Fz8/Y70w9EeqAdwDtfU012BhEHHaDqvQNzg=="], + + "@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.35.2", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.3.1" }, "os": "linux", "cpu": "x64" }, "sha512-YBqMMcjDi4QGYiSn4vNOYBhmlC4z5AXqkOUUqI2e0AFA4urNv4ESgOgwNl3K+4etQhha0twXlzeF20bbULm9Yg=="], + + "@img/sharp-wasm32": ["@img/sharp-wasm32@0.35.2", "", { "dependencies": { "@emnapi/runtime": "^1.11.1" } }, "sha512-Mrv4JQNYVQ94xH+jzZ9r+gowleN8mv2FTgKT+PI6bx5C0G8TdNYndu161pg2i7uoBwxy2ImPMHrJOM2LZef7Bw=="], + + "@img/sharp-webcontainers-wasm32": ["@img/sharp-webcontainers-wasm32@0.35.2", "", { "dependencies": { "@img/sharp-wasm32": "0.35.2" }, "cpu": "none" }, "sha512-QNV27pxs9wpApEiCfvHM1RDoP1w1+2KrUWWDPEhEwg+latvOrfuhWrHWZKwdSFwU6jh3myjw/yOCRsUIuOft3g=="], + + "@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.35.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-BiVRYc/t6/Vl3e1hBx0hugG4oN9Pydf4fgMSpxTQJmwGUg/YoXTWHiFeRymHfCZzifxu4F4rpk/I67D0LQ20wQ=="], + + "@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.35.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-YYEhx9PImCC7T0tI8JDMi4DB9LwLCXCU5OWNYEXAxh5Q1ShKkyC6byxzoBJ3gEFDnH2lQckWuDe70G7mB2XJog=="], + + "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.35.2", "", { "os": "win32", "cpu": "x64" }, "sha512-imoOyBcoM/iiUr4J6VPpCNjPnjvP/Gks95898yB8YqoGGYmHYbOyCuNv9FMhFgtaiHFGbHW8bxKqRV6VjtXThQ=="], + + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.9", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" } }, "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ=="], + + "@poppinss/colors": ["@poppinss/colors@4.1.6", "", { "dependencies": { "kleur": "^4.1.5" } }, "sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg=="], + + "@poppinss/dumper": ["@poppinss/dumper@0.6.5", "", { "dependencies": { "@poppinss/colors": "^4.1.5", "@sindresorhus/is": "^7.0.2", "supports-color": "^10.0.0" } }, "sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw=="], + + "@poppinss/exception": ["@poppinss/exception@1.2.3", "", {}, "sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw=="], + + "@sindresorhus/is": ["@sindresorhus/is@7.2.0", "", {}, "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw=="], + + "@speed-highlight/core": ["@speed-highlight/core@1.2.24", "", {}, "sha512-qeW2e1l78afw8VhRPfPQ1Gjj+KU5XFQ/OFV5ti6eTa9bruO7mJyZtA4vw0ofqmA3tKCkROE9xLk3VZoeRc98nw=="], + + "aws4fetch": ["aws4fetch@1.0.20", "", {}, "sha512-/djoAN709iY65ETD6LKCtyyEI04XIBP5xVvfmNxsEP0uJB5tyaGBztSryRr4HqMStr9R06PisQE7m9zDTXKu6g=="], + + "blake3-wasm": ["blake3-wasm@2.1.5", "", {}, "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g=="], + + "capnweb": ["capnweb@0.8.0", "", {}, "sha512-BK/TuXUiyfLSKsmjojn70yN7oYG/JJzoURZ3tckjg5Zj2KcygPm0A5jyOlswK7SYB4f0Gh9tt+RZ132b80iLfA=="], + + "cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="], + + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + + "error-stack-parser-es": ["error-stack-parser-es@1.0.5", "", {}, "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA=="], + + "esbuild": ["esbuild@0.28.1", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.1", "@esbuild/android-arm": "0.28.1", "@esbuild/android-arm64": "0.28.1", "@esbuild/android-x64": "0.28.1", "@esbuild/darwin-arm64": "0.28.1", "@esbuild/darwin-x64": "0.28.1", "@esbuild/freebsd-arm64": "0.28.1", "@esbuild/freebsd-x64": "0.28.1", "@esbuild/linux-arm": "0.28.1", "@esbuild/linux-arm64": "0.28.1", "@esbuild/linux-ia32": "0.28.1", "@esbuild/linux-loong64": "0.28.1", "@esbuild/linux-mips64el": "0.28.1", "@esbuild/linux-ppc64": "0.28.1", "@esbuild/linux-riscv64": "0.28.1", "@esbuild/linux-s390x": "0.28.1", "@esbuild/linux-x64": "0.28.1", "@esbuild/netbsd-arm64": "0.28.1", "@esbuild/netbsd-x64": "0.28.1", "@esbuild/openbsd-arm64": "0.28.1", "@esbuild/openbsd-x64": "0.28.1", "@esbuild/openharmony-arm64": "0.28.1", "@esbuild/sunos-x64": "0.28.1", "@esbuild/win32-arm64": "0.28.1", "@esbuild/win32-ia32": "0.28.1", "@esbuild/win32-x64": "0.28.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw=="], + + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "hono": ["hono@4.13.1", "", {}, "sha512-kdJoFVv2xmayw6cY09H7AbMJMt8Jn5jdlEdXsP7AGBdF2DIptVlKlOLKXP41yPip4/a3yQPv9gVcJYI8YY04dw=="], + + "kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="], + + "miniflare": ["miniflare@5.20260811.0-alpha", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "sharp": "0.35.2", "undici": "7.29.0", "workerd": "1.20260811.1", "ws": "8.21.0", "youch": "4.1.0-beta.10" } }, "sha512-sypXsD5fjY88fZNedPqnwrwR1dwfnfbfW7MfvMyIfPJdtRiCCOpUnjWGeFVYYZ+0fQVICye6Juu+vZgzTEx8XA=="], + + "path-to-regexp": ["path-to-regexp@6.3.0", "", {}, "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ=="], + + "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + + "semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], + + "sharp": ["sharp@0.35.2", "", { "dependencies": { "@img/colour": "^1.1.0", "detect-libc": "^2.1.2", "semver": "^7.8.4" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.35.2", "@img/sharp-darwin-x64": "0.35.2", "@img/sharp-freebsd-wasm32": "0.35.2", "@img/sharp-libvips-darwin-arm64": "1.3.1", "@img/sharp-libvips-darwin-x64": "1.3.1", "@img/sharp-libvips-linux-arm": "1.3.1", "@img/sharp-libvips-linux-arm64": "1.3.1", "@img/sharp-libvips-linux-ppc64": "1.3.1", "@img/sharp-libvips-linux-riscv64": "1.3.1", "@img/sharp-libvips-linux-s390x": "1.3.1", "@img/sharp-libvips-linux-x64": "1.3.1", "@img/sharp-libvips-linuxmusl-arm64": "1.3.1", "@img/sharp-libvips-linuxmusl-x64": "1.3.1", "@img/sharp-linux-arm": "0.35.2", "@img/sharp-linux-arm64": "0.35.2", "@img/sharp-linux-ppc64": "0.35.2", "@img/sharp-linux-riscv64": "0.35.2", "@img/sharp-linux-s390x": "0.35.2", "@img/sharp-linux-x64": "0.35.2", "@img/sharp-linuxmusl-arm64": "0.35.2", "@img/sharp-linuxmusl-x64": "0.35.2", "@img/sharp-webcontainers-wasm32": "0.35.2", "@img/sharp-win32-arm64": "0.35.2", "@img/sharp-win32-ia32": "0.35.2", "@img/sharp-win32-x64": "0.35.2" } }, "sha512-FVtFjtBCMiJS6yb5CX7Sop45WFMpeGw6oRKuJnXYgf/f1ms/D7LE/ZUSNxnW7rZ/dbslQWYkoqFHGPaDBtaK4w=="], + + "supports-color": ["supports-color@10.2.2", "", {}, "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g=="], + + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "undici": ["undici@7.29.0", "", {}, "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw=="], + + "unenv": ["unenv@2.0.0-rc.24", "", { "dependencies": { "pathe": "^2.0.3" } }, "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw=="], + + "workerd": ["workerd@1.20260811.1", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20260811.1", "@cloudflare/workerd-darwin-arm64": "1.20260811.1", "@cloudflare/workerd-linux-64": "1.20260811.1", "@cloudflare/workerd-linux-arm64": "1.20260811.1", "@cloudflare/workerd-windows-64": "1.20260811.1" }, "bin": { "workerd": "bin/workerd" } }, "sha512-kh+FFm55JQ4ssxhHZV9VPdMQq3D1nHxNJgwxMtWGD4dGppJvLySdguTRDKgeNTvgq6heSz+6TTXyPSDGj8Yllw=="], + + "wrangler": ["wrangler@4.122.0", "", { "dependencies": { "@cloudflare/kv-asset-handler": "0.5.0", "@cloudflare/unenv-preset": "2.16.1", "blake3-wasm": "2.1.5", "esbuild": "0.28.1", "miniflare": "5.20260811.0-alpha", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.24", "workerd": "1.20260811.1" }, "optionalDependencies": { "fsevents": "2.3.3" }, "peerDependencies": { "@cloudflare/workers-types": "^5.20260811.1" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js", "cf-wrangler": "bin/cf-wrangler.js" } }, "sha512-qkskzgQ76Y1qvVe5JARgvc3RISq6BC2rPoxQhFoKH1dKIwQc3GDFttQ/7m2OfeQ+tmQRzynv2dy/DXnxFCj2Lw=="], + + "ws": ["ws@8.21.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g=="], + + "youch": ["youch@4.1.0-beta.10", "", { "dependencies": { "@poppinss/colors": "^4.1.5", "@poppinss/dumper": "^0.6.4", "@speed-highlight/core": "^1.2.7", "cookie": "^1.0.2", "youch-core": "^0.3.3" } }, "sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ=="], + + "youch-core": ["youch-core@0.3.3", "", { "dependencies": { "@poppinss/exception": "^1.2.2", "error-stack-parser-es": "^1.0.5" } }, "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA=="], + } +} diff --git a/packages/producer/cloudflare-sandbox/package.json b/packages/producer/cloudflare-sandbox/package.json new file mode 100644 index 0000000000..801a1da465 --- /dev/null +++ b/packages/producer/cloudflare-sandbox/package.json @@ -0,0 +1,16 @@ +{ + "name": "hf-render-sandbox", + "private": true, + "type": "module", + "scripts": { + "build:chunk-worker": "bun build ../src/services/distributed/renderChunkCli.ts --outfile chunk-worker.mjs --target bun && mkdir -p runtime && cp ../../core/dist/hyperframe.manifest.json ../../core/dist/hyperframe.runtime.iife.js runtime/", + "dev": "bun run build:chunk-worker && wrangler dev", + "deploy": "bun run build:chunk-worker && wrangler deploy" + }, + "dependencies": { + "@cloudflare/sandbox": "^0.12.5" + }, + "devDependencies": { + "wrangler": "^4.83.0" + } +} diff --git a/packages/producer/cloudflare-sandbox/src/worker.ts b/packages/producer/cloudflare-sandbox/src/worker.ts new file mode 100644 index 0000000000..a464520245 --- /dev/null +++ b/packages/producer/cloudflare-sandbox/src/worker.ts @@ -0,0 +1,150 @@ +import { getSandbox, type Sandbox } from "@cloudflare/sandbox"; + +export { Sandbox } from "@cloudflare/sandbox"; + +export type Env = { + Sandbox: DurableObjectNamespace; +}; + +const MAX_INSTANCES = 10; + +function sandboxId(chunkIndex: number): string { + if (!Number.isInteger(chunkIndex) || chunkIndex < 0 || chunkIndex >= MAX_INSTANCES) { + throw new Error(`chunkIndex must be 0..${MAX_INSTANCES - 1}`); + } + return `hf-chunk-${chunkIndex}`; +} + +export default { + async fetch(request: Request, env: Env): Promise { + const url = new URL(request.url); + try { + if (request.method === "POST" && url.pathname === "/destroy") { + const chunkIndex = Number(url.searchParams.get("chunk") ?? "0"); + const sandbox = getSandbox(env.Sandbox, sandboxId(chunkIndex)); + await sandbox.destroy(); + return Response.json({ ok: true, destroyed: sandboxId(chunkIndex) }); + } + + if (request.method === "GET" && url.pathname === "/health") { + return Response.json({ + ok: true, + accountPinned: true, + maxInstances: MAX_INSTANCES, + }); + } + + if (request.method === "POST" && url.pathname === "/smoke") { + const chunkIndex = Number(url.searchParams.get("chunk") ?? "0"); + const sandbox = getSandbox(env.Sandbox, sandboxId(chunkIndex)); + const ffmpeg = await sandbox.exec("ffmpeg -version"); + const chrome = await sandbox.exec( + "sh -c 'chromium --version 2>/dev/null || chrome-headless-shell --version 2>/dev/null || echo missing'", + ); + return Response.json({ + ffmpeg: { ok: ffmpeg.success, out: ffmpeg.stdout.slice(0, 200) }, + chrome: { ok: chrome.success, out: chrome.stdout.slice(0, 200) }, + }); + } + + if (request.method === "POST" && url.pathname === "/plan") { + const chunkIndex = Number(url.searchParams.get("chunk") ?? "0"); + const sandbox = getSandbox(env.Sandbox, sandboxId(chunkIndex)); + const bytes = new Uint8Array(await request.arrayBuffer()); + await sandbox.writeFile("/tmp/project.tar.gz.b64", uint8ToBase64(bytes)); + const unpacked = await sandbox.exec( + "sh -c 'base64 -d /tmp/project.tar.gz.b64 > /tmp/project.tar.gz && rm -rf /workspace/project /workspace/plan && mkdir -p /workspace/project /workspace/plan && tar -xzf /tmp/project.tar.gz -C /workspace/project'", + ); + if (!unpacked.success) { + return Response.json( + { ok: false, error: unpacked.stderr || unpacked.stdout }, + { status: 500 }, + ); + } + const planned = await sandbox.exec( + "sh -c 'cd /opt/hf && mkdir -p node_modules && (test -d node_modules/ws || npm install --omit=dev ws) && NODE_PATH=/opt/hf/node_modules PRODUCER_HYPERFRAME_MANIFEST_PATH=/opt/hf/runtime/hyperframe.manifest.json HF_ACTION=plan HF_PROJECT_DIR=/workspace/project HF_PLAN_DIR=/workspace/plan HF_MAX_SANDBOXES=10 bun ./chunk-worker.mjs'", + ); + if (!planned.success) { + return Response.json( + { ok: false, error: planned.stderr || planned.stdout }, + { status: 500 }, + ); + } + const packed = await sandbox.exec( + "sh -c 'tar -czf /tmp/plan.tar.gz -C /workspace/plan . && base64 /tmp/plan.tar.gz'", + ); + if (!packed.success) { + return Response.json({ ok: false, error: packed.stderr }, { status: 500 }); + } + return Response.json({ + ok: true, + meta: planned.stdout.trim(), + planTarBase64: packed.stdout.replace(/\s+/g, ""), + }); + } + + if (request.method === "POST" && url.pathname === "/write-plan") { + const chunkIndex = Number(url.searchParams.get("chunk") ?? "0"); + const sandbox = getSandbox(env.Sandbox, sandboxId(chunkIndex)); + const bytes = new Uint8Array(await request.arrayBuffer()); + const b64 = uint8ToBase64(bytes); + await sandbox.writeFile("/tmp/plan.tar.gz.b64", b64); + const unpacked = await sandbox.exec( + "sh -c 'base64 -d /tmp/plan.tar.gz.b64 > /tmp/plan.tar.gz && rm -rf /workspace/plan && mkdir -p /workspace/plan && tar -xzf /tmp/plan.tar.gz -C /workspace/plan'", + ); + if (!unpacked.success) { + return Response.json( + { ok: false, error: unpacked.stderr || unpacked.stdout }, + { status: 500 }, + ); + } + return Response.json({ + ok: true, + sandboxId: sandboxId(chunkIndex), + bytes: bytes.byteLength, + }); + } + + if (request.method === "POST" && url.pathname === "/render-chunk") { + const chunkIndex = Number(url.searchParams.get("chunk") ?? "0"); + const sandbox = getSandbox(env.Sandbox, sandboxId(chunkIndex)); + const started = Date.now(); + const result = await sandbox.exec( + `sh -c 'cd /opt/hf && mkdir -p node_modules && (test -d node_modules/ws || npm install --omit=dev ws) && NODE_PATH=/opt/hf/node_modules PRODUCER_HYPERFRAME_MANIFEST_PATH=/opt/hf/runtime/hyperframe.manifest.json HF_ACTION=render HF_PLAN_DIR=/workspace/plan HF_CHUNK_INDEX=${chunkIndex} HF_OUTPUT=/workspace/chunk.mp4 bun ./chunk-worker.mjs'`, + ); + if (!result.success) { + return Response.json( + { ok: false, error: result.stderr || result.stdout, elapsedMs: Date.now() - started }, + { status: 500 }, + ); + } + const encoded = await sandbox.exec("base64 /workspace/chunk.mp4"); + if (!encoded.success) { + return Response.json({ ok: false, error: encoded.stderr }, { status: 500 }); + } + return Response.json({ + ok: true, + chunkIndex, + elapsedMs: Date.now() - started, + mp4Base64: encoded.stdout.replace(/\s+/g, ""), + }); + } + + return new Response("not found", { status: 404 }); + } catch (err) { + return Response.json( + { ok: false, error: err instanceof Error ? err.message : String(err) }, + { status: 500 }, + ); + } + }, +}; + +function uint8ToBase64(bytes: Uint8Array): string { + let binary = ""; + const chunk = 0x8000; + for (let i = 0; i < bytes.length; i += chunk) { + binary += String.fromCharCode(...bytes.subarray(i, i + chunk)); + } + return btoa(binary); +} diff --git a/packages/producer/cloudflare-sandbox/wrangler.jsonc b/packages/producer/cloudflare-sandbox/wrangler.jsonc new file mode 100644 index 0000000000..09402fe909 --- /dev/null +++ b/packages/producer/cloudflare-sandbox/wrangler.jsonc @@ -0,0 +1,33 @@ +{ + "name": "hf-render-sandbox", + "main": "src/worker.ts", + "compatibility_date": "2026-08-01", + "compatibility_flags": ["nodejs_compat"], + "account_id": "86bb57b655af7915f42b29dfc2d8807d", + "workers_dev": true, + "limits": { + "cpu_ms": 300000 + }, + "containers": [ + { + "class_name": "Sandbox", + "image": "./Dockerfile", + "instance_type": "standard-3", + "max_instances": 10 + } + ], + "durable_objects": { + "bindings": [ + { + "class_name": "Sandbox", + "name": "Sandbox" + } + ] + }, + "migrations": [ + { + "new_sqlite_classes": ["Sandbox"], + "tag": "v1" + } + ] +} diff --git a/packages/producer/package.json b/packages/producer/package.json index 02a9ffb7be..f5db3eaa5f 100644 --- a/packages/producer/package.json +++ b/packages/producer/package.json @@ -54,6 +54,7 @@ "check:runtime-conformance": "tsx src/runtime-conformance.ts", "benchmark": "tsx src/benchmark.ts", "bench:hdr": "tsx src/benchmark.ts --tags hdr", + "bench:sandbox": "tsx src/cloudflareSandboxBench.ts", "test": "bun run test:unit", "test:classification": "node --test scripts/test-classification.test.mjs && node scripts/check-test-classification.mjs", "test:unit": "bun run test:classification && node scripts/run-test-lane.mjs unit", diff --git a/packages/producer/src/cloudflareSandboxBench.ts b/packages/producer/src/cloudflareSandboxBench.ts new file mode 100644 index 0000000000..0ef533156b --- /dev/null +++ b/packages/producer/src/cloudflareSandboxBench.ts @@ -0,0 +1,342 @@ +#!/usr/bin/env tsx +/** + * Sandbox-backed 3-run bench for heygen-promo-preview-assets. + * + * bun src/cloudflareSandboxBench.ts --runs 3 --output-json + * + * Requires HF_SANDBOX_URL (wrangler dev or deployed worker). Without it, + * exits 2 after writing launch-unavailable.log next to --output-json. + */ +import { execFile, execFileSync } from "node:child_process"; +import { promisify } from "node:util"; +import { existsSync, mkdirSync, writeFileSync } from "node:fs"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { randomUUID } from "node:crypto"; +import type { ChunkResult } from "./services/distributed/renderChunk.js"; +import { + renderViaSandboxes, + sandboxRenderToPerfSummary, + type ChunkExecutor, + type SandboxRenderResult, +} from "./services/distributed/cloudflareSandbox.js"; + +const execFileAsync = promisify(execFile); +const scriptDir = dirname(fileURLToPath(import.meta.url)); +const producerRoot = resolve(scriptDir, ".."); +const defaultFixture = join(producerRoot, "tests/heygen-promo-preview-assets/src"); + +interface BenchArgs { + runs: number; + outputJson: string; + fixture: string; + workerUrl: string | null; +} + +interface RemotePlan { + meta: { chunkCount: number; totalFrames: number; fps: number }; + planTar: Buffer; +} + +type PerfSummary = ReturnType; + +function applyFlag(args: BenchArgs, flag: string, value: string): void { + if (flag === "--runs") args.runs = Number(value); + else if (flag === "--output-json") args.outputJson = resolve(value); + else if (flag === "--fixture") args.fixture = resolve(value); + else if (flag === "--worker-url") args.workerUrl = value; +} + +function parseArgs(argv: string[]): BenchArgs { + const args: BenchArgs = { + runs: 3, + outputJson: join(producerRoot, "tests/perf/sandbox-benchmark-results.json"), + fixture: defaultFixture, + workerUrl: process.env.HF_SANDBOX_URL ?? null, + }; + for (let i = 2; i < argv.length; i++) { + const value = argv[i + 1]; + if (value) { + applyFlag(args, argv[i]!, value); + if (argv[i]?.startsWith("--")) i += 1; + } + } + return args; +} + +function tarDirectory(dir: string, outFile: string): void { + execFileSync("tar", ["-czf", outFile, "-C", dir, "."], { stdio: "pipe" }); +} + +function errorText(err: unknown): string { + return err instanceof Error ? err.message : String(err); +} + +async function curlJson( + url: string, + bodyPath?: string, + maxTimeSec = 90, +): Promise> { + const args = ["-sS", "--max-time", String(maxTimeSec), "-X", "POST", url]; + if (bodyPath) args.push("--data-binary", `@${bodyPath}`); + const { stdout } = await execFileAsync("curl", args, { maxBuffer: 64 * 1024 * 1024 }); + return JSON.parse(stdout.toString("utf-8")) as Record; +} + +async function writePlanAttempt( + base: string, + chunkIndex: number, + planTarPath: string, +): Promise { + try { + const write = await curlJson(`${base}/write-plan?chunk=${chunkIndex}`, planTarPath, 90); + return write.ok === true ? null : JSON.stringify(write); + } catch (err) { + return errorText(err); + } +} + +async function writePlanWithRetry( + base: string, + chunkIndex: number, + planTarPath: string, +): Promise { + for (let attempt = 1; attempt <= 3; attempt++) { + const lastError = await writePlanAttempt(base, chunkIndex, planTarPath); + if (lastError === null) return; + console.log( + ` write-plan hf-chunk-${chunkIndex} retry ${attempt}: ${lastError.slice(0, 200)}`, + ); + if (attempt === 3) { + throw new Error( + `write-plan hf-chunk-${chunkIndex} failed after retries: ${lastError.slice(0, 500)}`, + ); + } + } +} + +function chunkResultFromPayload( + outputChunkPath: string, + payload: Record, +): ChunkResult { + mkdirSync(dirname(outputChunkPath), { recursive: true }); + writeFileSync(outputChunkPath, Buffer.from(payload.mp4Base64 as string, "base64")); + const elapsedMs = typeof payload.elapsedMs === "number" ? payload.elapsedMs : 0; + return { + outputPath: outputChunkPath, + outputKind: "file", + framesEncoded: 0, + sha256: "", + durationMs: elapsedMs, + planHashMs: 0, + sessionBootMs: 0, + captureStageMs: elapsedMs, + encodeStageMs: 0, + workers: 1, + perfPath: `${outputChunkPath}.json`, + }; +} + +async function renderChunkAttempt( + base: string, + chunkIndex: number, +): Promise> { + try { + return await curlJson(`${base}/render-chunk?chunk=${chunkIndex}`, undefined, 300); + } catch (err) { + return { ok: false, error: errorText(err) }; + } +} + +async function executeRemoteChunk( + workerUrl: string, + planTarPath: string, + chunkIndex: number, + sandboxId: string, + outputChunkPath: string, +): Promise { + const base = workerUrl.replace(/\/$/, ""); + let payload: Record = {}; + for (let attempt = 1; attempt <= 3; attempt++) { + console.log(` render-chunk ${sandboxId}${attempt > 1 ? ` retry ${attempt}` : ""}`); + payload = await renderChunkAttempt(base, chunkIndex); + if (payload.ok === true && typeof payload.mp4Base64 === "string") { + return chunkResultFromPayload(outputChunkPath, payload); + } + await writePlanWithRetry(base, chunkIndex, planTarPath); + } + throw new Error(`render-chunk ${sandboxId} failed: ${JSON.stringify(payload).slice(0, 1500)}`); +} + +function cloudflareExecutor(workerUrl: string, planTarPath: string): ChunkExecutor { + return ({ chunkIndex, sandboxId, outputChunkPath }) => + executeRemoteChunk(workerUrl, planTarPath, chunkIndex, sandboxId, outputChunkPath); +} + +async function probeWorker(url: string): Promise { + try { + const res = await fetch(`${url.replace(/\/$/, "")}/health`); + return res.ok; + } catch { + return false; + } +} + +async function planRemotely(workerUrl: string, projectTarPath: string): Promise { + const payload = await curlJson( + `${workerUrl.replace(/\/$/, "")}/plan?chunk=0`, + projectTarPath, + 90, + ); + if ( + payload.ok !== true || + typeof payload.planTarBase64 !== "string" || + typeof payload.meta !== "string" + ) { + throw new Error(`remote plan failed: ${JSON.stringify(payload).slice(0, 1500)}`); + } + return { + meta: JSON.parse(payload.meta) as RemotePlan["meta"], + planTar: Buffer.from(payload.planTarBase64, "base64"), + }; +} + +function writeUnavailableLog(outputJson: string, workerUrl: string | null): never { + const logPath = join(dirname(outputJson), "launch-unavailable.log"); + writeFileSync( + logPath, + [ + "Cloudflare sandbox worker is not reachable.", + `HF_SANDBOX_URL=${workerUrl ?? "(unset)"}`, + "Start `wrangler dev` in packages/producer/cloudflare-sandbox or deploy and set HF_SANDBOX_URL.", + `wrangler whoami / docker info should succeed before a remote run.`, + ].join("\n") + "\n", + "utf-8", + ); + console.error(`Sandbox worker unavailable. Wrote ${logPath}`); + process.exit(2); +} + +function reusedRemotePlan(planDir: string, remote: RemotePlan) { + return async () => ({ + planDir, + planHash: "remote", + chunkCount: remote.meta.chunkCount, + totalFrames: remote.meta.totalFrames, + fps: remote.meta.fps as 24 | 30 | 60, + width: 1920, + height: 1080, + format: "mp4" as const, + ffmpegVersion: "remote", + producerVersion: "remote", + }); +} + +async function runOnce( + workerUrl: string, + fixture: string, + projectTarPath: string, + runDir: string, +): Promise { + const planDir = join(runDir, "plan"); + mkdirSync(planDir, { recursive: true }); + const started = Date.now(); + const remote = await planRemotely(workerUrl, projectTarPath); + const planTarPath = join(runDir, "plan.tar.gz"); + writeFileSync(planTarPath, remote.planTar); + execFileSync("tar", ["-xzf", planTarPath, "-C", planDir], { stdio: "pipe" }); + + const base = workerUrl.replace(/\/$/, ""); + for (let i = 0; i < remote.meta.chunkCount; i++) { + console.log(` write-plan hf-chunk-${i}`); + await writePlanWithRetry(base, i, planTarPath); + } + + const result = await renderViaSandboxes({ + projectDir: fixture, + outputPath: join(runDir, "output.mp4"), + planDir, + workDir: runDir, + maxSandboxes: 10, + primitives: { + // Plan already ran on a sandbox (matching worker ffmpeg). Reuse it. + plan: reusedRemotePlan(planDir, remote), + }, + executeChunk: cloudflareExecutor(workerUrl, planTarPath), + }); + result.totalElapsedMs = Date.now() - started; + result.stages.planMs = Math.max( + 0, + result.totalElapsedMs - result.stages.captureMs - result.stages.assembleMs, + ); + return result; +} + +function writeResults( + outputJson: string, + runs: number, + runsOut: Array<{ run: number; perfSummary: PerfSummary }>, +): void { + const avgTotal = Math.round( + runsOut.reduce((s, r) => s + r.perfSummary.totalElapsedMs, 0) / runsOut.length, + ); + const results = { + timestamp: new Date().toISOString(), + platform: `${process.platform} ${process.arch}`, + nodeVersion: process.version, + runsPerFixture: runs, + fixtures: [ + { + fixture: "heygen-promo-preview-assets", + name: "heygen-promo-preview-assets", + runs: runsOut, + averages: { + totalElapsedMs: avgTotal, + captureAvgMs: Math.round( + runsOut.reduce((s, r) => s + (r.perfSummary.captureAvgMs ?? 0), 0) / runsOut.length, + ), + stages: runsOut[0]?.perfSummary.stages ?? {}, + }, + }, + ], + }; + writeFileSync(outputJson, `${JSON.stringify(results, null, 2)}\n`, "utf-8"); + console.log(`\nAverage totalElapsedMs=${avgTotal}`); + console.log(`Results saved to ${outputJson}`); +} + +async function main(): Promise { + const { runs, outputJson, fixture, workerUrl } = parseArgs(process.argv); + mkdirSync(dirname(outputJson), { recursive: true }); + if (!workerUrl || !(await probeWorker(workerUrl))) writeUnavailableLog(outputJson, workerUrl); + if (!existsSync(join(fixture, "index.html"))) { + throw new Error(`fixture missing index.html: ${fixture}`); + } + + const workRoot = join(dirname(outputJson), "sandbox-work"); + mkdirSync(workRoot, { recursive: true }); + const projectTarPath = join(workRoot, "project.tar.gz"); + tarDirectory(fixture, projectTarPath); + + const runsOut: Array<{ run: number; perfSummary: PerfSummary }> = []; + for (let r = 1; r <= runs; r++) { + console.log(`\n━━━ sandbox run ${r}/${runs} ━━━`); + const runDir = join(workRoot, `run-${r}`); + mkdirSync(runDir, { recursive: true }); + const result = await runOnce(workerUrl, fixture, projectTarPath, runDir); + const summary = sandboxRenderToPerfSummary(result, { + renderId: randomUUID(), + workers: result.sandboxIds.length, + }); + console.log( + ` ✓ ${summary.totalElapsedMs}ms total | ${summary.totalFrames} frames | ${result.sandboxIds.length} sandboxes`, + ); + runsOut.push({ run: r, perfSummary: summary }); + } + writeResults(outputJson, runs, runsOut); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/packages/producer/src/distributed.ts b/packages/producer/src/distributed.ts index 25db28ca88..732a6524ac 100644 --- a/packages/producer/src/distributed.ts +++ b/packages/producer/src/distributed.ts @@ -82,6 +82,20 @@ export { } from "./services/distributed/renderConfigValidation.js"; export { hashProjectDir } from "./services/distributed/projectHash.js"; +export { + HEYGEN_PROMO_TOTAL_FRAMES, + CLOUDFLARE_ACCOUNT_ID, + SANDBOX_INSTANCE_TYPE, + SANDBOX_MAX_INSTANCES, + formatCloudflareAccountPin, + heygenPromoDistributedConfig, + renderViaSandboxes, + resolveSandboxFanout, + sandboxIdForChunk, + sandboxRenderToPerfSummary, + type SandboxRenderResult, +} from "./services/distributed/cloudflareSandbox.js"; + // ── Format union ──────────────────────────────────────────────────────────── // Canonical output-format type. The aws-lambda package re-exports it so // CLI / adopter SDKs can derive runtime allowlists from one source. diff --git a/packages/producer/src/renderRequest.ts b/packages/producer/src/renderRequest.ts index e4e373aff9..1f6820fcba 100644 --- a/packages/producer/src/renderRequest.ts +++ b/packages/producer/src/renderRequest.ts @@ -149,6 +149,7 @@ function assertDistributedOptions(value: unknown): void { "temporal", "cloud-run-job", "k8s-job", + "cloudflare-sandbox", "none", ]); for (const field of ["rejectOnSystemFonts", "failClosedFontFetch", "cfr"] as const) { diff --git a/packages/producer/src/services/distributed/cloudflareSandbox.test.ts b/packages/producer/src/services/distributed/cloudflareSandbox.test.ts new file mode 100644 index 0000000000..6d79801e34 --- /dev/null +++ b/packages/producer/src/services/distributed/cloudflareSandbox.test.ts @@ -0,0 +1,184 @@ +/** + * Unit tests for the Cloudflare Sandbox adapter. + * + * Drive the shipped `resolveChunkPlan` / `plan` / adapter orchestration. + * No live Cloudflare — I/O is injected. + */ +import { afterAll, beforeAll, describe, expect, it } from "bun:test"; +import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { plan, resolveChunkPlan } from "./plan.js"; +import { + HEYGEN_PROMO_TOTAL_FRAMES, + CLOUDFLARE_ACCOUNT_ID, + SANDBOX_MAX_INSTANCES, + formatCloudflareAccountPin, + heygenPromoDistributedConfig, + renderViaSandboxes, + resolveSandboxFanout, + sandboxIdForChunk, + sandboxRenderToPerfSummary, +} from "./cloudflareSandbox.js"; +import type { AssembleResult } from "./assemble.js"; +import type { ChunkResult } from "./renderChunk.js"; +import type { PlanResult } from "./plan.js"; + +const here = dirname(fileURLToPath(import.meta.url)); +const WRANGLER_PATH = join(here, "../../../cloudflare-sandbox/wrangler.jsonc"); + +const FIXTURE_16S = ` + +sandbox-plan fixture + +
+

sandbox-plan fixture

+
+ +`; + +let runRoot: string; +let projectDir: string; + +beforeAll(() => { + runRoot = mkdtempSync(join(tmpdir(), "hf-cf-sandbox-test-")); + projectDir = join(runRoot, "project"); + mkdirSync(projectDir, { recursive: true }); + writeFileSync(join(projectDir, "index.html"), FIXTURE_16S, "utf-8"); +}); + +afterAll(() => { + rmSync(runRoot, { recursive: true, force: true }); +}); + +function emptyChunk(outputPath: string, index: number): ChunkResult { + return { + outputPath, + outputKind: "file", + framesEncoded: 48, + sha256: `chunk-${index}`, + durationMs: 10, + planHashMs: 1, + sessionBootMs: 1, + captureStageMs: 7, + encodeStageMs: 1, + workers: 1, + perfPath: `${outputPath}.json`, + }; +} + +describe("Cloudflare fleet pin", () => { + it("pins the configured Cloudflare account and a ≤10 instance cap", () => { + expect(CLOUDFLARE_ACCOUNT_ID).toBe("86bb57b655af7915f42b29dfc2d8807d"); + expect(SANDBOX_MAX_INSTANCES).toBe(10); + const pin = formatCloudflareAccountPin(); + expect(pin).toContain("account_id=86bb57b655af7915f42b29dfc2d8807d"); + expect(pin).toContain("max_instances=10"); + }); + + it("wrangler.jsonc matches the same account and cap", () => { + const raw = readFileSync(WRANGLER_PATH, "utf-8"); + const json = JSON.parse(raw.replace(/\/\/.*$/gm, "").replace(/,(\s*[}\]])/g, "$1")) as { + account_id: string; + containers: Array<{ max_instances: number; instance_type: string }>; + }; + expect(json.account_id).toBe(CLOUDFLARE_ACCOUNT_ID); + expect(json.containers[0]?.max_instances).toBeLessThanOrEqual(SANDBOX_MAX_INSTANCES); + expect(json.containers[0]?.max_instances).toBeGreaterThanOrEqual(1); + expect(json.containers[0]?.instance_type).toBe("standard-3"); + }); +}); + +describe("resolveSandboxFanout", () => { + it("uses shipped resolveChunkPlan for 480 frames / 10 sandboxes", () => { + const shipped = resolveChunkPlan(HEYGEN_PROMO_TOTAL_FRAMES, undefined, 10); + const fanout = resolveSandboxFanout(HEYGEN_PROMO_TOTAL_FRAMES, 10); + expect(fanout.chunkCount).toBe(shipped.chunkCount); + expect(fanout.effectiveChunkSize).toBe(shipped.effectiveChunkSize); + expect(fanout.chunkCount).toBeGreaterThanOrEqual(1); + expect(fanout.chunkCount).toBeLessThanOrEqual(10); + expect(fanout.sandboxIds).toHaveLength(fanout.chunkCount); + expect(fanout.sandboxIds[0]).toBe("hf-chunk-0"); + expect(fanout.sandboxIds.at(-1)).toBe(sandboxIdForChunk(fanout.chunkCount - 1)); + }); + + it("never opens more than 10 sandboxes even if asked", () => { + const fanout = resolveSandboxFanout(HEYGEN_PROMO_TOTAL_FRAMES, 99); + expect(fanout.maxSandboxes).toBe(10); + expect(fanout.chunkCount).toBeLessThanOrEqual(10); + }); +}); + +describe("plan() wiring", () => { + it("plans a 16s/30fps composition into ≤10 chunks via the adapter config", async () => { + const planDir = join(runRoot, "plan-real"); + mkdirSync(planDir, { recursive: true }); + const result = await plan(projectDir, heygenPromoDistributedConfig(10), planDir); + expect(result.totalFrames).toBe(HEYGEN_PROMO_TOTAL_FRAMES); + expect(result.chunkCount).toBeGreaterThanOrEqual(1); + expect(result.chunkCount).toBeLessThanOrEqual(10); + const expected = resolveChunkPlan(result.totalFrames, undefined, 10); + expect(result.chunkCount).toBe(expected.chunkCount); + }); +}); + +describe("renderViaSandboxes", () => { + it("calls plan → renderChunk × N → assemble and stays within 10 sandboxes", async () => { + const calls: string[] = []; + const chunkIndexes: number[] = []; + const fakePlan: PlanResult = { + planDir: join(runRoot, "fake-plan"), + planHash: "test", + chunkCount: 10, + totalFrames: 480, + fps: 30, + width: 1920, + height: 1080, + format: "mp4", + ffmpegVersion: "test", + producerVersion: "test", + }; + const fakeAssemble: AssembleResult = { + outputPath: join(runRoot, "out.mp4"), + durationMs: 5, + framesEncoded: 480, + fileSize: 100, + }; + + const result = await renderViaSandboxes({ + projectDir, + outputPath: fakeAssemble.outputPath, + workDir: join(runRoot, "orch"), + maxSandboxes: 10, + primitives: { + plan: async () => { + calls.push("plan"); + return fakePlan; + }, + renderChunk: async (_planDir, chunkIndex, outputChunkPath) => { + calls.push("renderChunk"); + chunkIndexes.push(chunkIndex); + return emptyChunk(outputChunkPath, chunkIndex); + }, + assemble: async () => { + calls.push("assemble"); + return fakeAssemble; + }, + }, + }); + + expect(calls[0]).toBe("plan"); + expect(calls.at(-1)).toBe("assemble"); + expect(calls.filter((c) => c === "renderChunk")).toHaveLength(10); + expect(chunkIndexes).toEqual([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]); + expect(result.sandboxIds).toHaveLength(10); + expect(result.accountId).toBe(CLOUDFLARE_ACCOUNT_ID); + expect(result.plan.chunkCount).toBeLessThanOrEqual(10); + expect(result.plan.chunkCount).toBeGreaterThanOrEqual(1); + + const summary = sandboxRenderToPerfSummary(result, { renderId: "t", workers: 10 }); + expect(summary.totalFrames).toBe(480); + expect(summary.workers).toBe(10); + }); +}); diff --git a/packages/producer/src/services/distributed/cloudflareSandbox.ts b/packages/producer/src/services/distributed/cloudflareSandbox.ts new file mode 100644 index 0000000000..467c979e44 --- /dev/null +++ b/packages/producer/src/services/distributed/cloudflareSandbox.ts @@ -0,0 +1,265 @@ +/** + * Thin Cloudflare Sandbox adapter for the distributed render pipeline. + * + * Orchestration only: `plan` → `renderChunk` × N → `assemble`. Capture + * inside each sandbox is software/SwiftShader (`renderChunk` asserts that). + * Parallelism is across sandboxes (≤10), not inside one Chrome. + */ + +import { existsSync, mkdirSync, mkdtempSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { assemble, type AssembleResult } from "./assemble.js"; +import { type DistributedRenderConfig, type PlanResult, plan, resolveChunkPlan } from "./plan.js"; +import { type ChunkResult, renderChunk } from "./renderChunk.js"; + +/** Cloudflare account the sandbox fleet is pinned to. */ +export const CLOUDFLARE_ACCOUNT_ID = "86bb57b655af7915f42b29dfc2d8807d"; + +/** Hard cap on live sandboxes — matches wrangler `max_instances`. */ +export const SANDBOX_MAX_INSTANCES = 10; + +/** First-bet instance type; bump to standard-3 only if Chrome OOMs. */ +export const SANDBOX_INSTANCE_TYPE = "standard-3"; + +const SANDBOX_ID_PREFIX = "hf-chunk-"; + +export const HEYGEN_PROMO_TOTAL_FRAMES = 480; +const HEYGEN_PROMO_FPS = 30; +const HEYGEN_PROMO_WIDTH = 1920; +const HEYGEN_PROMO_HEIGHT = 1080; + +export interface SandboxFanout { + chunkCount: number; + effectiveChunkSize: number; + sandboxIds: string[]; + maxSandboxes: number; +} + +export interface DistributedPrimitives { + plan: typeof plan; + renderChunk: typeof renderChunk; + assemble: typeof assemble; +} + +/** + * One remote (or test-double) place that can run `renderChunk` against a + * planDir. The adapter never talks to Cloudflare APIs itself — the bench + * / Worker injects this. + */ +export interface ChunkExecutor { + (args: { + chunkIndex: number; + sandboxId: string; + planDir: string; + outputChunkPath: string; + renderChunk: typeof renderChunk; + }): Promise; +} + +export interface SandboxRenderInput { + projectDir: string; + outputPath: string; + planDir?: string; + workDir?: string; + maxSandboxes?: number; + primitives?: Partial; + executeChunk?: ChunkExecutor; + config?: Partial; +} + +export interface SandboxRenderResult { + plan: PlanResult; + assemble: AssembleResult; + chunks: ChunkResult[]; + sandboxIds: string[]; + instanceType: typeof SANDBOX_INSTANCE_TYPE; + accountId: typeof CLOUDFLARE_ACCOUNT_ID; + totalElapsedMs: number; + stages: { + planMs: number; + captureMs: number; + assembleMs: number; + }; +} + +function capSandboxes(requested: number | undefined): number { + const n = requested ?? SANDBOX_MAX_INSTANCES; + if (!Number.isInteger(n) || n <= 0) { + throw new Error( + `[cloudflareSandbox] maxSandboxes must be a positive integer (received ${String(n)})`, + ); + } + return Math.min(n, SANDBOX_MAX_INSTANCES); +} + +export function sandboxIdForChunk(chunkIndex: number): string { + if (!Number.isInteger(chunkIndex) || chunkIndex < 0 || chunkIndex >= SANDBOX_MAX_INSTANCES) { + throw new Error( + `[cloudflareSandbox] chunkIndex must be in 0..${SANDBOX_MAX_INSTANCES - 1} (received ${String(chunkIndex)})`, + ); + } + return `${SANDBOX_ID_PREFIX}${chunkIndex}`; +} + +/** + * Fan-out for a known frame count. Delegates sizing to the shipped + * `resolveChunkPlan` so adapters cannot drift from plan() math. + */ +export function resolveSandboxFanout(totalFrames: number, maxSandboxes?: number): SandboxFanout { + const max = capSandboxes(maxSandboxes); + const { chunkCount, effectiveChunkSize } = resolveChunkPlan(totalFrames, undefined, max); + const sandboxIds = Array.from({ length: chunkCount }, (_, i) => sandboxIdForChunk(i)); + return { chunkCount, effectiveChunkSize, sandboxIds, maxSandboxes: max }; +} + +export function heygenPromoDistributedConfig(maxSandboxes?: number): DistributedRenderConfig { + const maxParallelChunks = capSandboxes(maxSandboxes); + return { + fps: HEYGEN_PROMO_FPS, + width: HEYGEN_PROMO_WIDTH, + height: HEYGEN_PROMO_HEIGHT, + format: "mp4", + quality: "high", + maxParallelChunks, + runtimeCap: "cloudflare-sandbox", + hdrMode: "force-sdr", + }; +} + +function defaultExecuteChunk(): ChunkExecutor { + return async ({ planDir, chunkIndex, outputChunkPath, renderChunk: run }) => + run(planDir, chunkIndex, outputChunkPath); +} + +/** + * Controller-side orchestration. `plan` and `assemble` run here; + * `executeChunk` is how each sandbox (or a test double) runs `renderChunk`. + */ +export async function renderViaSandboxes(input: SandboxRenderInput): Promise { + const started = Date.now(); + const primitives: DistributedPrimitives = { + plan, + renderChunk, + assemble, + ...input.primitives, + }; + const executeChunk = input.executeChunk ?? defaultExecuteChunk(); + const maxSandboxes = capSandboxes(input.maxSandboxes); + const workDir = input.workDir ?? mkdtempSync(join(tmpdir(), "hf-cf-sandbox-")); + const planDir = input.planDir ?? join(workDir, "plan"); + mkdirSync(planDir, { recursive: true }); + mkdirSync(join(workDir, "chunks"), { recursive: true }); + + const config: DistributedRenderConfig = { + ...heygenPromoDistributedConfig(maxSandboxes), + ...input.config, + maxParallelChunks: maxSandboxes, + runtimeCap: "cloudflare-sandbox", + }; + + const planStarted = Date.now(); + const planResult = await primitives.plan(input.projectDir, config, planDir); + const planMs = Date.now() - planStarted; + + if (planResult.chunkCount < 1 || planResult.chunkCount > SANDBOX_MAX_INSTANCES) { + throw new Error( + `[cloudflareSandbox] plan produced ${planResult.chunkCount} chunks; ` + + `must be 1..${SANDBOX_MAX_INSTANCES}`, + ); + } + + const sandboxIds = Array.from({ length: planResult.chunkCount }, (_, i) => sandboxIdForChunk(i)); + const chunkPaths = sandboxIds.map((_, i) => join(workDir, "chunks", `chunk-${i}.mp4`)); + + const captureStarted = Date.now(); + const chunks = await Promise.all( + sandboxIds.map((sandboxId, chunkIndex) => + executeChunk({ + chunkIndex, + sandboxId, + planDir, + outputChunkPath: chunkPaths[chunkIndex]!, + renderChunk: primitives.renderChunk, + }), + ), + ); + const captureMs = Date.now() - captureStarted; + + const audioCandidate = join(planDir, "audio.aac"); + const audioPath = existsSync(audioCandidate) ? audioCandidate : null; + const assembleStarted = Date.now(); + const assembleResult = await primitives.assemble( + planDir, + chunkPaths, + audioPath, + input.outputPath, + ); + const assembleMs = Date.now() - assembleStarted; + + return { + plan: planResult, + assemble: assembleResult, + chunks, + sandboxIds, + instanceType: SANDBOX_INSTANCE_TYPE, + accountId: CLOUDFLARE_ACCOUNT_ID, + totalElapsedMs: Date.now() - started, + stages: { planMs, captureMs, assembleMs }, + }; +} + +export function sandboxRenderToPerfSummary( + result: SandboxRenderResult, + opts: { renderId: string; workers: number }, +): { + renderId: string; + totalElapsedMs: number; + fps: number; + quality: "high"; + workers: number; + chunkedEncode: boolean; + chunkSizeFrames: number; + compositionDurationSeconds: number; + totalFrames: number; + resolution: { width: number; height: number }; + videoCount: number; + audioCount: number; + stages: Record; + captureAvgMs: number; +} { + const totalFrames = result.plan.totalFrames; + return { + renderId: opts.renderId, + totalElapsedMs: result.totalElapsedMs, + fps: result.plan.fps, + quality: "high", + workers: opts.workers, + chunkedEncode: true, + chunkSizeFrames: result.plan.totalFrames / result.plan.chunkCount, + compositionDurationSeconds: totalFrames / result.plan.fps, + totalFrames, + resolution: { width: result.plan.width, height: result.plan.height }, + videoCount: 0, + audioCount: 0, + stages: { + compileMs: result.stages.planMs, + videoExtractMs: 0, + audioProcessMs: 0, + captureMs: result.stages.captureMs, + encodeMs: result.chunks.reduce((sum, chunk) => sum + chunk.encodeStageMs, 0), + assembleMs: result.stages.assembleMs, + }, + captureAvgMs: totalFrames > 0 ? Math.round(result.stages.captureMs / totalFrames) : 0, + }; +} + +/** Persist a one-line pin of the account + fleet for the verifier. */ +export function formatCloudflareAccountPin(): string { + return [ + `account_id=${CLOUDFLARE_ACCOUNT_ID}`, + `max_instances=${SANDBOX_MAX_INSTANCES}`, + `instance_type=${SANDBOX_INSTANCE_TYPE}`, + `sandbox_id_prefix=${SANDBOX_ID_PREFIX}`, + ].join("\n"); +} diff --git a/packages/producer/src/services/distributed/plan.ts b/packages/producer/src/services/distributed/plan.ts index 8f11d7bd55..7f44e82e9c 100644 --- a/packages/producer/src/services/distributed/plan.ts +++ b/packages/producer/src/services/distributed/plan.ts @@ -174,7 +174,7 @@ export interface DistributedRenderConfig { */ targetChunkFrames?: number; /** Runtime hint; consumed by future per-runtime budget checks. The current implementation records the value but does not enforce. */ - runtimeCap?: "lambda" | "temporal" | "cloud-run-job" | "k8s-job" | "none"; + runtimeCap?: "lambda" | "temporal" | "cloud-run-job" | "k8s-job" | "cloudflare-sandbox" | "none"; /** * Reject compositions whose primary font-family resolves to a host-OS / diff --git a/packages/producer/src/services/distributed/publicExports.test.ts b/packages/producer/src/services/distributed/publicExports.test.ts index 004ba4fce6..60b262ab9e 100644 --- a/packages/producer/src/services/distributed/publicExports.test.ts +++ b/packages/producer/src/services/distributed/publicExports.test.ts @@ -39,6 +39,9 @@ describe("@hyperframes/producer/distributed (subpath)", () => { expect(distributedSubpath.DEFAULT_CHUNK_SIZE).toBe(240); expect(distributedSubpath.DEFAULT_MAX_PARALLEL_CHUNKS).toBe(16); expect(distributedSubpath.PLAN_DIR_SIZE_LIMIT_BYTES).toBe(2 * 1024 * 1024 * 1024); + expect(typeof distributedSubpath.resolveSandboxFanout).toBe("function"); + expect(distributedSubpath.SANDBOX_MAX_INSTANCES).toBe(10); + expect(distributedSubpath.CLOUDFLARE_ACCOUNT_ID).toBe("86bb57b655af7915f42b29dfc2d8807d"); }); it("exports the non-retryable error codes + classes", () => { diff --git a/packages/producer/src/services/distributed/renderChunkCli.ts b/packages/producer/src/services/distributed/renderChunkCli.ts new file mode 100644 index 0000000000..5117eff232 --- /dev/null +++ b/packages/producer/src/services/distributed/renderChunkCli.ts @@ -0,0 +1,38 @@ +/** + * CLI entry used inside a Cloudflare sandbox. + * + * Bun swallows unknown `--flags`, so configuration is via env vars: + * + * HF_ACTION=render HF_PLAN_DIR=... HF_CHUNK_INDEX=0 HF_OUTPUT=... bun chunk-worker.mjs + * HF_ACTION=plan HF_PROJECT_DIR=... HF_PLAN_DIR=... HF_MAX_SANDBOXES=10 bun chunk-worker.mjs + */ +import { heygenPromoDistributedConfig } from "./cloudflareSandbox.js"; +import { plan } from "./plan.js"; +import { renderChunk } from "./renderChunk.js"; + +function env(name: string, required = true): string | undefined { + const value = process.env[name]; + if (required && (!value || value.length === 0)) throw new Error(`missing env ${name}`); + return value; +} + +const action = env("HF_ACTION", false) ?? "render"; + +if (action === "plan") { + const projectDir = env("HF_PROJECT_DIR")!; + const planDir = env("HF_PLAN_DIR")!; + const maxSandboxes = Number(env("HF_MAX_SANDBOXES", false) ?? "10"); + const result = await plan(projectDir, heygenPromoDistributedConfig(maxSandboxes), planDir); + process.stdout.write(`${JSON.stringify(result)}\n`); +} else if (action === "render") { + const planDir = env("HF_PLAN_DIR")!; + const chunkIndex = Number(env("HF_CHUNK_INDEX")); + const output = env("HF_OUTPUT")!; + if (!Number.isInteger(chunkIndex) || chunkIndex < 0) { + throw new Error(`invalid HF_CHUNK_INDEX ${String(chunkIndex)}`); + } + const result = await renderChunk(planDir, chunkIndex, output); + process.stdout.write(`${JSON.stringify(result)}\n`); +} else { + throw new Error(`unknown HF_ACTION ${action}`); +} diff --git a/packages/producer/src/services/distributed/renderConfigValidation.ts b/packages/producer/src/services/distributed/renderConfigValidation.ts index 261fc11d10..f65c2d0b91 100644 --- a/packages/producer/src/services/distributed/renderConfigValidation.ts +++ b/packages/producer/src/services/distributed/renderConfigValidation.ts @@ -57,7 +57,14 @@ const ALLOWED_FORMATS = [ ] as const satisfies readonly DistributedFormat[]; const ALLOWED_CODECS = ["h264", "h265"] as const; const ALLOWED_QUALITIES = ["draft", "standard", "high"] as const; -const ALLOWED_RUNTIME_CAPS = ["lambda", "temporal", "cloud-run-job", "k8s-job", "none"] as const; +const ALLOWED_RUNTIME_CAPS = [ + "lambda", + "temporal", + "cloud-run-job", + "k8s-job", + "cloudflare-sandbox", + "none", +] as const; const ALLOWED_HDR_MODES = ["auto", "force-sdr"] as const; const MAX_DIMENSION = 7680; diff --git a/packages/producer/tests/perf/README.md b/packages/producer/tests/perf/README.md index 543fe840dc..041a63e7d0 100644 --- a/packages/producer/tests/perf/README.md +++ b/packages/producer/tests/perf/README.md @@ -10,6 +10,9 @@ designed for **regression spotting**: capture a baseline, change something, re-run, eyeball the diff. For pass/fail thresholds see `tests/perf/baseline.json` and the perf-regression checks in the integration test suite. +The current optimization target — beat `heygen-promo-preview-assets` by 25% +using up to 10 Cloudflare sandboxes — lives in [`goal.md`](./goal.md). + ## Quick start ```bash diff --git a/packages/producer/tests/perf/goal.md b/packages/producer/tests/perf/goal.md new file mode 100644 index 0000000000..b91b04b1c5 --- /dev/null +++ b/packages/producer/tests/perf/goal.md @@ -0,0 +1,156 @@ +# Goal: beat the render benchmark by 25% with remote parallel workers + +Cut wall-clock time of the `heygen-promo-preview-assets` producer benchmark by **at least 25%** by spreading capture across **up to 10 Cloudflare Sandboxes** on the existing Cloudflare account pinned in wrangler. Do not create a new account. + +This is the reusable optimization target. Re-measure the same fixture with the same harness after every change. + +## Success + +A later run of the same fixture is ≥25% faster on **total wall-clock** (`totalElapsedMs`) than the frozen baseline below. + +```bash +cd packages/producer +bun src/benchmark.ts --only heygen-promo-preview-assets --runs 3 +``` + +Pass if the 3-run average `totalElapsedMs` is **≤ 142,712 ms** (190,283 × 0.75). + +Secondary metrics to report, not pass/fail: + +- `stages.captureMs` and `captureAvgMs` +- peak RSS / heap +- sandbox count actually used +- cold-start vs warm-start split +- plan / transfer / assemble overhead + +Visual quality must stay at the fixture's existing PSNR bar (`minPsnr: 30`). A faster black video is a fail. + +## Frozen baseline + +Captured 2026-08-13 on `darwin arm64`, Bun, quality `high`, **1 auto worker** (calibration p95 3,317 ms collapsed the auto budget 4 → 1). + +| | | +|---|---| +| Fixture | `packages/producer/tests/heygen-promo-preview-assets` | +| Shape | 16 s, 1920×1080, 30 fps, **480 frames**, 3 GSAP sub-comps, 9 images, 4 local fonts | +| Media | no `