?

Log in

No account? Create an account
 
 
01 Октябрь 2008 @ 09:35
Рецепты отладки. Четырехмесячный дебаг.  
Еще один из случаев, произошедших достаточно давно, не может использоваться как иллюстрация того, как можно быстро и эффективно поймать ошибку, но, тем не менее, является хороший примером с далеко идущими последствиями. Речь идет о четырехмесячном и неудачном дебаггинге.
 
8 лет назад мы разрабатывали (точнее, портировали на Dreamcast) игру Millenium Racer (в России известна как Вираж-3000). До поры до времени все шло гладко, пока в какой-то момент времени не было обнаружена феноменальная загадка: на пятом круге езды по трассе в том месте, где должна была находиться довольно прямая дорога, обнаружилась длинная стена (для тех, кто не видел игру, напомню, что игра заключалась в кольцевых гонках на футуристических гравимотоциклах по закольцованному уровню). Довольно быстро было обнаружено, что стена возникает через 10-15 минут игры на уровне, причиной ее возникновения является то, что 1 вполне себе валидная вершина у одного из трианглов вдруг неожиданно оказывается в начале координат. Как следствие - стена протягивается через весь уровень, и хотя она абсолютно не мешает коллижену, визуально играть становится невозможно.

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

Ситуация усугублялась тем, что это был проект для Dreamcast с довольно ограниченными функциями отладки под Metrowerks CodeWarrior, а на PC ситуацию воспроизвести не удавалось.

Это был один из редких случаев отладки, когда на решение проблемы потребовалось 4 месяца (естественно, не full time, но целенаправленно на поиск ошибки я тратил несколько полных рабочих дней в месяц). Ошибка не поддавалась. Мы сдвинули пространство так, чтобы нулевых вертексов не было по определению, и на каждом фрейме проверяли VB. Мы мониторили все возможные записи данных. Мы делали огромное количество других попыток, но все было бесполезно. Нам требовалось ровно то место записи в VB, которое было причиной; детектирование ситуации через *** миллисекунд после произошедшего не давало нам ключа к разгадке.

Ситуация разрешилась именно благодаря тому, что бага была в очередной раз отложена в надежде, что постоянно модифицирущийся код рано или поздно сломает мерзкую запись нуля в VB и мы таки получим новую пищу для размышлений. И действительно, как то раз на длительном заезде игра упала в access violation на нулевом четырехбайтовом присвоении. Почувствовав запах дичи, я взревел как раненый гризли, немедленно огородил висящий в дебаге компьютер ленточками с надписью "don't cross" и засел за тщательное выяснение того, почему упала программа.

Найденный ответ был неожиданным. В Millenium Racer использовалась озвучка персонажей, когда мы обгоняем их, или они обгоняют нас. При этом для разнообразия звуков было несколько, выбирался один из звуков по рандому, ему ставился флаг "использования", чтобы рандом на следующем применении не дал снова этот звук, а выбрал какой-то другой. Выяснилось, что раз в несколько тысяч запусков программа рандомизации дает сбой. Несмотря на то, что в нашей спецификации было указано, что random(count) выдает числа от 0 до count-1, иногда могло получиться и значение count. Как следствие - код звуковой логики адресовался к элементу звука Sound* array[count], который чудесным образом 4 месяца подряд попадал внутрь VB. random был наш собственный, VB в дримкасте лежал в основной памяти, в итоге проверка вида "если этот звук использовался на предыдущем фрейме, занулить флаг использования" воздвигала однополигональную стенку на нашем пути.
 
Несмотря на то, что бага не была отлажена традиционными путями, какие выводы были сделаны из нее?
1. Принцип "попробуй позже". Если багу отловить не удается, но есть запас времени, можно просто попробовать вернуться к проблеме потом, когда изменения в программе и данных позволят проявиться проблеме в более легкой форме. Тем не менее, см. второе замечание.
2. Если бага проявляется с трудом, попробуй отловить ее на измененных настройках приложения. Это могут быть другие параметры компиляции или оптимизации, временное включение ассертов в релизе и т.п. Хорошие результаты может дать также защита проекта какой-либо системой защиты (Starforce, Securom и т.п.).
3. Несмотря на то, что я не отношу себя к ярым поклонникам TDD, необходимость использования юнит-тестов очевидна. В данном конкретном случае тест для рандома пишется тривиально, но он помог бы сэкономить полмесяца моего дебаггинга. Как пример: сейчас в RedKey система автоматизированной сборки прогоняет тесты на подсистемы, и за последние два месяца дважды срубались неожиданные тесты после несущественных коммитов – это верификация интерсекшена и ошибки в тестах на мультизадачность. Несмотря на то, что повторный прогон тестов никаких проблем не выявил, это – серьезный сигнал, который требует существенного внимания. Потому что если проблема произошла 1 раз, она запросто произойдет и снова.
4. Необходимость ассертирования и отладочной диагностики. К сожалению, в 2000 году мы писали на Pure C, поэтому об ассертировании оператора [] можно было и не думать. Вообще, если пользоваться моей любимой аналогией с расследованием авиакатастроф, то для возникновения этого сложноуловимого бага было две основные причины, и изменения в каждой из них привели бы к тому, что баг не состоялся. Первая – это работоспособность random() не таким образом, как предполагалось, и вторая – это отсутствие диагностики у [].
Crossposted to blog.gamedeff.com
Метки: ,
 
 
Местонахождение: office