Под катом – анимация, демонстрирующая сборку приложения для macOS в режиме реального времени:
Я расскажу, как она получилась, но для начала обрисую контекст этого проекта.
Компиляция конкретного софта может быть очень длительной просто потому, что в этой программе очень много кода — как, например, в проекте LLVM. Но бывает и так, что сборка идёт медленно по глупым и вполне устранимым причинам. Подозреваю, что большинство сборок просто тормозят из-за ерунды, но проверить это мне пока не удавалось. Поэтому я разработал кроссплатформенный инструмент для визуализации сборок (пока он существует в приватной бета-версии, ссылка в конце статьи). Он работает с любой системой сборки и с любым языком программирования (а не только C/C++/Rust).
Это не просто универсальный профилировщик системы; вдобавок он помогает выявить проблемы, специфичные для сборок. Примеры таких проблем: использование make без флага -j, непропорционально длительная работа над некоторыми файлами или фазами компилятора (такие данные можно получить через специальные инструменты, например, -ftime-trace
из clang
). Также бывают команды, которые можно выполнять параллельно, а это сделано не было. Всё это особенно помогает при оптимизации сборок в ходе непрерывной интеграции, которые зачастую сводятся к простой пересборке.
Я назвал этот инструмент What the Fork по системному вызову fork(), который порождает новые процессы. Чтобы запустить инструмент, напишите wtf перед командой сборки:
$ # Несколько возможных примеров:
$ wtf make
$ wtf cargo build
$ wtf gradle build
$ wtf npm run build
$ wtf zig build
$ wtf -x # начинает сборку с самого первого окна Xcode
Так запускается пользовательский интерфейс, который обновляется по мере хода сборки.

В этом UI каждый из процессов, входящих в сборку, представлен в виде рамки, и цвет рамки соответствует типу процесса. Процессы выкладываются в хронологическом порядке слева направо. Дочерние процессы показаны как вложенные под своим родительским процессом. В панели, расположенной внизу окна, выводится информация о выбранном процессе: сколько времени он работал, в каком каталоге, а также полностью сообщается команда со всеми аргументами.
Как это работает
Сборка — это просто совокупность команд, после выполнения которых получается готовая программа. В качестве простейшей сборки может выступать шелл-скрипт примерно такого содержания:
#!/bin/bash
clang main.c -o program
Чтобы этот скрипт дал готовый результат, требуется задействовать три программы: bash
, clang
и — сюрприз! — ld
. Последняя программа – это линковщик, выполняемый в clang автоматически. Сборка может притормаживать из-за того, что в ней обнаруживаются какие-то непредвиденные этапы, и они особенно вероятны в сравнительно крупных проектах, где вместо шелл-скрипта могут выполняться такие вещи как cargo
, make
, bazel
, gradle
или xcodebuild
. Все эти инструменты просто выполняют команды, но вдобавок ещё и занимаются кэшированием, анализом зависимостей и планированием, так, чтобы выполнить минимальное количество работы максимально эффективно.
Притом, что в окне терминала вполне можно просматривать, какие команды выполняет сборочный инструмент, вы ничего не узнаёте о том, какие другие команды они выполняют в свою очередь (например, clang
выполняет ld
), а также не видите подробного тайминга! Поэтому, если мы хотим увидеть всё, что происходит при сборке, то придётся слушать системные вызовы, запускающие и завершающие процессы: fork
, exec
и exit
. В каждой операционной системе это делается по-своему:
В macOS предусмотрен Endpoint Security API
В Linux есть
ptrace()
В Windows есть «Худший API в истории программирования»: он отвечает за трассировку событий
Каждый из этих API по-своему неудобен, но все они действительно предоставляют всю необходимую информацию, по которой восстанавливается хронология событий. Ниже визуализировано выполнение нашего простого шелл-скрипта в версии What the Fork для macOS.

Бывалые читатели уже заметили, что все эти приёмы позволяют использовать приложение с какой угодно программой, запускающей подпроцессы — не только с инструментами сборки!
Некоторые мои наблюдения
Когда видишь свою сборку насквозь, тебе многое открывается. Мой инструмент уже попробовали в своих проектах программисты из Delta, Mozilla и Apple, и все они обнаружили что-то неожиданное. Позвольте привести несколько примеров.
Начну с опенсорсного проекта, для сборки которого используется cargo. Сосредоточимся на компиляции отдельно взятой зависимости:

