git-bisect-lk2009
Аннотация
"git bisect" позволяет пользователям и разработчикам программного обеспечения легко найти коммит, который внёс регрессию. Мы показываем, почему важно иметь хорошие инструменты для борьбы с регрессиями. Мы описываем, как работает "git bisect" снаружи и какие алгоритмы он использует внутри. Затем мы объясняем, как использовать "git bisect", чтобы улучшить текущие практики. И мы обсуждаем, как "git bisect" можно улучшить в будущем.
Введение в "git bisect"
Git — это система распределенного контроля версий (DVCS), созданная Линусом Торвальдсом и поддерживаемая Джунио Хамано.
В Git, как и во многих других системах контроля версий (VCS), различные состояния управляемых данных называются коммитами. И поскольку системы VCS в основном используются для управления исходным кодом программного обеспечения, иногда в некоторых коммитах вносятся «интересные» изменения в поведении программного обеспечения.
На самом деле людей особенно интересуют коммиты, которые вводят «плохое» поведение, называемое ошибкой или регрессией. Их интересуют эти коммиты, потому что коммит (в идеале) содержит очень небольшой набор изменений исходного кода. И намного легче понять и исправить проблему, когда вам нужно проверить только очень небольшой набор изменений, чем когда вы не знаете, где искать в первую очередь.
Поэтому для помощи людям в поиске коммитов, которые вводят «плохое» поведение, была изобретена команда "git bisect". И, естественно, в терминологии "git bisect" коммиты, в которых присутствует «интересное поведение», называются «плохими» коммитами, а другие коммиты называются «хорошими» коммитами. А коммит, который ввёл поведение, которое нас интересует, называется «первым плохим коммитом». Обратите внимание, что в пространстве коммитов, которое мы ищем, может быть более одного «первого плохого коммита».
Таким образом, "git bisect" разработан для помощи в поиске «первого плохого коммита». И для максимальной эффективности он пытается выполнить бинарный поиск.
Обзор борьбы с регрессиями
Регрессии: большая проблема
Регрессии — большая проблема в программной индустрии. Но сложно привести конкретные цифры, подтверждающие это утверждение.
Есть некоторые цифры о багах в целом, например, исследование NIST 2002 года [1], которое гласило:
Программные ошибки, или ошибки, настолько распространены и вредны, что наносят ущерб экономике США, по оценкам, на 59,5 млрд долларов в год, или около 0,6 процента валового внутреннего продукта, согласно недавно опубликованному исследованию, проведенному по заказу Национального института стандартов и технологий (NIST) Министерства торговли США. На национальном уровне более половины затрат несут пользователи программного обеспечения, а остальная часть — разработчики/поставщики программного обеспечения. В исследовании также было обнаружено, что, хотя не все ошибки можно устранить, более трети этих затрат, или около 22,2 млрд долларов, можно было бы избежать благодаря улучшенной инфраструктуре тестирования, которая позволяет раньше и эффективнее выявлять и устранять программные дефекты. Это экономия, связанная с обнаружением большего процента (но не 100%) ошибок на более ранних этапах разработки, на которых они были введены. В настоящее время более половины всех ошибок обнаруживаются только «позже» на этапе разработки или во время использования программного обеспечения после продажи.
И затем:
Разработчики программного обеспечения уже тратят примерно 80 процентов затрат на разработку на поиск и исправление дефектов, и тем не менее, мало каких продуктов, кроме программного обеспечения, поставляются с таким высоким уровнем ошибок.
В конечном итоге вывод начинался с:
Путь к более высокому качеству программного обеспечения существенно улучшается тестированием программного обеспечения.
Существуют и другие оценки, утверждающие, что 80% затрат, связанных с программным обеспечением, составляют затраты на обслуживание [2].
Однако, согласно Википедии [3]:
Распространённое мнение о сопровождении заключается в том, что оно сводится лишь к исправлению ошибок. Однако исследования и опросы на протяжении многих лет показывают, что основная часть, более 80%, усилий по сопровождению используется для неисправительных действий (Pigosky 1997). Это мнение поддерживается пользователями, которые подают отчёты о проблемах, которые на самом деле являются расширением функциональности системы.
Но мы можем предположить, что усовершенствование существующего программного обеспечения очень дорого, потому что нужно следить за регрессиями. По крайней мере, это сделало бы вышеупомянутые исследования согласованными друг с другом.
Конечно, какое-то программное обеспечение разрабатывается, затем используется некоторое время без существенного улучшения, и, наконец, выбрасывается. В этом случае регрессии могут не быть большой проблемой. С другой стороны, существует множество крупного программного обеспечения, которое постоянно развивается и поддерживается в течение многих лет или даже десятилетий множеством людей. И поскольку часто многие люди зависят (иногда критически) от такого программного обеспечения, регрессии представляют собой действительно большую проблему.
Одним из таких программных обеспечений является ядро Linux. И если мы посмотрим на ядро Linux, мы увидим, что много времени и усилий тратится на борьбу с регрессиями. Цикл выпуска начинается с 2-недельного периода слияния. Затем помечается первая версия кандидата выпуска (rc). После этого появляется ещё около 7 или 8 версий rc с примерно одной неделей между каждой из них, прежде чем выйдет окончательный релиз.
Время между первым релизом rc и окончательным релизом должно использоваться для тестирования версий rc и борьбы с ошибками, особенно с регрессиями. И это время составляет более 80% времени цикла выпуска. Но это ещё не конец борьбы, поскольку, конечно, она продолжается и после выпуска.
И вот что говорит Инго Молнар (известный разработчик ядра Linux) о своём использовании "git bisect":
Я наиболее активно использую его во время периода слияния (когда большое количество деревьев сливается в upstream и когда приток ошибок самый высокий) — и да, были случаи, когда я использовал его несколько раз в день. Моя средняя частота использования примерно один раз в день.
Таким образом, разработчики постоянно борются с регрессиями, и, действительно, хорошо известно, что ошибки должны исправляться как можно скорее, как только они обнаружены. Вот почему важно иметь хорошие инструменты для этого.
Другие инструменты для борьбы с регрессиями
Итак, какие инструменты используются для борьбы с регрессиями? Они почти такие же, как и те, которые используются для борьбы с обычными ошибками. Единственными специфическими инструментами являются наборы тестов и инструменты, аналогичные "git bisect".
Наборы тестов очень хороши. Но когда они используются сами по себе, предполагается, что все тесты проверяются после каждого коммита. Это означает, что они не очень эффективны, потому что многие тесты выполняются без интересного результата, и они страдают от комбинаторного взрыва.
На самом деле проблема в том, что большое программное обеспечение часто имеет много различных параметров конфигурации, и каждый тестовый случай должен пройти для каждой конфигурации после каждого коммита. Итак, если у вас для каждого выпуска: N конфигураций, M коммитов и T тестовых случаев, вы должны выполнить:
N * M * T tests
где N, M и T — все увеличиваются с ростом размера вашего программного обеспечения.
Таким образом, очень скоро не будет возможности полностью протестировать всё.
И если какие-то ошибки проскользнут через ваш набор тестов, то вы можете добавить тест в свой набор тестов. Но если вы хотите использовать свой улучшенный набор тестов для поиска места, где ошибка просочилась, то вам придется либо эмулировать процесс бисекции, либо, возможно, грубо проверить каждый коммит назад, начиная с «плохого» коммита, который у вас есть, что может быть очень неэффективным.
Обзор "git bisect"
Начало бисекции
Первая подкоманда «git bisect», которую нужно использовать, это «git bisect start», для начала поиска. Затем нужно установить границы, чтобы ограничить пространство коммитов. Обычно это делается путём указания одного «плохого» и как минимум одного «хорошего» коммита. Их можно передать в начальном вызове «git bisect start» так:
$ git bisect start [BAD [GOOD...]]
или их можно установить, используя:
$ git bisect bad [COMMIT]
и:
$ git bisect good [COMMIT...]
где BAD, GOOD и COMMIT — все имена, которые могут быть разрешены до коммита.
Затем «git bisect» переключится на выбранный коммит и попросит пользователя протестировать его, примерно так:
$ git bisect start v2.6.27 v2.6.25 Bisecting: 10928 revisions left to test after this (roughly 14 steps) [2ec65f8b89ea003c27ff7723525a2ee335a2b393] x86: clean up using max_low_pfn on 32-bit
Обратите внимание, что пример, который мы будем использовать, является на самом деле учебным примером. Мы будем искать первый коммит, который имеет версию типа «2.6.26-что-то», то есть коммит, который содержит строку «SUBLEVEL = 26» в файле Makefile верхнего уровня. Это учебный пример, потому что есть лучшие способы найти этот коммит с помощью Git, чем использование «git bisect» (например, «git blame» или «git log -S<строка>»).
Ручное управление бисекцией
На данном этапе существует два основных способа управления поиском. Он может управляться пользователем вручную или автоматически скриптом или командой.
Если пользователь управляет им, то на каждом шаге поиска пользователь должен протестировать текущий коммит и указать, является ли он «хорошим» или «плохим», используя команды «git bisect good» или «git bisect bad» соответственно, которые были описаны выше. Например:
$ git bisect bad Bisecting: 5480 revisions left to test after this (roughly 13 steps) [66c0b394f08fd89236515c1c84485ea712a157be] KVM: kill file->f_count abuse in kvm
И после нескольких таких шагов «git bisect» в конечном итоге найдёт первый плохой коммит:
$ git bisect bad
2ddcca36c8bcfa251724fe342c8327451988be0d is the first bad commit
commit 2ddcca36c8bcfa251724fe342c8327451988be0d
Author: Linus Torvalds <torvalds@linux-foundation.org>
Date: Sat May 3 11:59:44 2008 -0700
Linux 2.6.26-rc1
:100644 100644 5cf82581... 4492984e... M Makefile На этом этапе мы можем посмотреть, что делает коммит, переключиться на него (если он ещё не переключён) или подправить его, например:
$ git show HEAD
commit 2ddcca36c8bcfa251724fe342c8327451988be0d
Author: Linus Torvalds <torvalds@linux-foundation.org>
Date: Sat May 3 11:59:44 2008 -0700
Linux 2.6.26-rc1
diff --git a/Makefile b/Makefile
index 5cf8258..4492984 100644
--- a/Makefile
+++ b/Makefile
@@ -1,7 +1,7 @@
VERSION = 2
PATCHLEVEL = 6
-SUBLEVEL = 25
-EXTRAVERSION =
+SUBLEVEL = 26
+EXTRAVERSION = -rc1
NAME = Funky Weasel is Jiggy wit it
# *DOCUMENTATION* И после завершения мы можем использовать «git bisect reset», чтобы вернуться к ветке, в которой мы находились до начала бисекции:
$ git bisect reset Checking out files: 100% (21549/21549), done. Previous HEAD position was 2ddcca3... Linux 2.6.26-rc1 Switched to branch 'master'
Автоматическое управление бисекцией
Другой способ управления процессом бисекции — указать «git bisect», запустить скрипт или команду на каждом шаге бисекции, чтобы узнать, является ли текущий коммит «хорошим» или «плохим». Для этого используется команда «git bisect run». Например:
$ git bisect start v2.6.27 v2.6.25
Bisecting: 10928 revisions left to test after this (roughly 14 steps)
[2ec65f8b89ea003c27ff7723525a2ee335a2b393] x86: clean up using max_low_pfn on 32-bit
$
$ git bisect run grep '^SUBLEVEL = 25' Makefile
running grep ^SUBLEVEL = 25 Makefile
Bisecting: 5480 revisions left to test after this (roughly 13 steps)
[66c0b394f08fd89236515c1c84485ea712a157be] KVM: kill file->f_count abuse in kvm
running grep ^SUBLEVEL = 25 Makefile
SUBLEVEL = 25
Bisecting: 2740 revisions left to test after this (roughly 12 steps)
[671294719628f1671faefd4882764886f8ad08cb] V4L/DVB(7879): Adding cx18 Support for mxl5005s
...
...
running grep ^SUBLEVEL = 25 Makefile
Bisecting: 0 revisions left to test after this (roughly 0 steps)
[2ddcca36c8bcfa251724fe342c8327451988be0d] Linux 2.6.26-rc1
running grep ^SUBLEVEL = 25 Makefile
2ddcca36c8bcfa251724fe342c8327451988be0d is the first bad commit
commit 2ddcca36c8bcfa251724fe342c8327451988be0d
Author: Linus Torvalds <torvalds@linux-foundation.org>
Date: Sat May 3 11:59:44 2008 -0700
Linux 2.6.26-rc1
:100644 100644 5cf82581... 4492984e... M Makefile
bisect run success В этом примере мы передали «grep ^SUBLEVEL = 25 Makefile» в качестве параметра для «git bisect run». Это означает, что на каждом шаге будет запущена команда grep, которую мы передали. И если она завершится с кодом 0 (что означает успех), то git bisect пометит текущее состояние как «хорошее». Если она завершится с кодом 1 (или любым кодом между 1 и 127 включительно, за исключением специального кода 125), то текущее состояние будет помечено как «плохое».
Код выхода между 128 и 255 является специальным для «git bisect run». Он заставляет немедленно остановить процесс бисекции. Это полезно, например, если передаваемая команда занимает слишком много времени для завершения, потому что вы можете её убить с помощью сигнала, и это остановит процесс бисекции.
Это также может быть полезно в скриптах, переданных в «git bisect run», чтобы «exit 255», если обнаружено какое-либо очень необычное состояние.
Избегание нетестируемых коммитов
Иногда бывает, что текущее состояние не может быть протестировано, например, если оно не компилируется, потому что в то время была ошибка, мешающая этому. Для этого предназначен специальный код выхода 125. Он сообщает «git bisect run», что текущий коммит следует отметить как нетестируемый, и что следует выбрать и проверить другой.
Если процесс бисекции управляется вручную, вы можете использовать «git bisect skip», чтобы сделать то же самое. (На самом деле специальный код выхода 125 заставляет «git bisect run» использовать «git bisect skip» в фоновом режиме.)
Или, если вы хотите больше контроля, вы можете проверить текущее состояние, например, используя «git bisect visualize». Он запустит gitk (или «git log», если переменная среды DISPLAY не установлена), чтобы помочь вам найти лучшую точку бисекции.
В любом случае, если у вас есть последовательность нетестируемых коммитов, возможно, что регрессия, которую вы ищете, была введена одним из этих нетестируемых коммитов. В этом случае нельзя точно сказать, какой коммит внёс регрессию.
Поэтому, если вы использовали «git bisect skip» (или скрипт выполнения завершился со специальным кодом 125), вы могли получить результат такого типа:
There are only 'skip'ped commits left to test. The first bad commit could be any of: 15722f2fa328eaba97022898a305ffc8172db6b1 78e86cf3e850bd755bb71831f42e200626fbd1e0 e15b73ad3db9b48d7d1ade32f8cd23a751fe0ace 070eab2303024706f2924822bfec8b9847e4ac1b We cannot bisect more!
Сохранение журнала и его воспроизведение
Если вы хотите показать другим людям процесс бисекции, вы можете получить журнал, например:
$ git bisect log > bisect_log.txt
И его можно воспроизвести, используя:
$ git bisect replay bisect_log.txt
"git bisect" details
Алгоритм бисекции
Так как коммиты Git образуют ориентированный ациклический граф (DAG), найти лучший коммит бисекции для проверки на каждом шаге не так просто. В любом случае Линус нашел и реализовал «настоящий глупый» алгоритм, позже улучшенный Джунио Хамано, который работает довольно хорошо.
Итак, алгоритм, используемый «git bisect» для поиска лучшего коммита бисекции, когда нет пропущенных коммитов, следующий:
1) оставляются только коммиты, которые:
a) являются предками «плохого» коммита (включая сам «плохой» коммит), b) не являются предками «хорошего» коммита (исключая «хорошие» коммиты).
Это означает, что мы избавляемся от неинтересных коммитов в DAG.
Например, если мы начнем с такого графа:
G-Y-G-W-W-W-X-X-X-X
\ /
W-W-B
/
Y---G-W---W
\ / \
Y-Y X-X-X-X
-> time goes this way -> где B — «плохой» коммит, «G» — «хорошие» коммиты, а W, X и Y — другие коммиты, мы получим следующий граф после этого первого шага:
W-W-W
\
W-W-B
/
W---W Таким образом, будут сохранены только коммиты W и B. Потому что коммиты X и Y будут удалены по правилам a) и b) соответственно, а коммиты G будут удалены по правилу b).
Примечание для пользователей Git, что это эквивалентно сохранению только коммита, заданного:
git rev-list BAD --not GOOD1 GOOD2...
Также обратите внимание, что нам не нужно, чтобы сохраняемые коммиты были потомками «хорошего» коммита. Итак, в следующем примере будут сохранены коммиты W и Z:
G-W-W-W-B / Z-Z
2) начиная с «хороших» концов графа, к каждому коммиту присваивается число его предков плюс один
Например, с помощью следующего графа, где H — «плохой» коммит, а A и D — некоторые родители некоторых «хороших» коммитов:
A-B-C
\
F-G-H
/
D---E это даст:
1 2 3
A-B-C
\6 7 8
F-G-H
1 2/
D---E 3) к каждому коммиту присваивается: min(X, N - X)
где X — значение, присвоенное коммиту на шаге 2), а N — общее число коммитов в графе.
В приведенном выше примере N = 8, поэтому это даст:
1 2 3
A-B-C
\2 1 0
F-G-H
1 2/
D---E 4) лучшей точкой бисекции является коммит с наибольшим присвоенным числом
Итак, в приведенном выше примере лучшей точкой бисекции является коммит C.
5) следует отметить, что для ускорения алгоритма реализованы некоторые сокращения
Поскольку мы знаем N с самого начала, мы знаем, что min(X, N - X) не может быть больше N/2. Таким образом, во время шагов 2) и 3), если мы присвоили бы N/2 коммиту, то мы знаем, что это лучшая точка бисекции. В этом случае мы можем просто прекратить обработку любого другого коммита и вернуть текущий коммит.
Отладка алгоритма бисекции
Для любого графа коммитов вы можете увидеть число, связанное с каждым коммитом, используя «git rev-list --bisect-all».
Например, для приведенного выше графа команда типа:
$ git rev-list --bisect-all BAD --not GOOD1 GOOD2
выведет что-то вроде:
e15b73ad3db9b48d7d1ade32f8cd23a751fe0ace (dist=3) 15722f2fa328eaba97022898a305ffc8172db6b1 (dist=2) 78e86cf3e850bd755bb71831f42e200626fbd1e0 (dist=2) a1939d9a142de972094af4dde9a544e577ddef0e (dist=2) 070eab2303024706f2924822bfec8b9847e4ac1b (dist=1) a3864d4f32a3bf5ed177ddef598490a08760b70d (dist=1) a41baa717dd74f1180abf55e9341bc7a0bb9d556 (dist=1) 9e622a6dad403b71c40979743bb9d5be17b16bd6 (dist=0)
Обсуждение алгоритма бисекции
Сначала определим «лучшую точку бисекции». Мы будем говорить, что коммит X является лучшей точкой бисекции или лучшим коммитом бисекции, если знание его состояния («хороший» или «плохой») дает как можно больше информации о состоянии коммита, который оказывается «хорошим» или «плохим».
Это означает, что лучшие коммиты бисекции — это коммиты, где следующая функция имеет максимальное значение:
f(X) = min(information_if_good(X), information_if_bad(X))
где information_if_good(X) — это информация, которую мы получаем, если X хороший, а information_if_bad(X) — информация, которую мы получаем, если X плохой.
Теперь предположим, что существует только один «первый плохой коммит». Это означает, что все его потомки — «плохие», а все остальные коммиты — «хорошие». И мы предположим, что все коммиты имеют равную вероятность быть хорошими или плохими, или быть первым плохим коммитом, так что знание состояния c коммитов всегда дает одинаковое количество информации, где бы эти c коммиты ни находились в графе и каково бы ни было c. (Таким образом, мы предполагаем, что эти коммиты, например, на ветке или рядом с хорошим или плохим коммитом, не дают больше или меньше информации).
Также предположим, что у нас есть очищенный граф, как один после шага 1) в алгоритме бисекции выше. Это означает, что мы можем измерить получаемую информацию в терминах числа коммитов, которые мы можем удалить из графа.
И давайте рассмотрим коммит X в графе.
Если X окажется «хорошим», то мы знаем, что его предки — все «хорошие», поэтому мы хотим сказать, что:
information_if_good(X) = number_of_ancestors(X) (TRUE)
И это верно, потому что на шаге 1) b) мы удаляем предков «хороших» коммитов.
Если X окажется «плохим», то мы знаем, что его потомки — все «плохие», поэтому мы хотим сказать, что:
information_if_bad(X) = number_of_descendants(X) (WRONG)
Но это неправильно, потому что на шаге 1) a) мы оставляем только предков плохого коммита. Поэтому мы получаем больше информации, когда коммит помечается как «плохой», потому что мы также знаем, что предки предыдущего «плохого» коммита, которые не являются предками нового «плохого» коммита, не являются первым плохим коммитом. Мы не знаем, являются ли они хорошими или плохими, но мы знаем, что они не являются первым плохим коммитом, потому что они не являются предками нового «плохого» коммита.
Итак, когда коммит помечается как «плохой», мы знаем, что можем удалить все коммиты в графе, кроме тех, которые являются предками нового «плохого» коммита. Это означает, что:
information_if_bad(X) = N - number_of_ancestors(X) (TRUE)
где N — количество коммитов в (очищенном) графе.
Таким образом, в итоге это означает, что для поиска лучших коммитов бисекции мы должны максимизировать функцию:
f(X) = min(number_of_ancestors(X), N - number_of_ancestors(X))
И это хорошо, потому что на шаге 2) мы вычисляем number_of_ancestors(X), и поэтому на шаге 3) мы вычисляем f(X).
Давайте рассмотрим следующий граф в качестве примера:
G-H-I-J
/ \
A-B-C-D-E-F O
\ /
K-L-M-N Если мы вычислим следующую неоптимальную функцию на нем:
g(X) = min(number_of_ancestors(X), number_of_descendants(X))
мы получим:
4 3 2 1
G-H-I-J
1 2 3 4 5 6/ \0
A-B-C-D-E-F O
\ /
K-L-M-N
4 3 2 1 но с алгоритмом, используемым git bisect, мы получим:
7 7 6 5
G-H-I-J
1 2 3 4 5 6/ \0
A-B-C-D-E-F O
\ /
K-L-M-N
7 7 6 5 Таким образом, мы выбрали G, H, K или L в качестве лучшей точки бисекции, что лучше, чем F. Потому что, например, если L — плохой, то мы будем знать не только то, что L, M и N плохие, но и то, что G, H, I и J не являются первым плохим коммитом (поскольку мы предполагаем, что существует только один первый плохой коммит, и он должен быть предком L).
Таким образом, текущий алгоритм, кажется, является лучшим возможным, учитывая то, что мы изначально предположили.
Алгоритм пропуска
Когда некоторые коммиты были пропущены (с помощью «git bisect skip»), то алгоритм бисекции такой же для шагов 1) по 3). Но затем мы используем примерно следующие шаги:
6) отсортировать коммиты по убыванию связанного значения
7) если первый коммит не пропущен, мы можем вернуть его и остановиться
8) иначе отфильтровать все пропущенные коммиты в отсортированном списке
9) использовать псевдослучайный генератор чисел (PRNG) для генерации случайного числа от 0 до 1
10) умножить это случайное число на его квадратный корень, чтобы сместить его к 0
11) умножить результат на количество коммитов в отфильтрованном списке, чтобы получить индекс в этом списке
12) вернуть коммит в вычисленном индексе
Обсуждение алгоритма пропуска
После шага 7) (в алгоритме пропуска) мы могли бы проверить, не пропущен ли второй коммит, и вернуть его, если это не так. И, по сути, именно этот алгоритм мы использовали от момента разработки «git bisect skip» в Git версии 1.5.4 (выпущенной 1 февраля 2008 г.) до Git версии 1.6.4 (выпущенной 29 июля 2009 г.).
Однако Инго Молнар и Х. Питер Анвин (ещё один известный разработчик ядра Linux) оба пожаловались, что иногда лучшие точки бисекции оказывались в области, где все коммиты не поддавались проверке. И в этом случае пользователю приходилось проверять много непроверяемых коммитов, что могло быть очень неэффективным.
Действительно, неподдающиеся проверке коммиты часто не поддавались проверке, потому что ошибка была введена в какой-то момент, и эта ошибка была исправлена только после того, как было внесено множество других коммитов.
Конечно, эта ошибка, как правило, никак не связана с ошибкой, которую мы пытаемся найти в графе коммитов. Но она мешает нам узнать, присутствует или нет интересующее нас «плохое поведение».
Поэтому факт, что коммиты рядом с непроверяемым коммитом имеют высокую вероятность быть непроверяемыми. И лучшие коммиты бисекции часто находятся вместе (из-за алгоритма бисекции).
Вот почему не следует просто выбирать следующий лучший непропущенный коммит бисекции, когда первый пропущен.
Мы обнаружили, что большинство коммитов в графе могут дать довольно много информации при проверке. А коммиты, которые, в среднем, не дадут много информации, — это те, которые находятся рядом с хорошими и плохими коммитами.
Поэтому использование PRNG со смещением в пользу коммитов, удаленных от хороших и плохих коммитов, показалось хорошим выбором.
Очевидное улучшение этого алгоритма заключалось бы в поиске коммита, значение которого близко к значению лучшего коммита бисекции, и который находится на другой ветке, прежде чем использовать PRNG. Потому что, если такой коммит существует, то он, скорее всего, тоже не будет непроверяемым, поэтому он, вероятно, даст больше информации, чем случайный.
Проверка баз слияния
В алгоритме бисекции есть еще одна тонкость, которая не была описана в вышеприведенном описании алгоритма бисекции.
В предыдущих примерах мы предполагали, что «хорошие» коммиты являются предками «плохого» коммита. Но это не является требованием «git bisect».
Конечно, «плохой» коммит не может быть предком «хорошего» коммита, потому что предки хороших коммитов должны быть «хорошими». И все «хорошие» коммиты должны быть связаны с плохим коммитом. Они не могут находиться на ветке, у которой нет связи с веткой «плохого» коммита. Но хороший коммит может быть связан с плохим коммитом и при этом не являться ни его предком, ни его потомком.
Например, может быть ветка «main» и ветка «dev», которая была ответвлена от ветки «main» в коммите с именем «D», как показано ниже:
A-B-C-D-E-F-G <--main
\
H-I-J <--dev Коммит «D» называется «базой слияния» для ветвей «main» и «dev», потому что это лучший общий предок этих веток для слияния.
Теперь предположим, что коммит J — плохой, а коммит G — хороший, и мы применяем алгоритм бисекции, как описано ранее.
Как описано в шаге 1) b) алгоритма бисекции, мы удаляем всех предков хороших коммитов, поскольку они тоже должны быть хорошими.
Таким образом, у нас останутся только:
H-I-J
Но что произойдет, если первый плохой коммит — «B», а он был исправлен на ветке «main» коммитом «F»?
Результат такой бисекции будет заключаться в том, что мы обнаружим, что H — это первый плохой коммит, тогда как на самом деле это B. Это будет неправильно!
И да, на практике может случиться, что люди, работающие над одной веткой, не знают, что люди, работающие над другой веткой, исправили ошибку! Также может случиться, что F исправил больше одной ошибки или что это откат большой работы разработки, которая не была готова к релизу.
Фактически, команды разработки часто поддерживают как ветку разработки, так и ветку обслуживания, и для них было бы довольно просто, если бы «git bisect» просто работал, когда они хотят бисектировать регрессию на ветке разработки, которая не находится на ветке обслуживания. Они должны иметь возможность начать бисектирование с использованием:
$ git bisect start dev main
Для включения этой дополнительной полезной функции, когда бисектирование начато, а некоторые хорошие коммиты не являются предками плохого коммита, мы сначала вычисляем базы слияния между плохими и хорошими коммитами и выбираем эти базы слияния в качестве первых коммитов, которые будут проверены и протестированы.
Если какая-либо база слияния является плохой, то процесс бисекции останавливается с сообщением, подобным:
The merge base BBBBBB is bad. This means the bug has been fixed between BBBBBB and [GGGGGG,...].
где BBBBBB — sha1-хеш плохого основания слияния, а [GGGGGG,…] — список, разделенный запятыми, sha1-хешей хороших коммитов.
Если некоторые из баз слияния пропущены, процесс бисекции продолжается, но для каждой пропущенной базы слияния выводится следующее сообщение:
Warning: the merge base between BBBBBB and [GGGGGG,...] must be skipped. So we cannot be sure the first bad commit is between MMMMMM and BBBBBB. We continue anyway.
где BBBBBB — sha1-хеш плохого коммита, MMMMMM — sha1-хеш пропущенной базы слияния, а [GGGGGG,…] — список, разделенный запятыми, sha1-хешей хороших коммитов.
Итак, если нет плохой базы слияния, процесс бисекции продолжается как обычно после этого шага.
Лучшие практики бисекции
Использование наборов тестов и git bisect вместе
Если у вас есть набор тестов и вы используете git bisect, то проверка того, что все тесты проходят после каждого коммита, становится менее важной. Хотя, конечно, вероятно, неплохо иметь некоторые проверки, чтобы избежать разрушения слишком большого количества вещей, потому что это может затруднить бисекцию других ошибок.
Вы можете сосредоточить свои усилия на проверке в нескольких точках (например, rc и beta релизах), что все тестовые случаи T проходят для всех N конфигураций. И когда некоторые тесты не проходят, вы можете использовать «git bisect» (или лучше «git bisect run»). Таким образом, вы должны выполнить примерно:
c * N * T + b * M * log2(M) tests
где c — количество раундов тестирования (поэтому небольшая константа), а b — отношение ошибок к коммитам (надеюсь, тоже небольшая константа).
Таким образом, это намного лучше, так как это O(N * T) по сравнению с O(N * T * M), если вы будете тестировать все после каждого коммита.
Это означает, что наборы тестов хорошо предотвращают коммит некоторых ошибок, и они также довольно хорошо указывают на то, что у вас есть некоторые ошибки. Но они не так хорошо показывают, где были введены некоторые ошибки. Для того, чтобы эффективно сообщить об этом, необходим git bisect.
Другое приятное свойство наборов тестов заключается в том, что когда у вас есть набор тестов, вы уже знаете, как тестировать неправильное поведение. Поэтому вы можете использовать эти знания для создания нового тестового случая для «git bisect», когда, по всей видимости, произошла регрессия. Таким образом, бисекция ошибки и ее исправление будет проще. А затем вы можете добавить созданный вами тестовый случай в свой набор тестов.
Итак, если вы знаете, как создавать тестовые случаи и как бисектировать, вы будете подвержены положительной обратной связи:
больше тестов ⇒ проще создавать тесты ⇒ проще бисектировать ⇒ больше тестов
Таким образом, наборы тестов и «git bisect» — это взаимодополняющие инструменты, которые очень мощные и эффективные при совместном использовании.
Бисекция сбоев сборки
Вы можете очень легко автоматически бисектировать сломанные сборки, используя что-то вроде:
$ git bisect start BAD GOOD $ git bisect run make
Передача sh -c "некоторые команды" в "git bisect run"
Например:
$ git bisect run sh -c "make || exit 125; ./my_app | grep 'good output'"
С другой стороны, если вы делаете это часто, тогда может стоить иметь скрипты, чтобы избежать излишнего набора текста.
Поиск регрессий производительности
Вот пример скрипта, немного изменённого из реального скрипта, используемого Junio Hamano [4].
Этот скрипт можно передать в «git bisect run», чтобы найти коммит, который ввёл регрессию производительности:
#!/bin/sh
# Build errors are not what I am interested in.
make my_app || exit 255
# We are checking if it stops in a reasonable amount of time, so
# let it run in the background...
./my_app >log 2>&1 &
# ... and grab its process ID.
pid=$!
# ... and then wait for sufficiently long.
sleep $NORMAL_TIME
# ... and then see if the process is still there.
if kill -0 $pid
then
# It is still running -- that is bad.
kill $pid; sleep 1; kill $pid;
exit 1
else
# It has already finished (the $pid process was no more),
# and we are happy.
exit 0
fi Следование общим рекомендациям по наилучшей практике
Очевидно, что неплохо не создавать коммиты с изменениями, которые сознательно ломают вещи, даже если некоторые другие коммиты позже исправят поломки.
Также неплохо, при использовании любого VCS, иметь только одно небольшое логическое изменение в каждом коммите.
Чем меньше изменения в вашем коммите, тем эффективнее будет «git bisect». И вам, вероятно, потребуется «git bisect» меньше в первую очередь, так как небольшие изменения легче проверить, даже если они проверяются только автором коммита.
Ещё одна хорошая идея — использовать хорошие сообщения коммитов. Они могут быть очень полезны для понимания того, почему были внесены некоторые изменения.
Эти общие рекомендации по наилучшей практике очень полезны, если вы часто бисектируете.
Избегание проблемных слияний
Первые слияния сами по себе могут вносить регрессии даже тогда, когда для разрешения конфликтов с исходным кодом слияние не требуется. Это происходит потому, что семантическое изменение может произойти в одной ветке, в то время как другая ветка об этом не знает.
Например, одна ветка может изменить семантику функции, а другая ветка добавит больше вызовов этой функции.
Это усугубляется, если для разрешения конфликтов нужно исправлять множество файлов. Именно поэтому такие слияния называются «злыми слияниями». Они могут очень затруднить отслеживание регрессий. Это может даже вводить в заблуждение, зная первый плохой коммит, если он случайно оказывается таким слиянием, потому что люди могут подумать, что ошибка происходит из-за плохого разрешения конфликтов, когда она происходит из-за семантического изменения в одной ветке.
В любом случае, «git rebase» можно использовать для линеаризации истории. Это можно использовать как для избегания слияния в первую очередь, так и для бисекции линейной истории вместо нелинейной, поскольку это должно дать больше информации в случае семантического изменения в одной ветке.
Слияния также можно упростить, используя более мелкие ветки или используя много темных веток вместо одной длинной ветки, связанной с версией.
А тестирование можно проводить чаще в специальных интеграционных ветках, таких как linux-next для ядра Linux.
Адаптация вашего рабочего процесса
Специальный рабочий процесс для обработки регрессий может дать отличные результаты.
Вот пример рабочего процесса, используемого Andreas Ericsson:
-
напишите в наборе тестов скрипт теста, который выявляет регрессию
-
используйте «git bisect run», чтобы найти коммит, который его ввёл
-
исправить ошибку, которая часто очевидна на предыдущем шаге
-
совершите коммит как исправления, так и скрипта теста (и, при необходимости, дополнительных тестов)
И вот что сказал Andreas об этом рабочем процессе [5]:
Чтобы привести конкретные цифры, у нас раньше был средний цикл от сообщения об ошибке до исправления 142,6 часа (согласно нашему несколько странному трекеру ошибок, который измеряет только время работы). С тех пор, как мы перешли на Git, мы снизили его до 16,2 часа. В основном потому, что мы теперь можем следить за исправлением ошибок, и потому, что каждый стремится исправить ошибки (мы довольно гордимся тем, как ленивы позволяем Git находить ошибки за нас). Каждый новый выпуск приводит к ~40% меньшему количеству ошибок (практически наверняка из-за того, как мы теперь относимся к написанию тестов).
Очевидно, что этот рабочий процесс использует положительную обратную связь между наборами тестов и «git bisect». Фактически, он делает его стандартной процедурой для работы с регрессиями.
В других сообщениях Andreas говорит, что они также используют описанные выше «рекомендации по наилучшей практике»: небольшие логические коммиты, темы веток, отсутствие «злых слияний»... Эти практики улучшают бисекцию графа коммитов, делая её более лёгкой и полезной.
Таким образом, хороший рабочий процесс должен быть разработан вокруг вышеперечисленных пунктов. То есть, упростить бисекцию, сделать её более полезной и стандартной.
Включение людей QA и, если возможно, конечных пользователей
Одно хорошее свойство «git bisect» заключается в том, что это не только инструмент разработчика. Его эффективно могут использовать специалисты QA или даже конечные пользователи (если у них есть доступ к исходному коду или если они могут получить доступ ко всем сборкам).
В какой-то момент на почтовой рассылке ядра Linux обсуждалось, нормально ли всегда просить конечного пользователя бисектировать, и были высказаны очень веские аргументы в поддержку точки зрения, что это нормально.
Например, Дэвид Миллер написал [6]:
Люди не понимают, что в этой ситуации применяется «принцип конечной точки». Когда у вас ограниченные ресурсы (здесь: разработчики), вы не перекладываете основную нагрузку на них. Вместо этого вы передаёте её ресурсу, которого у вас много, конечным узлам (здесь: пользователи), чтобы ситуация действительно масштабировалась.
Это означает, что часто «дешевле», если это могут сделать специалисты QA или конечные пользователи.
Интересно также, что конечные пользователи, которые сообщают об ошибках (или специалисты QA, которые воспроизвели ошибку), имеют доступ к среде, где происходит ошибка. Поэтому они часто легче воспроизводят регрессию. И если они могут бисектировать, то будет получена больше информации об окружающей среде, где происходит ошибка, что означает, что будет легче понять и исправить ошибку.
Для проектов с открытым исходным кодом это может быть хороший способ получить больше полезных вкладов от конечных пользователей и познакомить их с деятельностью QA и разработки.
Использование сложных скриптов
В некоторых случаях, например, при разработке ядра, может быть целесообразно разрабатывать сложные скрипты, чтобы полностью автоматизировать бисекцию.
Вот что говорит об этом Инго Молнар [7]:
У меня есть полностью автоматизированный скрипт бисекции зависаний при загрузке. Он основан на «git-bisect run». Я запускаю скрипт, он полностью автоматически собирает и загружает ядра, и когда загрузка завершается неудачно (скрипт замечает это через последовательный лог, за которым он непрерывно наблюдает — или через таймаут, если система не загружается в течение 10 минут, это «плохое» ядро), скрипт обращает моё внимание с помощью сигнала, и я перезагружаю тестовый компьютер. (да, я должен использовать управляемую розетку питания, чтобы на 100% автоматизировать это)
Сочетание наборов тестов, git bisect и других систем вместе
Мы видели, что наборы тестов и git bisect очень мощны при совместном использовании. Это может быть ещё мощнее, если вы сможете объединить их с другими системами.
Например, некоторые наборы тестов можно запускать автоматически ночью с некоторыми необычными (или даже случайными) конфигурациями. И если набор тестов обнаружит регрессию, то «git bisect» можно автоматически запустить, а его результат можно отправить по электронной почте автору первого плохого коммита, обнаруженного «git bisect», и, возможно, другим людям. Кроме того, можно автоматически создать новую запись в системе отслеживания ошибок.
Будущее бисекции
"git replace"
Ранее мы видели, что "git bisect skip" теперь использует генератор псевдослучайных чисел (PRNG), чтобы пытаться избегать участков в графе коммитов, где коммиты не подлежат тестированию. Проблема в том, что иногда первый плохой коммит будет находиться в недоступной для тестирования области.
Для упрощения обсуждения предположим, что недоступная для тестирования область представляет собой простую цепочку коммитов, и что она была создана ошибкой, внесенной одним коммитом (назовем его BBC, от bisect breaking commit), и позже исправлена другим (назовем его BFC, от bisect fixing commit).
Например:
...-Y-BBC-X1-X2-X3-X4-X5-X6-BFC-Z-...
где известно, что Y хороший, а BFC плохой, а BBC и X1 до X6 не подлежат тестированию.
В этом случае, если вы выполняете бисекцию вручную, то можете создать специальную ветвь, которая начинается непосредственно перед BBC. Первый коммит в этой ветви должен быть BBC с BFC, сжатым в него. Остальные коммиты в ветви должны быть коммитами между BBC и BFC, перебазированными на первый коммит ветви, а затем и коммит после BFC, также перебазированный.
Например:
(BBC+BFC)-X1'-X2'-X3'-X4'-X5'-X6'-Z'
/
...-Y-BBC-X1-X2-X3-X4-X5-X6-BFC-Z-... где коммиты, помеченные кавычками, были перебазированы.
Вы можете легко создать такую ветвь с помощью Git, используя интерактивную перебазирование.
Например, используя:
$ git rebase -i Y Z
а затем переместив BFC после BBC и сжав его.
После этого вы можете начать бисекцию как обычно в новой ветви, и в конечном итоге вы должны найти первый плохой коммит.
Например:
$ git bisect start Z' Y
Если вы используете "git bisect run", вы можете использовать то же самое исправление вручную, как указано выше, а затем запустить другой "git bisect run" в специальной ветви. Или, как говорится в справке "git bisect", скрипт, переданный в "git bisect run", может применить патч перед компиляцией и тестированием программного обеспечения [8]. Патч должен преобразовать текущие нетестируемые коммиты в тестируемые. Таким образом, тестирование приведет к результатам "хорошо" или "плохо", и "git bisect" сможет найти первый плохой коммит. И скрипт не должен забыть удалить патч после завершения тестирования перед выходом из скрипта.
(Обратите внимание, что вместо патча можно использовать "git cherry-pick BFC" для применения исправления, и в этом случае следует использовать "git reset --hard HEAD^", чтобы отменить cherry-pick после тестирования и перед возвращением из скрипта.)
Однако приведенные выше способы обхода недоступных для тестирования областей немного громоздки. Использование специальных ветвей удобно, потому что эти ветви могут быть совместно использованы разработчиками, как и обычные ветви, но есть риск, что у людей накопится много таких ветвей. И это нарушает стандартный рабочий процесс "git bisect". Поэтому, если вы хотите использовать "git bisect run" полностью автоматически, вам необходимо добавить специальный код в ваш скрипт для перезапуска бисекции в специальных ветвях.
В любом случае, можно заметить в примере специальной ветви, что коммиты Z' и Z должны указывать на одно и то же состояние исходного кода (одну и ту же "дерево" в терминологии Git). Это происходит потому, что Z' получается из применения тех же изменений, что и Z, но в немного другом порядке.
Таким образом, если мы могли бы просто "заменить" Z на Z' во время бисекции, то нам не пришлось бы добавлять что-либо в скрипт. Он просто будет работать для всех в проекте, которые будут совместно использовать специальные ветви и замены.
Вот почему была создана команда "git replace". Технически она хранит замены "refs" в иерархии "refs/replace/". Эти "refs" похожи на ветви (которые хранятся в "refs/heads/") или теги (которые хранятся в "refs/tags"), а это означает, что они могут автоматически быть совместно использованы, как ветви или теги, среди разработчиков.
"git replace" - очень мощный механизм. Он может использоваться для исправления коммитов в уже выпущенной истории, например, для изменения сообщения коммита или автора. Он также может использоваться вместо git "grafts" для связывания репозитория с другим старым репозиторием.
Фактически, именно это последнее свойство убедило сообщество Git, поэтому оно теперь находится в ветке "master" репозитория Git и должно быть выпущено в Git 1.6.5 в октябре или ноябре 2009 года.
Одна проблема с "git replace" заключается в том, что в настоящее время все refs замен хранятся в "refs/replace/", но, возможно, было бы лучше, если бы refs замен, полезных только для бисекции, находились в "refs/replace/bisect/". Таким образом, refs замен могли бы использоваться только для бисекции, а другие refs непосредственно в "refs/replace/" использовались бы практически постоянно.
Бисекция спорадических ошибок
Другим возможным улучшением "git bisect" было бы добавление некоторой избыточности к выполняемым тестам, чтобы повысить надёжность при отслеживании спорадических ошибок.
Это было запрошено некоторыми разработчиками ядра, потому что некоторые ошибки, называемые спорадическими, не появляются во всех сборках ядра, потому что сильно зависят от выходных данных компилятора.
Идея заключается в том, что каждые 3 теста, например, "git bisect" может попросить пользователя проверить коммит, который уже был признан "хорошим" или "плохим" (потому что один из его потомков или один из его предков был признан "хорошим" или "плохим" соответственно). Если окажется, что коммит был ранее неправильно классифицирован, то бисекция может быть прервана на ранней стадии, предположительно до того, как будут допущены слишком много ошибок. Тогда пользователю придётся разобраться в том, что произошло, и затем перезапустить бисекцию с использованием исправленного журнала бисекции.
Уже существует проект под названием BBChop, созданный Ealdwulf Wuffinga на GitHub, который делает нечто подобное, используя байесовскую теорию поиска [9]:
BBChop похож на
git bisect(или эквивалент), но работает, когда ваша ошибка является прерывистой. То есть, он работает при наличии ложных отрицательных результатов (когда версия работает в этот раз, даже если она содержит ошибку). Он предполагает, что нет ложных положительных результатов (в принципе, тот же подход будет работать, но добавление его может быть нетривиальным).
Но BBChop не зависит от какой-либо СКВ, и для пользователей Git было бы проще иметь что-то интегрированное в Git.
Заключение
Мы увидели, что регрессии представляют собой важную проблему, и что "git bisect" имеет хорошие возможности, которые очень хорошо дополняют практики и другие инструменты, особенно наборы тестов, которые обычно используются для борьбы с регрессиями. Но, возможно, потребуется изменить некоторые рабочие процессы и (плохие) привычки, чтобы извлечь из него максимальную пользу.
Возможны некоторые улучшения алгоритмов внутри "git bisect", и в некоторых случаях могут помочь новые функции, но в целом "git bisect" уже работает очень хорошо, широко используется и уже очень полезен. Чтобы подтвердить последнее утверждение, давайте дадим последнее слово Инго Молнару, когда его спросили, сколько времени, по его мнению, "git bisect" экономит ему, когда он его использует:
a
lot.Примерно десять лет назад я выполнил свой первый
bisectionочереди патчей ядра Linux. Это было до эпохи Git (и даже до эпохи BitKeeper). Я буквально тратил дни на сортировку патчей, создавая то, что по сути были автономными коммитами, которые, как я предполагал, были связаны с этой ошибкой.Это был инструмент абсолютного последнего средства. Я бы предпочел потратить дни на просмотр вывода printk, чем выполнять ручную
patch bisection.С Git bisect это просто: в лучшем случае я могу выполнить бисекцию ядра из ~15 шагов за 20-30 минут в автоматическом режиме. Даже с ручной помощью или при бисекции нескольких перекрывающихся ошибок, это редко занимает более часа.
На самом деле, это неоценимо, потому что есть ошибки, которые я никогда бы даже
tryне стал отлаживать, если бы не было git bisect. В прошлом были типы ошибок, которые сразу были безнадёжными для отладки - в лучшем случае я мог отправить сигнатуру аварии/ошибки на lkml и надеяться, что кто-то ещё сможет придумать что-нибудь.И даже если бисекция терпит неудачу сегодня, это говорит нам что-то ценное об ошибке: что она не детерминированная - зависимость от времени или структуры образа ядра.
Таким образом, git bisect - это безусловное благо - и чувствуйте себя свободными цитировать это ;-)
Благодарности
Большое спасибо Джунио Хамано за помощь в рецензировании этой статьи, за рецензирование патчей, которые я отправил на список рассылки Git, за обсуждение некоторых идей и помощь в их улучшении, за значительные улучшения "git bisect" и за его потрясающую работу по поддержанию и развитию Git.
Большое спасибо Инго Молнару за предоставление очень полезной информации, которая содержится в этой статье, за комментарии к этой статье, за его предложения по улучшению "git bisect" и за пропаганду "git bisect" в списках рассылки ядра Linux.
Большое спасибо Линусу Торвальдсу за изобретение, разработку и пропаганду "git bisect", Git и Linux.
Большое спасибо многим другим замечательным людям, которые помогали тем или иным способом, когда я работал над Git, особенно Андреасу Эриксону, Йоханнесу Шиндлину, Х. Питеру Анвину, Дэниелу Баркалоу, Билли Лиру, Джону Холи, Шону О. Пирсу, Джеффу Кингу, Саму Вилану, Джону Сеймуру.
Большое спасибо программному комитету Linux-Kongress за выбор автора для выступления и за публикацию этой статьи.
Ссылки
-
[[[1]]] Стоимость ошибок в программном обеспечении для экономики США составляет 59,5 миллиарда долларов в год. Пресс-релиз NIST. См. также Экономическое воздействие недостаточной инфраструктуры для тестирования программного обеспечения. Отчет NIST о планировании 02-3, резюме и глава 8.
-
[[[2]]] Правила кодирования для языка программирования Java: 1. Введение. Sun Microsystems.
-
[[[3]]] Техническое обслуживание программного обеспечения. Википедия.
-
[[[4]]] Junio C Hamano. История успешного автоматического бисекционирования.
-
[[[5]]] Christian Couder. Полностью автоматизированная бисекция с помощью "git bisect run". LWN.net.
-
[[[6]]] Jonathan Corbet. Бисекция разделяет пользователей и разработчиков. LWN.net.
-
[[[7]]] Ingo Molnar. Re: BUG 2.6.23-rc3 не видит разделов sd на Alpha. Список рассылки ядра Linux.
-
[[[8]]] Junio C Hamano and the git-list. git-bisect(1) Справочная страница. Архивы ядра Linux.
-
[[[9]]] Ealdwulf. bbchop. GitHub.
© 2005–2026 Linus Torvalds and others
Licensed under the GNU General Public License version 2.
https://git-scm.com/docs/git-bisect-lk2009