Наши партнеры

UnixForum





Библиотека сайта rus-linux.net

Ninja

Глава 3 из книги "Производительность приложений с открытым исходным кодом".

Оригинал: Ninja
Автор: Evan Martin
Перевод: А.Панин

Архитектура системы Ninja

На высоком уровне любая сборочная система выполняет три основных задачи. Она загружает директивы сборки (1) и анализирует ее цели, (2) устанавливает то, какие шаги необходимы для достижения этих целей и (3) выполняет эти шаги.

Для ускорения шага запуска (1) система Ninja должна выполнять минимальный объем работы в процессе загрузки файлов сборок. Системы сборки проектов обычно используются людьми, поэтому в них должен применяться удобный, высокоуровневый синтаксис для описания целей сборок. Это также означает, что непосредственно в момент начала сборки проекта сборочная система должна дополнительно обработать инструкции: например, в какой-то момент система сборки Visual Studio должна на основании конфигурации системы сборки четко определить место размещения выходных файлов или то, какие файлы должны быть скомпилированы с помощью компилятора языка C++, а какие - с помощью компилятора языка C.

В связи с этим, работа GYP по генерации файлов сборки Visual Studio была четко ограничена преобразованием списков файлов исходного кода в списки файлов с использованием синтаксиса Visual Studio, с последующим выполнением остальной части работы средствами Visual Studio. При работе над Ninja я столкнулся с возможностью переноса максимального объема работы на систему GYP. Я имею в виду однократное выполнение всех описанных выше операций в ходе генерации файлов сборки Ninja средствами GYP. После этого у GYP должна была быть возможность сохранения снимка данных промежуточного состояния в формате, который Ninja может быстро загрузить в ходе каждой из последующих сборок.

Поэтому язык, использующийся в файлах сборки системы Ninja, был простым до такой степени, чтобы человек имел возможность использовать его по назначению. В нем не было условных переходов или правил, базирующихся на расширениях файлов. Вместо этого содержимое файла было представлено в формате списка, состоящего из конкретных путей к исходным файлам и конкретных имен выходных файлов. Такие файлы могли быстро загружаться и практически не требовали интерпретации.

Такая минималистичная архитектура, как это не парадоксально, повышает гибкость инструмента. Из-за того, что система Ninja не имеет представления о таких стандартных высокоуровневых концепциях сборки, как выходная директория или текущая конфигурация, ее можно без лишних сложностей встроить в более сложные системы сборки (например, CMake, как мы увидим позднее), в которых сборочное окружение организуется по-другому. Например, система Ninja не устанавливает четкого ограничения того, должны ли результирующие файлы сборки (т.е., объектные файлы) размещаться в одной директории с файлами исходного кода (что считается плохим, приводящим к неразберихе методом размещения файлов некоторыми разработчиками) или в отдельной директории для результирующих файлов сборки (что считается затрудняющим понимание методом размещения файлов другими разработчиками). Спустя долгие годы после выпуска первой версии системы Ninja я наконец придумал правильную метафору для ее описания: если считать все другие системы сборки проектов компиляторами, то Ninja будет ассемблером.

Какую работу выполняет Ninja

Если система Ninja оставляет большую часть работы генератору файлов сборки, то какую работу остается выполнить ее силами? Описанная идеология хороша в принципе, но реальный мир всегда гораздо сложнее. Система Ninja развивалась, приобретая (и теряя) возможности в процессе разработки. В каждый момент времени мы задавались важным вопросом: "Можем ли мы выполнить меньший объем работы?" Ниже приведено краткое описание принципа работы системы.

Человеку требуется проводить отладку файлов в случае обнаружения ошибки в правилах сборки, поэтому файлы сборки .ninja представлены в простом текстовом формате, аналогичном формату файлов сборки Makefile, причем они поддерживают несколько абстракций для упрощения чтения.

Первой абстракцией является "правило", которое представляет вызов команды оболочки для отдельного инструмента. Впоследствии правило используется на различных этапах сборки. Ниже приведен пример синтаксиса файла системы Ninja с описанием правила под названием "compile", с помощью которого осуществляется вызов компилятора gcc с двумя объявлениями build, которые позволяют использовать его для компиляции заданных файлов.
rule compile
  command = gcc -Wall -c $in -o $out
build out/foo.o: compile src/foo.c
build out/bar.o: compile src/bar.c
Второй абстракцией является переменная. В примере выше переменные объявляются с использованием знака доллара ($in и $out). Переменные могут представлять и входные и выходные данные команды, а также могут использоваться для задания коротких имен длинных строк. Ниже приведен пример расширенного объявления правила компиляции, которое использует переменную для хранения флагов компилятора:
cflags = -Wall
rule compile
  command = gcc $cflags -c $in -o $out
Значения переменных, используемых при создании правила, могут быть ограничены рамками отдельного блока build путем их дополнительного описания. Продолжая рассмотрение описанного выше примера, мы можем преобразовать его таким образом, что значение переменной cflags будет применено к отдельному файлу:
build out/file_with_extra_flags.o: compile src/baz.c
  cflags = -Wall -Wextra

Правила ведут себя практически аналогично функциям, а переменные - аргументам. Две этих простых возможности находятся в опасной близости от возможностей языка программирования - что не соответствует цели "избавления от дополнительной работы". При этом они имеют важное достоинство, заключающееся в возможности сокращения повторений строк, что полезно не только для людей, но и для компьютеров, так как сокращается объем текста, который необходимо разобрать.

Файл сборки после разбора представляет граф зависимостей: результирующий выходной двоичный файл создается в результате связывания множества объектных файлов, каждый из которых, в свою очередь, создается в результате компиляции файла исходного кода. В нашем случае мы имеем дело с двудольным графом, в котором "узлы" (входные файлы) связаны с "ребрами" (командами сборки), которые в свою очередь связаны с узлами (результирующими файлами)6. Впоследствии в процессе сборки производится обход данного графа.

После задания целевого файла Ninja в первую очередь обходит граф для установления параметров состояния каждого из файлов исходного кода, представленного вершиной: существует ли данный файл и какова метка времени его модификации. После этого Ninja создает план (plan). План является набором ребер, причем каждое представленное ребром правило должно быть исполнено для обновления результирующего файла в соответствии с метками времени модификации промежуточных файлов. Наконец, после выполнения плана происходит обратный обход графа с целью проверки выполнения и корректного завершения представленных ребрами правил.

После реализации всех элементов данного механизма я мог провести базовую проверку производительности системы при работе с Chrome: мне нужно было измерить время работы Ninja при повторном запуске после успешного завершения сборки. Это время, которое тратится на загрузку файлов сборки, проверку состояния сборки и установление того, что никакой работы выполнять не требуется. Время, полученное входе данного тестирования производительности, равнялось примерно одной секунде. Это был мой новый начальный показатель производительности. Однако, в процессе развития Chrome, система Ninja должна была работать быстрее для предотвращения регрессивного изменения данного показателя производительности.


Продолжение статьи: Оптимизация системы Ninja.