mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-10-22 01:53:33 +00:00
🐆 perf: Build Dev Branch Images on Native ARM and Cheapen Asset Brotli (#15441)
The dev-branch image build takes 20-40 minutes and has hit its 130-minute timeout. Neither cause was in the bundler. Per-step timings from run 33424998112 show QEMU is ~84% of the work: npm ci 52.6s (amd64) vs 333.2s (arm64, QEMU) — 6.3x npm run frontend 90.4s (amd64) vs 442.8s (arm64, QEMU) — 4.9x So build each platform on a runner of its own architecture, push by digest, and stitch the digests into the multi-platform manifest lists in a merge job. The tags consumers pull are unchanged. Layer caching goes to GHCR beside the image rather than `type=gha`: the Actions cache is capped at 10GB repo-wide and is branch-scoped, so `dev` and `main` runs would not share it, while a registry cache has no cap and is reusable by the other four image workflows when this pattern is rolled out to them. Separately, vite-plugin-compression2 defaults to brotli quality 11, which costs ~13s of CPU on this 18.4MB bundle against ~0.47s for gzip-9, and the plugin's scheduler serializes quality >= 10 as a high-memory operation so it cannot use the runner's cores. Quality 5 compresses in ~0.2s and still lands under gzip (4.3MB vs 5.0MB). `.br` is only served when ENABLE_STATIC_ASSET_BROTLI is set, which is off by default, so the 0.5MB the top quality buys is paid for on every build of every platform and rarely collected. The client build goes from ~11s to ~4s; gzip output is unchanged. This is a pilot on the dev-branch workflow only. dev-images, dev-staging-images, main-image-workflow and tag-images still use QEMU and still have no layer cache.
This commit is contained in:
parent
bd24e45489
commit
d3ea6f415d
2 changed files with 133 additions and 17 deletions
133
.github/workflows/dev-branch-images.yml
vendored
133
.github/workflows/dev-branch-images.yml
vendored
|
|
@ -33,29 +33,51 @@ concurrency:
|
|||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
# Each platform builds on a runner of its own architecture and pushes an
|
||||
# untagged, digest-addressed manifest. Building linux/arm64 on an amd64 runner
|
||||
# needs QEMU, which measured 5-6x slower than native on this image
|
||||
# (npm ci 52.6s -> 333.2s, npm run frontend 90.4s -> 442.8s) and made the
|
||||
# emulated leg ~84% of the build. The `merge` job below stitches the digests
|
||||
# into the multi-platform manifest lists that consumers actually pull.
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 130
|
||||
name: Build ${{ matrix.image_name }} (${{ matrix.platform }})
|
||||
runs-on: ${{ matrix.runner }}
|
||||
timeout-minutes: 90
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- target: api-build
|
||||
file: Dockerfile.multi
|
||||
image_name: lc-dev-api
|
||||
platform: linux/amd64
|
||||
arch: amd64
|
||||
runner: ubuntu-latest
|
||||
- target: api-build
|
||||
file: Dockerfile.multi
|
||||
image_name: lc-dev-api
|
||||
platform: linux/arm64
|
||||
arch: arm64
|
||||
runner: ubuntu-24.04-arm
|
||||
- target: node
|
||||
file: Dockerfile
|
||||
image_name: lc-dev
|
||||
platform: linux/amd64
|
||||
arch: amd64
|
||||
runner: ubuntu-latest
|
||||
- target: node
|
||||
file: Dockerfile
|
||||
image_name: lc-dev
|
||||
platform: linux/arm64
|
||||
arch: arm64
|
||||
runner: ubuntu-24.04-arm
|
||||
|
||||
steps:
|
||||
# Check out the repository
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
|
||||
# Set up QEMU
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v4
|
||||
|
||||
# Set up Docker Buildx
|
||||
# Set up Docker Buildx (no QEMU: the runner is already the target arch)
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
|
|
@ -85,21 +107,100 @@ jobs:
|
|||
echo "BUILD_BRANCH=${{ github.ref_name }}" >> $GITHUB_ENV
|
||||
echo "BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_ENV
|
||||
|
||||
# Build and push Docker images for each target
|
||||
- name: Build and push Docker images
|
||||
# Build and push a digest-only image for this one platform. The layer cache
|
||||
# lives in GHCR beside the image (no 10GB Actions-cache cap, and readable
|
||||
# from every branch and workflow, unlike type=gha which is branch-scoped).
|
||||
# The cache ref is per-architecture so the two platform jobs cannot clobber
|
||||
# each other's manifest.
|
||||
- name: Build and push by digest
|
||||
id: build
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
context: .
|
||||
file: ${{ matrix.file }}
|
||||
push: true
|
||||
tags: |
|
||||
ghcr.io/${{ github.repository_owner }}/${{ matrix.image_name }}:${{ github.sha }}
|
||||
ghcr.io/${{ github.repository_owner }}/${{ matrix.image_name }}:latest
|
||||
${{ secrets.DOCKERHUB_USERNAME }}/${{ matrix.image_name }}:${{ github.sha }}
|
||||
${{ secrets.DOCKERHUB_USERNAME }}/${{ matrix.image_name }}:latest
|
||||
platforms: linux/amd64,linux/arm64
|
||||
platforms: ${{ matrix.platform }}
|
||||
target: ${{ matrix.target }}
|
||||
cache-from: type=registry,ref=ghcr.io/${{ github.repository_owner }}/${{ matrix.image_name }}:buildcache-${{ matrix.arch }}
|
||||
cache-to: type=registry,ref=ghcr.io/${{ github.repository_owner }}/${{ matrix.image_name }}:buildcache-${{ matrix.arch }},mode=max
|
||||
outputs: type=image,"name=ghcr.io/${{ github.repository_owner }}/${{ matrix.image_name }},docker.io/${{ secrets.DOCKERHUB_USERNAME }}/${{ matrix.image_name }}",push-by-digest=true,name-canonical=true,push=true
|
||||
build-args: |
|
||||
BUILD_COMMIT=${{ env.BUILD_COMMIT }}
|
||||
BUILD_BRANCH=${{ env.BUILD_BRANCH }}
|
||||
BUILD_DATE=${{ env.BUILD_DATE }}
|
||||
|
||||
- name: Export digest
|
||||
run: |
|
||||
mkdir -p /tmp/digests
|
||||
digest="${{ steps.build.outputs.digest }}"
|
||||
touch "/tmp/digests/${digest#sha256:}"
|
||||
|
||||
- name: Upload digest
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: digests-${{ matrix.image_name }}-${{ matrix.arch }}
|
||||
path: /tmp/digests/*
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
# Combine the per-architecture digests into one multi-platform manifest list
|
||||
# per registry, and apply the tags consumers pull.
|
||||
merge:
|
||||
name: Merge manifests (${{ matrix.image_name }})
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
timeout-minutes: 15
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
image_name: [lc-dev-api, lc-dev]
|
||||
|
||||
steps:
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
pattern: digests-${{ matrix.image_name }}-*
|
||||
merge-multiple: true
|
||||
path: /tmp/digests
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
# The digests are identical across registries (the manifest is
|
||||
# content-addressed), so the same set sources both manifest lists.
|
||||
- name: Create manifest lists and push
|
||||
working-directory: /tmp/digests
|
||||
env:
|
||||
GHCR_IMAGE: ghcr.io/${{ github.repository_owner }}/${{ matrix.image_name }}
|
||||
DOCKERHUB_IMAGE: docker.io/${{ secrets.DOCKERHUB_USERNAME }}/${{ matrix.image_name }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
shopt -s nullglob
|
||||
digests=(*)
|
||||
if [ "${#digests[@]}" -eq 0 ]; then
|
||||
echo "No digests downloaded; refusing to publish an empty manifest." >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "Merging ${#digests[@]} platform digests: ${digests[*]}"
|
||||
for image in "$GHCR_IMAGE" "$DOCKERHUB_IMAGE"; do
|
||||
docker buildx imagetools create \
|
||||
-t "${image}:${{ github.sha }}" \
|
||||
-t "${image}:latest" \
|
||||
"${digests[@]/#/${image}@sha256:}"
|
||||
done
|
||||
|
||||
- name: Inspect image
|
||||
run: |
|
||||
docker buildx imagetools inspect ghcr.io/${{ github.repository_owner }}/${{ matrix.image_name }}:${{ github.sha }}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
import react from '@vitejs/plugin-react';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { constants } from 'zlib';
|
||||
import { defineConfig } from 'vite';
|
||||
import { createRequire } from 'module';
|
||||
import { VitePWA } from 'vite-plugin-pwa';
|
||||
import { compression } from 'vite-plugin-compression2';
|
||||
import { nodePolyfills } from 'vite-plugin-node-polyfills';
|
||||
import { compression, defineAlgorithm } from 'vite-plugin-compression2';
|
||||
import type { Plugin } from 'vite';
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
|
|
@ -178,6 +179,20 @@ export default defineConfig(({ command }) => ({
|
|||
...(buildSourceMap ? [sourcemapExclude({ excludeNodeModules: true })] : []),
|
||||
compression({
|
||||
threshold: 10240,
|
||||
/**
|
||||
* Brotli's default quality of 11 costs ~13s of single-threaded CPU on this bundle
|
||||
* against ~0.5s for gzip, and the plugin's scheduler serializes quality >= 10 as a
|
||||
* high-memory operation. Quality 5 compresses in ~0.2s, still lands under gzip
|
||||
* (4.3MB vs 5.0MB), and parallelizes. `.br` is only served when
|
||||
* ENABLE_STATIC_ASSET_BROTLI is set, so the extra 0.5MB buys far less than it costs
|
||||
* on every build of every platform.
|
||||
*/
|
||||
algorithms: [
|
||||
defineAlgorithm('gzip', { level: 9 }),
|
||||
defineAlgorithm('brotliCompress', {
|
||||
params: { [constants.BROTLI_PARAM_QUALITY]: 5 },
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
optimizeDeps: {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue