diff --git a/.github/workflows/benchmark_docker_pull.yml b/.github/workflows/benchmark_docker_pull.yml new file mode 100644 index 0000000000000..d1f80c8685673 --- /dev/null +++ b/.github/workflows/benchmark_docker_pull.yml @@ -0,0 +1,106 @@ +name: "benchmark: docker pull time" + +on: + workflow_dispatch: + +env: + ELECTRON_SKIP_BINARY_DOWNLOAD: 1 + +jobs: + benchmark: + name: "pull-time comparison" + runs-on: ubuntu-latest + permissions: + packages: write + contents: read + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-node@v6 + with: + node-version: 20 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Login to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - run: npm ci + - run: npm run build + - name: Build monolithic image (main branch Dockerfile) + run: | + node utils/pack_package.js playwright-core utils/docker/playwright-core.tar.gz + git show main:utils/docker/Dockerfile.jammy > /tmp/Dockerfile.monolithic + docker buildx build \ + --platform linux/amd64 \ + --push \ + -f /tmp/Dockerfile.monolithic \ + -t ghcr.io/${{ github.repository }}:bench-monolithic \ + utils/docker/ + - name: Build split-layer image (this branch Dockerfile) + run: | + docker buildx build \ + --platform linux/amd64 \ + --push \ + -f utils/docker/Dockerfile.jammy \ + -t ghcr.io/${{ github.repository }}:bench-split \ + utils/docker/ + - name: Benchmark cold pulls + run: | + echo "## Docker Pull Benchmark" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "Runner: $(uname -m), $(nproc) cores" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Image | Run 1 | Run 2 | Run 3 | Avg |" >> $GITHUB_STEP_SUMMARY + echo "|-------|-------|-------|-------|-----|" >> $GITHUB_STEP_SUMMARY + + for variant in monolithic split; do + IMAGE="ghcr.io/${{ github.repository }}:bench-${variant}" + TIMES=() + for i in 1 2 3; do + docker rmi "$IMAGE" 2>/dev/null + docker image prune -a -f >/dev/null 2>&1 + START=$(date +%s%N) + docker pull "$IMAGE" >/dev/null 2>&1 + END=$(date +%s%N) + ELAPSED=$(python3 -c "print(f'{($END - $START) / 1e9:.1f}')") + TIMES+=("$ELAPSED") + done + AVG=$(python3 -c "t=[${TIMES[0]},${TIMES[1]},${TIMES[2]}]; print(f'{sum(t)/len(t):.1f}')") + echo "| ${variant} | ${TIMES[0]}s | ${TIMES[1]}s | ${TIMES[2]}s | ${AVG}s |" >> $GITHUB_STEP_SUMMARY + done + + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Layer structure" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + echo "Monolithic:" >> $GITHUB_STEP_SUMMARY + docker manifest inspect ghcr.io/${{ github.repository }}:bench-monolithic 2>/dev/null | \ + python3 -c " + import json,sys + data = json.load(sys.stdin) + for m in data.get('manifests', []): + if m['platform']['architecture'] == 'amd64': + print(f\" digest: {m['digest'][:32]}...\") + " || true + docker buildx imagetools inspect ghcr.io/${{ github.repository }}:bench-monolithic --raw 2>/dev/null | \ + python3 -c " + import json,sys + data = json.load(sys.stdin) + layers = data.get('layers', []) + for i,l in enumerate(layers): + print(f' Layer {i}: {l[\"size\"]/1e6:.1f} MB') + print(f' Total: {sum(l[\"size\"] for l in layers)/1e6:.1f} MB') + " >> $GITHUB_STEP_SUMMARY || true + echo "" >> $GITHUB_STEP_SUMMARY + echo "Split:" >> $GITHUB_STEP_SUMMARY + docker buildx imagetools inspect ghcr.io/${{ github.repository }}:bench-split --raw 2>/dev/null | \ + python3 -c " + import json,sys + data = json.load(sys.stdin) + layers = data.get('layers', []) + for i,l in enumerate(layers): + print(f' Layer {i}: {l[\"size\"]/1e6:.1f} MB') + print(f' Total: {sum(l[\"size\"] for l in layers)/1e6:.1f} MB') + " >> $GITHUB_STEP_SUMMARY || true + echo '```' >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/publish_release_docker.yml b/.github/workflows/publish_release_docker.yml index adf58d00d190e..0063f94fbb6d0 100644 --- a/.github/workflows/publish_release_docker.yml +++ b/.github/workflows/publish_release_docker.yml @@ -27,6 +27,8 @@ jobs: uses: docker/setup-qemu-action@v4 with: platforms: arm64 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 - run: npm ci - run: npm run build - name: Azure Login diff --git a/utils/docker/Dockerfile.jammy b/utils/docker/Dockerfile.jammy index 626bdb48c69ef..3b4c2dc845aae 100644 --- a/utils/docker/Dockerfile.jammy +++ b/utils/docker/Dockerfile.jammy @@ -1,3 +1,4 @@ +# syntax=docker/dockerfile:1 FROM ubuntu:jammy ARG DEBIAN_FRONTEND=noninteractive @@ -11,7 +12,6 @@ ENV LC_ALL=C.UTF-8 # === INSTALL Node.js === RUN apt-get update && \ - # Install Node.js apt-get install -y curl wget gpg ca-certificates && \ mkdir -p /etc/apt/keyrings && \ curl -sL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg && \ @@ -27,6 +27,7 @@ RUN apt-get update && \ adduser pwuser # === BAKE BROWSERS INTO IMAGE === +# Browsers are split into separate layers to enable parallel pulls. ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright @@ -34,16 +35,13 @@ ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright # The package should be built beforehand from tip-of-tree Playwright. COPY ./playwright-core.tar.gz /tmp/playwright-core.tar.gz -# 2. Bake in browsers & deps. -# Browsers will be downloaded in `/ms-playwright`. -# Note: make sure to set 777 to the registry so that any user can access -# registry. +# 2. Set up playwright-core and install all system dependencies in one layer. RUN mkdir /ms-playwright && \ mkdir /ms-playwright-agent && \ cd /ms-playwright-agent && npm init -y && \ npm i /tmp/playwright-core.tar.gz && \ npm exec --no -- playwright-core mark-docker-image "${DOCKER_IMAGE_NAME_TEMPLATE}" && \ - npm exec --no -- playwright-core install --with-deps && rm -rf /var/lib/apt/lists/* && \ + npm exec --no -- playwright-core install-deps chromium chromium-headless-shell firefox webkit && \ # Workaround for https://github.com/microsoft/playwright/issues/27313 # While the gstreamer plugin load process can be in-process, it ended up throwing # an error that it can't have libsoup2 and libsoup3 in the same process because @@ -53,7 +51,25 @@ RUN mkdir /ms-playwright && \ else \ rm /usr/lib/x86_64-linux-gnu/gstreamer-1.0/libgstwebrtc.so; \ fi && \ + rm -rf /var/lib/apt/lists/* + +# 3. Install each browser binary in its own layer for parallel pulling. +RUN cd /ms-playwright-agent && \ + npm exec --no -- playwright-core install chromium chromium-headless-shell && \ + chmod -R 777 /ms-playwright/chromium-* /ms-playwright/chromium_headless_shell-* + +RUN cd /ms-playwright-agent && \ + npm exec --no -- playwright-core install firefox && \ + chmod -R 777 /ms-playwright/firefox-* + +RUN cd /ms-playwright-agent && \ + npm exec --no -- playwright-core install webkit && \ + chmod -R 777 /ms-playwright/webkit-* + +# 4. Install ffmpeg and clean up. +RUN cd /ms-playwright-agent && \ + npm exec --no -- playwright-core install ffmpeg && \ rm /tmp/playwright-core.tar.gz && \ rm -rf /ms-playwright-agent && \ rm -rf ~/.npm/ && \ - chmod -R 777 /ms-playwright + chmod -R 777 /ms-playwright/ffmpeg-* diff --git a/utils/docker/Dockerfile.noble b/utils/docker/Dockerfile.noble index 74bf34c0ac862..da8faf77f8268 100644 --- a/utils/docker/Dockerfile.noble +++ b/utils/docker/Dockerfile.noble @@ -1,3 +1,4 @@ +# syntax=docker/dockerfile:1 FROM ubuntu:noble ARG DEBIAN_FRONTEND=noninteractive @@ -11,7 +12,6 @@ ENV LC_ALL=C.UTF-8 # === INSTALL Node.js === RUN apt-get update && \ - # Install Node.js apt-get install -y curl wget gpg ca-certificates && \ mkdir -p /etc/apt/keyrings && \ curl -sL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg && \ @@ -27,6 +27,7 @@ RUN apt-get update && \ adduser pwuser # === BAKE BROWSERS INTO IMAGE === +# Browsers are split into separate layers to enable parallel pulls. ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright @@ -34,17 +35,32 @@ ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright # The package should be built beforehand from tip-of-tree Playwright. COPY ./playwright-core.tar.gz /tmp/playwright-core.tar.gz -# 2. Bake in browsers & deps. -# Browsers will be downloaded in `/ms-playwright`. -# Note: make sure to set 777 to the registry so that any user can access -# registry. +# 2. Set up playwright-core and install all system dependencies in one layer. RUN mkdir /ms-playwright && \ mkdir /ms-playwright-agent && \ cd /ms-playwright-agent && npm init -y && \ npm i /tmp/playwright-core.tar.gz && \ npm exec --no -- playwright-core mark-docker-image "${DOCKER_IMAGE_NAME_TEMPLATE}" && \ - npm exec --no -- playwright-core install --with-deps && rm -rf /var/lib/apt/lists/* && \ + npm exec --no -- playwright-core install-deps chromium chromium-headless-shell firefox webkit && \ + rm -rf /var/lib/apt/lists/* + +# 3. Install each browser binary in its own layer for parallel pulling. +RUN cd /ms-playwright-agent && \ + npm exec --no -- playwright-core install chromium chromium-headless-shell && \ + chmod -R 777 /ms-playwright/chromium-* /ms-playwright/chromium_headless_shell-* + +RUN cd /ms-playwright-agent && \ + npm exec --no -- playwright-core install firefox && \ + chmod -R 777 /ms-playwright/firefox-* + +RUN cd /ms-playwright-agent && \ + npm exec --no -- playwright-core install webkit && \ + chmod -R 777 /ms-playwright/webkit-* + +# 4. Install ffmpeg and clean up. +RUN cd /ms-playwright-agent && \ + npm exec --no -- playwright-core install ffmpeg && \ rm /tmp/playwright-core.tar.gz && \ rm -rf /ms-playwright-agent && \ rm -rf ~/.npm/ && \ - chmod -R 777 /ms-playwright + chmod -R 777 /ms-playwright/ffmpeg-* diff --git a/utils/docker/publish_docker.sh b/utils/docker/publish_docker.sh index 870da29a905a0..0032833ee87d0 100755 --- a/utils/docker/publish_docker.sh +++ b/utils/docker/publish_docker.sh @@ -36,9 +36,8 @@ tag_and_push() { local source="$1" local target="$2" echo "-- tagging: $target" - docker tag $source $target - docker push $target - attach_eol_manifest $target + docker buildx imagetools create --tag "$target" "$source" + attach_eol_manifest "$target" } attach_eol_manifest() { @@ -79,11 +78,24 @@ publish_docker_images_with_arch_suffix() { fi # Prune docker images to avoid platform conflicts docker system prune -fa - ./build.sh "--${ARCH}" "${FLAVOR}" playwright:localbuild - for ((i = 0; i < ${#TAGS[@]}; i++)) do + local CANONICAL_TAG="playwright.azurecr.io/public/${MCR_IMAGE_NAME}:${TAGS[0]}-${ARCH}" + + # Build and push the canonical tag. + node ../../utils/pack_package.js playwright-core ./playwright-core.tar.gz + docker buildx build \ + --platform "linux/${ARCH}" \ + --push \ + -f "Dockerfile.${FLAVOR}" \ + -t "${CANONICAL_TAG}" \ + . + rm -f playwright-core.tar.gz + attach_eol_manifest "${CANONICAL_TAG}" + + # Create additional tags via registry-side copy (no re-upload). + for ((i = 1; i < ${#TAGS[@]}; i++)) do local TAG="${TAGS[$i]}" - tag_and_push playwright:localbuild "playwright.azurecr.io/public/${MCR_IMAGE_NAME}:${TAG}-${ARCH}" + tag_and_push "${CANONICAL_TAG}" "playwright.azurecr.io/public/${MCR_IMAGE_NAME}:${TAG}-${ARCH}" done }