Упс! Ничего не распараллелено! Файлы компилируются один за другим. Сборку можно было бы ускорить примерно в 10 раз, если бы cargo выполняла много команд сразу, например, у меня на M1 с десятиядерным ЦП. Без визуализации хронологии я бы этого никогда увидел. Если хотите посмотреть, как выглядит хороший параллелизм — взгляните, как ninja собирает проект llvm:

Ни одно из ядер на моей машине во время работы не простаивало. Кстати, тут небольшой перегруз — 12 заданий динамически конкурируют за 10 ядер, но это было сделано намеренно, чтобы некоторые из них блокировались на операциях ввода/вывода. Совершенство. Оно бывает скучным, но, всё-таки, давайте прсмотримся к проблеме. Вот крошечный фрагмент сборки CMake, взятый из другого опенсорсного проекта:

Здесь CMake узнаёт путь Xcode командой xcode-select -print-path
, версию операционной системы — командой sw_vers
, а затем для полноты картины несколько раз рекурсивно вызывает cmake/make
, после чего, наконец, компилирует и линкует файл.
Лишь в зелёных отрезках этой хронологии выполняется полезная работа. Может показаться, что CMake не делает вообще ничего полезного — в том смысле, что она просто собирает ту штуку, которая, в свою очередь, собирает проект. Как бы то ни было, просто примем как данность, что CMake требуется проделать эти странные па cmake->make->make-> с clang
, чтобы настроить окружение для разработки.
В более широком контексте выясняется, что этот странный танец происходит 85 раз!

Ого, никакого параллелизма. Кроме того, программа снова и снова проверяет путь Xcode и версию ОС — и так 85 раз, а вдруг версия операционной системы изменится прямо посреди сборки?
Ладно, довольно о cmake. Интересно исследовать и другие инструменты сборки! Вот как xcodebuild
собирает 100 000-строчный проект на Objective-C:

Обратите внимание: ближе к концу сборки начинаются пробелы, где одновременно работают всего один-два процесса clang, даже когда работы ещё хватает.
Кроме того, программа ничего не делает по 6 секунд перед тем, как приступить к любой полезной работе. Для сравнения: ninja требуется 0,4 секунды, чтобы приступить к компиляции проекта llvm, состоящего из 2 468 083 строк. Сравнивая Ninja с другими аналогичными инструментами, мы немного лукавим, поскольку он выигрывает благодаря «запечённой» в него сборочной логике, которую туда закладывает другой инструмент, создающий файл ninja. Но думаю, что такая «крейсерская скорость» — вполне нормальный бенчмарк при оценке скорости работы сборочных систем.
Продолжая экскурсию по различным сборочным инструментам, рассмотрим, как Zig компилирует Orca Project:

В данном случае интересно, что zig build собирает зависимости в случайном порядке (так выявляются проблемы упорядочивания в неверно сконфигурированных сборках). Таким образом, иногда ему просто везёт со сборкой, как в предыдущем примере, где он справился быстро. А иногда не везёт — как в следующем примере, где зависимость от curl оказалась назначена в саааамый конец, поэтому во всём остальном проекте не удалось задействовать параллелизм:

Наконец, вот как make/go компилирует проект github cli:

В обширной области слева, которая пока пуста, постепенно загружаются все зависимости проекта. Поэтому, если бы я хотел ускорить чистые сборки этого проекта, то в первую очередь занялся бы сокращением зависимостей. Зависимости — это своего рода «проклятье двойной сборки» — только вдумайтесь, как долго будут компилироваться зависимости от go-control-plane
, protobuf
, gojq
, т.д.
Это лишь небольшая выборка из всего того, что я узнал благодаря визуализации сборок. Разумеется, есть и множество других более тонких проблем, которые можно выявить, целиком рассматривая команду каждого процесса, но здесь я хотел сосредоточиться на тех вещах, которые можно показать наглядно.
What the Fork работает под Windows, Linux и macOS. Если хотите сами её попробовать и поделиться впечатлениями с автором — приватная бета-версия находится здесь.
sim2q
если бы оно еще ncurses поддерживало...