Browse Source

feat: option to use mimalloc for static builds (#666)

* feat: use mimalloc for static builds

* fix: use Tweag's approach

* fix: debug build

* chore: mark USE_MIMALLOC as experimental

* ci: build a static binary using mimalloc
Kévin Dunglas 10 months ago
parent
commit
498294a561
5 changed files with 112 additions and 22 deletions
  1. 18 13
      .github/workflows/static.yaml
  2. 90 9
      build-static.sh
  3. 1 0
      docs/fr/static.md
  4. 1 0
      docs/static.md
  5. 2 0
      static-builder.Dockerfile

+ 18 - 13
.github/workflows/static.yaml

@@ -84,7 +84,11 @@ jobs:
             platform: linux/amd64
             qemu: false
             debug: true
-    name: Build ${{ matrix.platform }} static binary${{ matrix.debug && ' (debug)' || '' }}
+          -
+            platform: linux/amd64
+            qemu: false
+            mimalloc: true
+    name: Build ${{ matrix.platform }} static binary${{ matrix.debug && ' (debug)' || '' }}${{ matrix.mimalloc && ' (mimalloc)' || '' }}
     runs-on: ubuntu-latest
     needs: [ prepare ]
     steps:
@@ -121,12 +125,13 @@ jobs:
           targets: static-builder
           set: |
             ${{ matrix.debug && 'static-builder.args.DEBUG_SYMBOLS=1' || '' }}
+            ${{ matrix.mimalloc && 'static-builder.args.MIMALLOC=1' || '' }}
             *.tags=
             *.platform=${{ matrix.platform }}
-            *.cache-from=type=gha,scope=${{ needs.prepare.outputs.ref || github.ref }}-static-builder${{ matrix.debug && '-debug' && '' }}
-            *.cache-from=type=gha,scope=refs/heads/main-static-builder${{ matrix.debug && '-debug' && '' }}
-            *.cache-to=type=gha,scope=${{ needs.prepare.outputs.ref || github.ref }}-static-builder${{ matrix.debug && '-debug' && '' }},ignore-error=true
-            ${{ (fromJson(needs.prepare.outputs.push) && !matrix.debug) && format('*.output=type=image,name={0},push-by-digest=true,name-canonical=true,push=true', env.IMAGE_NAME) || '' }}
+            *.cache-from=type=gha,scope=${{ needs.prepare.outputs.ref || github.ref }}-static-builder${{ matrix.debug && '-debug' && '' }}${{ matrix.mimalloc && '-mimalloc' && '' }}
+            *.cache-from=type=gha,scope=refs/heads/main-static-builder${{ matrix.debug && '-debug' && '' }}${{ matrix.mimalloc && '-mimalloc' && '' }}
+            *.cache-to=type=gha,scope=${{ needs.prepare.outputs.ref || github.ref }}-static-builder${{ matrix.debug && '-debug' && '' }}${{ matrix.mimalloc && '-mimalloc' && '' }},ignore-error=true
+            ${{ (fromJson(needs.prepare.outputs.push) && !matrix.debug && !matrix.mimalloc) && format('*.output=type=image,name={0},push-by-digest=true,name-canonical=true,push=true', env.IMAGE_NAME) || '' }}
         env:
           SHA: ${{ github.sha }}
           VERSION: ${{ (github.ref_type == 'tag' && github.ref_name) || needs.prepare.outputs.ref || github.sha }}
@@ -145,7 +150,7 @@ jobs:
           METADATA: ${{ steps.build.outputs.metadata }}
       -
         name: Upload metadata
-        if: fromJson(needs.prepare.outputs.push) && !matrix.debug
+        if: fromJson(needs.prepare.outputs.push) && !matrix.debug && !matrix.mimalloc
         uses: actions/upload-artifact@v3
         with:
           name: metadata-static-builder
@@ -154,11 +159,11 @@ jobs:
           retention-days: 1
       -
         name: Copy binary
-        if: ${{ !fromJson(needs.prepare.outputs.push) || matrix.debug }}
+        if: ${{ !fromJson(needs.prepare.outputs.push) || matrix.debug || matrix.mimalloc }}
         run: |
           digest=$(jq -r '."static-builder"."containerimage.config.digest"' <<< "${METADATA}")
           docker create --platform=${{ matrix.platform }} --name static-builder "${digest}"
-          docker cp "static-builder:/go/src/app/dist/${BINARY}" "${BINARY}${{ matrix.debug && '-debug' || '' }}"
+          docker cp "static-builder:/go/src/app/dist/${BINARY}" "${BINARY}${{ matrix.debug && '-debug' || '' }}${{ matrix.mimalloc && '-mimalloc' || '' }}"
         env:
           METADATA: ${{ steps.build.outputs.metadata }}
           BINARY: frankenphp-linux-${{ matrix.platform == 'linux/amd64' && 'x86_64' || 'aarch64' }}
@@ -167,12 +172,12 @@ jobs:
         if: ${{ !fromJson(needs.prepare.outputs.push) }}
         uses: actions/upload-artifact@v3
         with:
-          name: frankenphp-linux-${{ matrix.platform == 'linux/amd64' && 'x86_64' || 'aarch64' }}${{ matrix.debug && '-debug' || '' }}
-          path: frankenphp-linux-${{ matrix.platform == 'linux/amd64' && 'x86_64' || 'aarch64' }}${{ matrix.debug && '-debug' || '' }}
+          name: frankenphp-linux-${{ matrix.platform == 'linux/amd64' && 'x86_64' || 'aarch64' }}${{ matrix.debug && '-debug' || '' }}${{ matrix.mimalloc && '-mimalloc' || '' }}
+          path: frankenphp-linux-${{ matrix.platform == 'linux/amd64' && 'x86_64' || 'aarch64' }}${{ matrix.debug && '-debug' || '' }}${{ matrix.mimalloc && '-mimalloc' || '' }}
       -
-        name: Upload debug asset
-        if: fromJson(needs.prepare.outputs.push) && matrix.debug && (needs.prepare.outputs.ref || github.ref_type == 'tag')
-        run: gh release upload "${{ (github.ref_type == 'tag' && github.ref_name) || needs.prepare.outputs.ref }}" frankenphp-linux-x86_64-debug --repo dunglas/frankenphp --clobber
+        name: Upload special assets
+        if: fromJson(needs.prepare.outputs.push) && (matrix.debug || matrix.mimalloc) && (needs.prepare.outputs.ref || github.ref_type == 'tag')
+        run: gh release upload "${{ (github.ref_type == 'tag' && github.ref_name) || needs.prepare.outputs.ref }}" frankenphp-linux-x86_64${{ matrix.debug && '-debug' || '' }}${{ matrix.mimalloc && '-mimalloc' || '' }} --repo dunglas/frankenphp --clobber
         env:
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
 

+ 90 - 9
build-static.sh

@@ -1,6 +1,7 @@
 #!/bin/sh
 
 set -o errexit
+set -x
 
 if ! type "git" > /dev/null; then
     echo "The \"git\" command must be installed."
@@ -15,6 +16,11 @@ if [ "${os}" = "darwin" ]; then
     md5binary="md5 -q"
 fi
 
+if [ "${os}" = "linux" ] && ! type "cmake" > /dev/null; then
+    echo "The \"cmake\" command must be installed."
+    exit 1
+fi
+
 if [ -z "${PHP_EXTENSIONS}" ]; then
     export PHP_EXTENSIONS="apcu,bcmath,bz2,calendar,ctype,curl,dba,dom,exif,fileinfo,filter,gd,iconv,igbinary,intl,ldap,mbregex,mbstring,mysqli,mysqlnd,opcache,openssl,pcntl,pdo,pdo_mysql,pdo_pgsql,pdo_sqlite,pgsql,phar,posix,readline,redis,session,simplexml,sockets,sodium,sqlite3,sysvsem,tokenizer,xml,xmlreader,xmlwriter,zip,zlib"
 fi
@@ -23,7 +29,7 @@ if [ -z "${PHP_EXTENSION_LIBS}" ]; then
     export PHP_EXTENSION_LIBS="bzip2,freetype,libavif,libjpeg,liblz4,libwebp,libzip"
 fi
 
-# the Brotli library must always be built as it is required by http://github.com/dunglas/caddy-cbrotli
+# The Brotli library must always be built as it is required by http://github.com/dunglas/caddy-cbrotli
 if ! echo "${PHP_EXTENSION_LIBS}" | grep -q "\bbrotli\b"; then
     export PHP_EXTENSION_LIBS="${PHP_EXTENSION_LIBS},brotli"
 fi
@@ -117,21 +123,88 @@ if [ "${os}" = "mac" ]; then
     export CGO_LDFLAGS="-framework CoreFoundation -framework SystemConfiguration"
 fi
 
-CGO_LDFLAGS="${CGO_LDFLAGS} ${PWD}/buildroot/lib/libbrotlicommon.a ${PWD}/buildroot/lib/libbrotlienc.a ${PWD}/buildroot/lib/libbrotlidec.a $(./buildroot/bin/php-config --ldflags) $(./buildroot/bin/php-config --libs)"
+CGO_LDFLAGS="${CGO_LDFLAGS} ${PWD}/buildroot/lib/libbrotlicommon.a ${PWD}/buildroot/lib/libbrotlienc.a ${PWD}/buildroot/lib/libbrotlidec.a $(./buildroot/bin/php-config --ldflags || true) $(./buildroot/bin/php-config --libs || true)"
 export CGO_LDFLAGS
 
 LIBPHP_VERSION="$(./buildroot/bin/php-config --version)"
 export LIBPHP_VERSION
 
-cd ../..
-
-# Embed PHP app, if any
-if [ -n "${EMBED}" ] && [ -d "${EMBED}" ]; then
-    tar -cf app.tar -C "${EMBED}" .
-    ${md5binary} app.tar > app_checksum.txt
-fi
+cd ../
 
 if [ "${os}" = "linux" ]; then
+    if  [ -n "${MIMALLOC}" ]; then
+        # Replace musl's mallocng by mimalloc
+        # The default musl allocator is slow, especially when used by multi-threaded apps,
+        # and triggers weird bugs
+        # Adapted from https://www.tweag.io/blog/2023-08-10-rust-static-link-with-mimalloc/
+
+        echo 'The USE_MIMALLOC environment variable is EXPERIMENTAL.'
+        echo 'This option can be removed or its behavior modified at any time.'
+
+        if [ ! -f "mimalloc/out/libmimalloc.a" ]; then
+            if [ -d "mimalloc" ]; then
+                cd mimalloc/
+                git reset --hard
+                git clean -xdf
+                git fetch --tags
+            else
+                git clone https://github.com/microsoft/mimalloc.git
+                cd mimalloc/
+            fi
+
+            git checkout "$(git describe --tags "$(git rev-list --tags --max-count=1 || true)" || true)"
+
+            curl -f -L --retry 5 https://raw.githubusercontent.com/tweag/rust-alpine-mimalloc/b26002b49d466a295ea8b50828cb7520a71a872a/mimalloc.diff -o mimalloc.diff
+            patch -p1 < mimalloc.diff
+
+            mkdir -p out/
+            cd out/
+            if [ -n "${DEBUG_SYMBOLS}" ]; then
+                cmake \
+                    -DCMAKE_BUILD_TYPE=Debug \
+                    -DMI_BUILD_SHARED=OFF \
+                    -DMI_BUILD_OBJECT=OFF \
+                    -DMI_BUILD_TESTS=OFF \
+                    ../
+            else
+                cmake \
+                    -DCMAKE_BUILD_TYPE=Release \
+                    -DMI_BUILD_SHARED=OFF \
+                    -DMI_BUILD_OBJECT=OFF \
+                    -DMI_BUILD_TESTS=OFF \
+                    ../
+            fi
+            make -j"$(nproc || true)"
+
+            cd ../../
+        fi
+
+        if [ -n "${DEBUG_SYMBOLS}" ]; then
+            libmimalloc_path=mimalloc/out/libmimalloc-debug.a
+        else
+            libmimalloc_path=mimalloc/out/libmimalloc.a
+        fi
+
+        # Patch musl library to use mimalloc
+        for libc_path in "/usr/local/musl/lib/libc.a" "/usr/local/musl/$(uname -m)-linux-musl/lib/libc.a" "/usr/lib/libc.a"
+        do
+            if [ ! -f "${libc_path}" ] || [ -f "${libc_path}.unpatched" ]; then
+                continue
+            fi
+
+            {
+                echo "CREATE libc.a"
+                echo "ADDLIB ${libc_path}"
+                echo "DELETE aligned_alloc.lo calloc.lo donate.lo free.lo libc_calloc.lo lite_malloc.lo malloc.lo malloc_usable_size.lo memalign.lo posix_memalign.lo realloc.lo reallocarray.lo valloc.lo"
+                echo "ADDLIB ${libmimalloc_path}"
+                echo "SAVE"
+            } | ar -M
+            mv "${libc_path}" "${libc_path}.unpatched"
+            mv libc.a "${libc_path}"
+        done
+    fi
+
+    # Increase the default stack size to prevents issues with code including many files such as Symfony containers
     extraExtldflags="-Wl,-z,stack-size=0x80000"
 fi
 
@@ -139,6 +212,14 @@ if [ -z "${DEBUG_SYMBOLS}" ]; then
     extraLdflags="-w -s"
 fi
 
+cd ../
+
+# Embed PHP app, if any
+if [ -n "${EMBED}" ] && [ -d "${EMBED}" ]; then
+    tar -cf app.tar -C "${EMBED}" .
+    ${md5binary} app.tar > app_checksum.txt
+fi
+
 cd caddy/frankenphp/
 go env
 go build -buildmode=pie -tags "cgo netgo osusergo static_build" -ldflags "-linkmode=external -extldflags '-static-pie ${extraExtldflags}' ${extraLdflags} -X 'github.com/caddyserver/caddy/v2.CustomVersion=FrankenPHP ${FRANKENPHP_VERSION} PHP ${LIBPHP_VERSION} Caddy'" -o "../../dist/${bin}"

+ 1 - 0
docs/fr/static.md

@@ -77,4 +77,5 @@ Les variables d'environnement suivantes peuvent être transmises à `docker buil
 * `CLEAN` : lorsque défini, `libphp` et toutes ses dépendances sont construites à partir de zéro (pas de cache)
 * `DEBUG_SYMBOLS` : lorsque défini, les symboles de débogage ne seront pas supprimés et seront ajoutés dans le binaire
 * `NO_COMPRESS`: ne pas compresser le binaire avec UPX
+* `MIMALLOC`: (expérimental, Linux seulement) remplace l'allocateur mallocng de musl par [mimalloc](https://github.com/microsoft/mimalloc) pour des performances améliorées
 * `RELEASE` : (uniquement pour les mainteneurs) lorsque défini, le binaire résultant sera uploadé sur GitHub

+ 1 - 0
docs/static.md

@@ -79,4 +79,5 @@ script to customize the static build:
 * `CLEAN`: when set, libphp and all its dependencies are built from scratch (no cache)
 * `NO_COMPRESS`: don't compress the resulting binary using UPX
 * `DEBUG_SYMBOLS`: when set, debug-symbols will not be stripped and will be added within the binary
+* `MIMALLOC`: (experimental, Linux-only) replace musl's mallocng by [mimalloc](https://github.com/microsoft/mimalloc) for improved performance
 * `RELEASE`: (maintainers only) when set, the resulting binary will be uploaded on GitHub

+ 2 - 0
static-builder.Dockerfile

@@ -12,6 +12,7 @@ ARG PHP_EXTENSION_LIBS=''
 ARG CLEAN=''
 ARG EMBED=''
 ARG DEBUG_SYMBOLS=''
+ARG MIMALLOC=''
 
 SHELL ["/bin/ash", "-eo", "pipefail", "-c"]
 
@@ -24,6 +25,7 @@ LABEL org.opencontainers.image.vendor="Kévin Dunglas"
 
 RUN apk update; \
 	apk add --no-cache \
+		alpine-sdk \
 		autoconf \
 		automake \
 		bash \