...

среда, 6 ноября 2013 г.

GitPHP в Badoo

Badoo — это проект с гигантским git-репозиторием, в котором есть тысячи веток и тегов. Мы используем сильно модифицированный GitPHP (http://gitphp.org) версии 0.2.4, над которой сделали множество надстроек (включая интеграцию с нашим workflow в JIRA, организацию процесса ревью и т.д.). В целом нас этот продукт устраивал, пока мы не стали замечать, что наш основной репозиторий открывается более 20 секунд. И сегодня мы расскажем о том, как мы исследовали производительность GitPHP и каких результатов добились, решая эту проблему.

Расстановка таймеров




При разработке badoo.com в девелоперском окружении мы используем весьма простую debug-панель для расстановки таймеров и отладки SQL-запросов. Поэтому первым делом мы переделали ее в GitPHP и стали измерять время выполнения участков кода, не учитывая вложенные таймеры. Вот так выглядит наша debug-панель:


В первой колонке находится имя вызываемого метода (или действия), во второй — дополнительная информация: аргументы для запуска, начало вывода команды и trace. В последнем столбце находится потраченное на вызов время (в секундах).



Вот небольшая выдержка из реализации самих таймеров:



<?php
class GitPHP_Log {
// ...
public function timerStart() {
array_push($this->timers, microtime(true));
}

public function timerStop($name, $value = null) {
$timer = array_pop($this->timers);
$duration = microtime(true) - $timer;
// Вычтем потраченное время из всех таймеров, которые включают этот таймер
foreach ($this->timers as &$item) $item += $duration;
$this->Log($name, $value, $duration);
}
// ...
}




Использование такого API очень простое. В начале измеряемого кода вызывается timerStart(), в конце — timerStop() с именем таймера и опциональными дополнительными данными:

<?php
$Log = new GitPHP_Log;
$Log->timerStart();

$result = 0;
$mult = 4;
for ($i = 1; $i < 1000000; $i+=2) {
$result += $mult / $i;
$mult = -$mult;
}

$Log->timerStop("PI computation", $result);




При этом вызовы могут быть вложенными, и приведенный выше класс учтет это при подсчете.

Для более легкой отладки кода внутри Smarty мы сделали «автотаймеры». Они позволяют легко измерять время, потраченное на работу методов с множеством точек выхода (много мест, где выполняется return):



<?php
class GitPHP_DebugAutoLog {
private $name;
public function __construct($name) {
$this->name = $name;
GitPHP_Log::GetInstance()->timerStart();
}

public function __destruct() {
GitPHP_Log::GetInstance()->timerStop($this->name);
}
}




Использовать такой класс очень просто: нужно вставить $Log = new GitPHP_DebugAutoLog(‘timer_name’); в начало любой функции или метода, и при выходе из функции будет автоматически измерено время ее исполнения:

<?php
function doSomething($a) {
$Log = GitPHP_DebugAutoLog('doSomething');
if ($a > 5) {
echo "Hello world!\n";
sleep(5);
return;
}
sleep(1);
}




Тысячи вызовов git cat-file -t <commit>




Благодаря расставленным таймерам мы быстро смогли найти, где GitPHP версии 0.2.4 тратил большую часть времени. На каждый тег в репозитории делался один вызов git cat-file -t только для того, чтобы узнать тип коммита, и является ли этот коммит «легковесным тегом» (http://git-scm.com/book/en/Git-Basics-Tagging#Lightweight-Tags). Легковесные теги в Git — это тип тега, который создается по умолчанию и содержит ссылку на конкретный коммит. Поскольку в нашем репозитории никакие другие типы тегов не присутствовали, мы просто убрали эту проверку и сэкономили пару тысяч вызовов git cat-file -t, занимавших около 20 секунд.

Как так получилось, что GitPHP нужно было для каждого тега в репозитории узнавать, является ли он «легковесным»? Все довольно просто.


На всех страницах GitPHP рядом с коммитом выводятся ветки и теги, которые на него указывают:



Для этого в классе GitPHP_TagList есть метод, который отвечает за получение списка тегов, ссылающихся на указанный коммит:



<?php
class GitPHP_TagList extends GitPHP_RefList {
// ...
public function GetCommitTags($commit) {
if (!$commit) return array();
$commitHash = $commit->GetHash();
if (!$this->dataLoaded) $this->LoadData();
$tags = array();
foreach ($this->refs as $tag => $hash) {
if (isset($this->commits[$tag])) {
// ...
} else {
$tagObj = $this->project->GetObjectManager()->GetTag($tag, $hash);
$tagCommitHash = $tagObj->GetCommitHash();
// ...
if ($tagCommitHash == $commitHash) {
$tags[] = $tagObj;
}
}
}
return $tags;
}
// ...
}




Т.е. для каждого коммита, для которого нужно получить список тегов, выполняется следующее:

  1. При первом вызове загружается список всех тегов в репозитории (вызов LoadData()).

  2. Перебирается список всех тегов.

  3. Для каждого тега загружается соответствующий ему объект.

  4. Вызывается GetCommitHash() у объекта тега и полученное значение сравнивается с искомым.




Помимо того что можно сначала составить карту вида array( commit_hash => array(tags) ), нужно обратить внимание на метод GetCommitHash(): он вызывает метод Load($tag), который в реализации с использованием внешней утилиты Git делает следующее:

<?php
class GitPHP_TagLoad_Git implements GitPHP_TagLoadStrategy_Interface {
// ...
public function Load($tag) {
// ...
$args[] = '-t';
$args[] = $tag->GetHash();
$ret = trim($this->exe->Execute($tag->GetProject()->GetPath(), GIT_CAT_FILE, $args));

if ($ret === 'commit') {
// ...
return array(/* ... */);
}
// ...
$ret = $this->exe->Execute($tag->GetProject()->GetPath(), GIT_CAT_FILE, $args);
// ...
return array(/* ... */);
}
}




Т.е. чтобы показать, какие ветки и теги входят в какой-либо коммит, GitPHP загружает список всех тегов и вызывает git cat-file -t для каждого из них. Неплохо, Кристофер, так держать!

Сотни вызовов git rev-list --max-count=1 … <commit>




Аналогичная ситуация и с информацией о коммите. Чтобы загрузить дату, сообщение коммита, автора и т.д, каждый раз вызывался git rev-list --max-count=1 … . Эта операция тоже не является бесплатной:

<?php
class GitPHP_CommitLoad_Git extends GitPHP_CommitLoad_Base {
public function Load($commit) {
// ...
/* get data from git_rev_list */
$args = array();
$args[] = '--header';
$args[] = '--parents';
$args[] = '--max-count=1';
$args[] = '--abbrev-commit';
$args[] = $commit->GetHash();
$ret = $this->exe->Execute($commit->GetProject()->GetPath(), GIT_REV_LIST, $args);
// ...
return array(
// ...
);
}
// ...
}




Решение: пакетная загрузка коммитов (git cat-file --batch)




Для того чтобы не делать много одиночных обращений к git cat-file, Git позволяет загружать сразу много коммитов с использованием опции --batch. При этом он принимает список коммитов в stdin, а результат записывает в stdout. Соответственно, можно сначала записать в файл все хэши коммитов, которые нам нужны, запустить git cat-file --batch и загрузить сразу все результаты.

Вот пример кода, который это делает (код приведен для версии GitPHP 0.2.4 и операционных систем семейства *nix):



<?php
class GitPHP_Project {
// ...
public function BatchReadData(array $hashes) {
if (!count($hashes)) return array();
$outfile = tempnam('/tmp', 'objlist');
$hashlistfile = tempnam('/tmp', 'objlist');
file_put_contents($hashlistfile, implode("\n", $hashes));
$Git = new GitPHP_GitExe($this);
$Git->Execute(GIT_CAT_FILE, array('--batch', ' < ' . escapeshellarg($hashlistfile), ' > ' . escapeshellarg($outfile)));
unlink($hashlistfile);
$fp = fopen($outfile, 'r');
unlink($outfile);

$types = $contents = array();
while (!feof($fp)) {
$ln = rtrim(fgets($fp));
if (!$ln) continue;
list($hash, $type, $n) = explode(" ", rtrim($ln));
$contents[$hash] = fread($fp, $n);
$types[$hash] = $type;
}

return array('contents' => $contents, 'types' => $types);
}
// ...
}




Мы стали использовать эту функцию для большей части страниц, где показывается информация о коммитах (т.е. мы собираем список коммитов и загружаем их все одним вызовом git cat-file --batch). Такая оптимизация сократила среднее время загрузки страницы с 20 с лишним секунд до 0,5 секунды. Таким образом мы решили проблему медленной работы GitPHP в нашем проекте.

Open-source: оптимизации GitPHP 0.2.9 (master)




Немного подумав, мы поняли, что можно было не переписывать весь код для использования git cat-file --batch. Хоть это и не отражено в документации, эта команда позволяет загружать информацию по одному коммиту за раз, не теряя в производительности! Во время работы производится чтение по одной строке из стандартного ввода и отправка результатов в стандартный вывод без их буферизации. Это означает, что мы можем открыть git cat-file --batch через proc_open() и получать результаты немедленно, без переделывания архитектуры!

Вот выдержка из реализации (для удобства чтения обработка ошибок убрана):



<?php
// ...
class GitPHP_GitExe implements GitPHP_Observable_Interface {
// ...
public function GetObjectData($projectPath, $hash) {
$process = $this->GetProcess($projectPath);
$pipes = $process['pipes'];
$data = $hash . "\n";
fwrite($pipes[0], $data);
fflush($pipes[0]);

$ln = rtrim(fgets($pipes[1]));
$parts = explode(" ", rtrim($ln));
list($hash, $type, $n) = $parts;
$contents = '';
while (strlen($contents) < $n) {
$buf = fread($pipes[1], min(4096, $n - strlen($contents)));
$contents .= $buf;
}

return array(
'contents' => $contents,
'type' => $type,
);
}
// ...
}




Учитывая, что мы теперь можем очень быстро загружать содержимое объектов, не делая каждый раз вызов команды git, получить большой прирост производительности стало просто: достаточно лишь поменять все вызовы git cat-file и git rev-list на вызов нашей оптимизированной функции.

Мы собрали все изменения в один коммит и отправили pull-request разработчику GitPHP. Через какое-то время патч приняли! Вот этот коммит:


source.gitphp.org/projects/gitphp.git/commitdiff/3c87676b3afe4b0c1a1f7198995cecc176200482


Автором были внесены некоторые исправления в код (отдельными коммитами), и сейчас в ветке master находится значительно ускоренная версия GitPHP! Для использования оптимизаций требуется выключить «режим совместимости», то есть поставить $compat = false; в конфигурации.


Юрий youROCK Насретдинов, PHP-разработчик, Badoo

Евгений eZH Махров, QA-инженер, Badoo


This entry passed through the Full-Text RSS service — if this is your content and you're reading it on someone else's site, please read the FAQ at fivefilters.org/content-only/faq.php#publishers. Five Filters recommends:



Комментариев нет:

Отправить комментарий