Я гоняю на нагрузочных тестах Rewind VM, детерминированную виртуальную машину: вычищаю шероховатости и баги и сам проверяю на себе, удобно ли ей пользоваться (тот самый dogfooding). Хотя я её и написал, меня до сих пор поражает, на какие фокусы она способна.
Но при всей этой магии, при том, как легко объявить баг «детерминированно воспроизведённым» и сузить его до одного-единственного шага, самым трудным остаётся прочитать код и понять, почему всё сломалось. Rewind может сказать, что пошло не так, но код за вас она не прочитает.
Именно здесь ИИ может по-настоящему помочь. Агенты уже умеют читать код и нередко находят в нём ошибки. Неудивительно, что если дать им много надёжных данных о том, что пошло не так, они указывают на баг гораздо быстрее, чем при чтении одного только кода. Детерминированные прогоны вместе с ИИ-агентами — очень мощное сочетание.
Находить баги с Rewind легко. rewind check раз за разом прогоняет тест, скрипт или бинарник при разных расписаниях потоков, пока один из прогонов не упадёт, а потом сужает падение до одного шага. Но на руках у вас остаётся номер шага и, возможно, большая кодовая база, с которой вы не знакомы. Трудная часть — прочитать код и понять, почему он упал.
Rewind не прочитает код за вас. Зато она даёт вам или агенту массу сведений о том, что пошло не так. Я натравил Rewind VM на целую кучу инструментов, и, как и следовало ожидать, она нашла баги… очень много багов. И самое приятное, что нашла она их так, что их можно воспроизвести и отладить, — поэтому агенты без труда предложили и исправления. Все найденные баги собраны на отдельной странице.
Кстати: если вы впервые слышите о Rewind, начните с постов a flaky build you only catch once и dafuq is a deterministic VM?
Ниже — небольшой пример бага, который Rewind нашла в git, и того, как я с её помощью добрался до причины и до строки кода, которая её вызвала. Баг живёт в git notes — команде, которая прикрепляет к коммиту заметку, не меняя сам коммит. Суть такая: если одновременно добавить две заметки к двум разным коммитам, одна из них может потеряться. Я отправил патч в апстрим.
Баг
git notes прикрепляет к коммиту заметку, не меняя сам коммит.
Начнём с идеи простого теста: создать две заметки на двух разных коммитах одновременно. (Тест показывает состояние гонки в git notes при параллельном добавлении заметок; его легко собрать как Nix-деривацию через runCommand.)
git notes add -m "note 1" HEAD~0 &git notes add -m "note 2" HEAD~1 &wait# обе команды завершились с кодом 0, значит, обе заметки должны быть на местеgit notes show HEAD~0 && git notes show HEAD~1
$ rewind check --all --schedules 32 --property exit-zero .#notes2schedule 1: exited:1 3167 steps run e5e5a1ecd0195f8f violates exit-zeroschedule 2: exited:1 3312 steps run ea6889e56564e374 violates exit-zero...15 of 32 other schedules violated a propertystep 2682 decides it: a request to switch threads there makes the run violate exit-zero
Под Rewind в пятнадцати расписаниях из тридцати двух одна заметка теряется, хотя каждый git notes add завершается успешно. На обычной Linux-машине заметка теряется в 25 запусках из 30, так что баг не специфичен для Rewind.
И что теперь?
Спрашивайте у прогонов
Прогон в Rewind — это полная трасса сборки: каждый процесс, каждый записанный им файл и содержимое любого файла на любом шаге. Её можно перемотать в любую точку. Это очень много данных. Rewind немного упрощает работу с ними: есть команды, которые помогают сузить круг подозреваемых. Три вопроса — три команды:
-
что в итоге оказалось по-разному? —
rewind diff; -
кто это записал и что он перед этим прочитал? —
rewind blame; -
какая строка это записала? —
rewind where.
Ни одна из этих трёх команд ничего не знает ни о git, ни о блокировках, ни о гонках. Они работают с любой программой.
Что оказалось по-разному?
rewind diff --outputs сравнивает файлы, которые оставили после себя два прогона. Это как diffoscope, только на стероидах: сравнивать можно любые два прогона, причём файлы на любом шаге, включая промежуточные. О том, как Rewind помогает сделать сборку побитово воспроизводимой, у меня есть пост подлиннее.
$ rewind diff --outputs 94c59fdf987c4944 ad0de5aec5a0be2d3 of the 30 files the runs left behind differ: /build/r/.git/logs/refs/notes/commits: 36 of 465 bytes differ, first at byte 351 /build/r/.git/refs/notes/commits: 36 of 41 bytes differ, first at byte 0 /nix/store/014fz1lj7hmk5h3rzcaw0w4576md3bg9-git-notes2: only in the right run
Зацепка вот в чём: .git/refs/notes/commits — это ссылка на последний коммит с заметками, и в двух прогонах она указывает на разные коммиты.
Кто это записал и что он прочитал?
rewind blame принимает два прогона и файл:
$ rewind blame 94c59fdf987c4944 ad0de5aec5a0be2d /build/r/.git/refs/notes/commitscommits differs at byte 0. git notes (pid 56) wrote that byte at step2,751 in run 94c59fdf and at step 2,717 in run ad0de5ae....Likely cause: git notes (pid 56) read /build/r/.git/refs/notes/commitsbefore git notes (pid 55) changed it at step 2,721 in run 94c59fdf,and after that change in run ad0de5ae. It read an older version ofthe file in run 94c59fdf.
В этом примере Rewind дала нам почти неопровержимую улику. Второй git notes add прочитал заметки до того, как первый сохранил свои, а потом записал свою версию поверх: классическая гонка «прочитать — изменить — записать».
Этот самый шаг можно посмотреть в графическом интерфейсе Rewind. На решающем шаге оба процесса git notes add живы (верхняя рамка), а первый сохранился наполовину: lock-файл он уже записал, но ещё не переименовал на место (нижняя рамка).
Какая строка это записала?
Если есть отладочные символы (Nixpkgs их предоставляет), rewind where покажет, в каком месте собственного кода git находился процесс на заданном шаге. Вот pid 56 в момент, когда он записывал ссылку:
$ rewind where 94c59fdf987c4944 2754 --pid 56 --frames 8process 56 (git), thread 56, at step 2754#1 rename_tempfile (tempfile.c:347)called from #2 commit_lock_file_to (/build/source/lockfile.h:364)called from #3 commit_lock_file (lockfile.c:358)called from #4 commit_ref (refs/files-backend.c:1889)called from #5 files_transaction_finish (refs/files-backend.c:3381)called from #6 ref_transaction_commit (refs.c:2806)called from #7 refs_update_ref (refs.c:1592)called from #8 commit_notes (notes-utils.c:64)
Дальше можно открыть код на конкретной строке и попытаться понять, почему всё сломалось. В нашем случае notes-utils.c:64 обновляет ссылку, не проверяя под блокировкой, что она всё ещё указывает туда же, куда указывала, когда команда загружала заметки.
Три вопроса, три команды — и прочитать мне пришлось одну строку git.
Кто всё это будет читать?
Падение git notes — это 2936 шагов и 1937 событий в одном прогоне, а rewind check сделал 33 прогона. Разобрать один прогон — уже большая работа, а все тридцать три — нереально.
Недавно знакомый рассказал мне, что ИИ здорово помогает разбирать трафик CAN-шины автомобиля. CAN-шина — это непрерывный поток сообщений от каждого датчика, кнопки, переключателя и лампочки машины. С LLM управляться с этим потоком стало проще.
То же самое я вижу и в Rewind. Прогон — такой же поток событий, и LLM тоже помогают его разбирать и находить баги. Просить LLM расследовать баг с помощью Rewind оказалось очень эффективно. Особенно помогает, что баг сужен до одного шага и конкретных строк исходников: контекст остаётся маленьким и сфокусированным. На этом этапе LLM может прочитать код, понять, что пошло не так, и, скорее всего, предложить исправление.
В LLM изначально заложено много случайности. Я заметил, что в паре с более детерминированными технологиями вроде Nix и Rewind они работают лучше. Сочетание детерминированных прогонов и LLM для поиска и исправления багов в больших кодовых базах вызывает у меня большой оптимизм.
Многие такие баги вносят сами LLM, но теперь они же умеют их и исправлять.
ссылка на оригинал статьи https://habr.com/ru/articles/1101358/