Оптимизация производительности в Composer 2.2

В декабре 2021 года вышло обновление пакетного менеджера Composer, версия 2.2. Заявлено увеличение производительност в некоторых случаях на 90%.

https://blog.packagist.com/composer-2-2/

Как это возможно и почему Composer раньше был на столько прожорливым? Я изучил изменения в исходном коде и вот что я нашел…

Релиз ноты содержат ссылки на два Pull Request:

Я изучил эти Pull Requests, хотел найти что-то интересное и полезное, возможно, применимое в моей работе. Например, какие-то хитрые трюки по использованию SplFixedArray вместо Array или определённый стиль работы с объектами, помогающий PHP интерпретатору или JIT компилятору лучше оптимизировать исполняемый код.

На деле всё оказалось банальнее: алгоритмы и ещё раз алгоритмы, ничего PHP-специфичного.

Внутри Composer есть некий объект Pool в который записывается информация о всех версиях пакетов удовлетворяющих граничным условиями описанным в composer.json. Позже этот Pool передаётся на вход некоему Solver, который решает задачу выбора не противоречивого набора версий. Чем больше пакетов передадим в Solver, тем сложнее Solver’у найти решение. Как пишет автор PR сложность возрастает экспоненциально.

Решение: передавать в Solver как можно меньше вариантов пакетов. Но разве можно просто так взять и выкинуть часть пакетов из Pool? Автор этого улучшения предположил, что можно. Он приводит следующий пример: если в нашем проекте используется пакет symfony/routing и в composer.json указана версия 4.4, то скорее всего нам подойдёт любая минорная версия в интервале от 4.4.0 и до последней 4.4.х. Подойдёт с точки зрения удовлетворения зависимостей. Какую выбрать, первую или последнюю зависит от флагов --prefer-stable и --prefer-lowest.

Раньше Pool содержал в себе информацию от всех версиях пакета symfony/routing от 4.4.0 вплоть до последней 4.4.x и это создавало много работы для процесса разрешения зависимостей в Solver.

После оптимизации, т. е. начиная с Composer 2.2, в Pool содержится только одна версия пакета, и это значительно упрощает работу для Solver.

Оптимизация будет работать надёжно и верно лишь в том случае, если в рамках минорных версий пакет не менял свои зависимости описанные в requirereplaceprovide и conflict. В противном случае, нельзя просто так взять и выкинуть из Pool часть минорных версий. Оптимизация не 100% надёжна, поэтому если на вашем проекте после перехода на Composer 2.2 не удаётся обновить зависимости из-за конфликтов, возможно Solver не смог подобрать подходящую комбинацию версий разных пакетов именно из-за того, что часть минорных версий была выброшена из анализа. Поэтому предусмотрена переменная окружения COMPOSER_POOL_OPTIMIZER=0 отключающая данную оптимизацию.

Естественно, всё это имеет значение только для команды composer update, когда требуется выбрать наилучшие версии пакетов подходящие под ограничения в файле composer.json. Если же мы делаем composer install – эти оптимизации не применяются, т. к. устанавливаются конкретные версии зафиксированные в composer.lock файле.

Тут мне в голову пришло интересное размышление. Мы знаем, что чем меньше версий пакетов требуется для анализа, тем быстрее composer update отработает. Значит при выпуске новой мажорной версии фреймворка, как только мы на него переходим и минорных версий пока нет, команда composer update должна отрабатывать максимально быстро. А с течением времени и появлением минорных версий пакетов composer update должен был работать всё медленнее и медленнее. Интересно, кто-нибудь замечал этот эффект?

Я лично сравнил работу composer update на одном из своих Laravel проектов с включённой оптимизацией Pool’а и с выключенной. Запускал командой composer update -v –dry-run–profile.

Без оптимизации максимальное использование памяти составило 2Гб и время работы 19 секунд. При этом было проанализировано 19 471 пакетов и 3 635 420 правил для разрешения зависимостей.

С оптимизацией максимальное использование памяти составило 1.3Гб и 80 секунд. При этом было проанализировано 5 715 пакетов и 2 190 540 правил для разрешения зависимостей.

Неожиданные результаты – уменьшение потребления памяти на 35%, уменьшение кол-ва пакетов для анализа почти в 4, но при этом увеличение времени работы в 4 раза!  Запускал несколько раз. И это точно не проблема с сетью, для тестов я использовал переменную окружения COMPOSER_DISABLE_NETWORK=1, полагаясь исключительно на локально закешированную информацию.

Выводы: если после обновления composer до версии 2.2 вы внезапно столкнётесь с увеличением времени работы команды composer update, попробуйте отключить новую оптимизацию пула с помощью переменной окружения COMPOSER_POOL_OPTIMIZER=0.

Что дальше? Я проверил issues в репозитории composer и не нашел похожего случая. Некоторые выкладывают свои бенчмарки, бывают просадки по времени установки на 10-15%, но не в 4 раза!

Надо собрать минимально воспроизводимый пример и засабмитить.