Skip to content

Reduce the size of the Turbo binaries in the phpstan/phpstan package #15423

Closed
phpstan/phpstan-src
#6703

Description

@SanderMuller

Feature request

Since 2.3.0, phpstan/phpstan ships 32 Turbo binaries, and every install carries all of them. A project needs one. This issue collects measurements for the options that came up, so that we can pick a direction. It follows #15386 and #15410.

The decision to make is where the size goes away:

  1. in the download and in anything compressed, such as a CI artifact or a Docker layer,
  2. only on disk after install,
  3. in the phpstan/phpstan git history.

The options below differ on exactly that.

Option Composer download CI artifact (compressed) Disk Git history
Today 108.6 MB 108.6 MB 256 MB grows on every build
Zip the binaries inside the package no change no change ~140 MB no change
Put the binaries inside the phar no change no change ~140 MB no change
User deletes the other platforms after install no change 21.5 MB 65 MB no change
Download the matching binary on first run ~8 MB ~8 MB ~38 MB stops growing
Commit binaries only for a release no change no change no change grows per release only

The ~140, ~8 and ~38 MB values are estimates from the measured parts below. The other values are measured.

My lean is the first-run download, with the bundled binaries or no Turbo as the offline fallback. It is the only option that makes the download smaller, and it also shrinks the CI artifact. Zipping or phar-packing only helps the disk, because the Composer download and the CI artifact are already compressed.

Open questions for the first-run download:

  • bin/phpstan selects the binary and restarts the process before the Composer autoloader and before the config are loaded. That code may use only PHP 7.4 functions, so it has no Guzzle, no configured tmpDir and no proxy settings.
  • The phar must carry the expected hash of each binary, and the code must check it before it loads the file.
  • vendor/ can be read-only, so the binary needs another location. A shared temporary directory is not safe for a file that PHP loads as an extension.
  • Several processes can start at the same time on a fresh install.
  • An offline CI job must still work.

A workaround that works today: Turbo checks is_file() and runs without the extension when the file is missing. I tested this on 2.3.0 with only macos-arm64/ left (Turbo enabled) and with no binaries (Turbo off, analysis still works). A Composer post-install-cmd and post-update-cmd can run:

find vendor/phpstan/phpstan/turbo-ext -mindepth 1 -maxdepth 1 -type d ! -name linux-gnu-x86_64 -exec rm -rf {} +

Replace linux-gnu-x86_64 with the platform directory that the project uses.

Measurements

Package, from the GitHub zipballs. Disk sizes are from du -sh:

2.2.17 2.3.0
Composer download 13.8 MB 108.6 MB
Disk after install 52 MB 256 MB
turbo-ext/ on disk 23 MB 226 MB
One binary 0.35 to 1.1 MB 6.6 to 8.4 MB

The binaries are already stripped. In the Linux x86_64 binary, .text is 6.3 MB of 7.3 MB. The turbo-ext Makefile records that -Os takes a binary from 7.5 to 5.0 MB. It also makes the analysis 10.4% slower, or 6.7% slower with PGO.

Compression of turbo-ext/ in 2.3.0:

Size
Inside today's Composer zip 103.8 MB
zip -r 103.9 MB
zip -9 -r 103.5 MB
tar.gz 103.5 MB
tar.zst -19 --long=27 39.4 MB
tar.xz -9e 35.6 MB
One Linux x86_64 binary, gzip / xz 3.3 / 2.6 MB

The binaries of one platform are almost the same across PHP versions. Only an archive that compresses all files as one stream can use that, for example xz or zstd with a long window. PHP has no built-in xz or zstd decompression.

phpstan.phar stores its 7,183 files uncompressed (30.6 MB). gzip shrinks the whole file to 4.7 MB.

A .so can't be loaded from a phar:// path, so binaries in the phar must be extracted to disk first. The Phar extension reads zip files with its own code and only needs zlib, so extraction does not need ext-zip.

CI artifacts: GitLab's default limit is 100 MB per compressed artifact archive. phpstan/phpstan 2.3.0 alone is 108.6 MB compressed, which is consistent with the failure in #15410. With only linux-gnu-x86_64/ left, the package zips to 21.5 MB.

Git history: since 2026-07-14, 86 commits on 2.3.x changed turbo-ext/. They added 2,312 distinct binaries with 10.4 GB uncompressed: 7.6 GB in September and 2.3 GB in October up to 2026-10-06. GitHub reports the repository at 9.6 GB. I did not measure how well git compresses these files.

Activity

  1. rvanlaak commented on Oct 7, 2026

    @rvanlaak

    Installing the turbo extension would be a great task for a Composer post-install script - when you don't prefer letting pie handle it.

    Maybe a pie.json can be added so PIE can resolve that one of the composer packages expects an extension will be installed?

    cc @asgrim

  2. SanderMuller commented on Oct 7, 2026

    @SanderMuller
    ContributorAuthor

    Another use case comes from the 2.3.0 release discussion: PHPStan as a non-dev dependency in a Docker image. See #15385 (comment). The delete command in the issue can run there as a RUN step after composer install.

    It also changes my lean. Containers are often ephemeral, read-only or offline, so a download on the first run would repeat in every fresh container, or fail. The binary should come from an explicit step during composer install or the image build, as suggested above. The first run is then only a fallback.

  3. ondrejmirtes commented on Oct 8, 2026

    @ondrejmirtes
    Member

    These suggestions are missing an out-of-the-box idea I came up with - share common version-agnostic code in one dynamically loaded file, have much smaller version-specific extension files. This will bring down the size of the package tremendously. I'm currently working on that, stay tuned.

  4. talkinnl commented on Oct 8, 2026

    @talkinnl

    Shared code approach sounds sexy, I’m curious how much it’ll save! :)

    Sounds like the best option. Before we have that, the “User deletes the other platforms after install” trick is a fair workaround, it’s a low-code pragmatic tweak which already fixes the CI and Docker size regressions.

  5. ondrejmirtes commented on Oct 8, 2026

    @ondrejmirtes
    Member

    Work in progress phpstan/phpstan-src#6703

  6. rvanlaak commented on Oct 8, 2026

    @rvanlaak

    Great progress! We run PHPStan with Turbo in FrankenPHP (dunglas/frankenphp:php8.5, Debian, thread-safe). With no linux-gnu ZTS binary before 8.6, we build Turbo ourselves through PIE and have Renovate keep its version in step with phpstan/phpstan. Every FrankenPHP and php:*-zts user has to work this out on their own.

    The turbo-compile comment says 8.3–8.5 ZTS was left out because those hosts are "a few percent". After this PR, though, each extra leg is a ~70 KB thin extension against the existing phpstan_turbo_core.so, which Linux already shares between ZTS and NTS. So it costs a lot less now.

    Could you add gnu-only ZTS legs for 8.3/8.4/8.5? I expect the main cost is the turbo-build:gnu-php8.x-zts images. I'm happy to help with those if that's useful.

  7. ondrejmirtes commented on Oct 8, 2026

    @ondrejmirtes
    Member

    Now that adding more builds to the matrix means the package will only grow by a few dozens kilobytes, we can add back ZTS pretty cheaply. I'll add it and release PHPStan 2.3.1 with these improvements 😊

  8. ondrejmirtes commented on Oct 8, 2026

    @ondrejmirtes
    Member

    PHPStan 2.3.0: 104 MB download, 268 MB unzipped
    2.3.x: 27.4 MB download, 86 MB unzipped

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions