?

Log in

No account? Create an account
 
 
26 Февраль 2009 @ 11:17
Теория ошибок. Нестабильности первого рода.  
Любые ошибки в программах - это результат нестабильностей.
Как правило, для фатального невыполнения задачи недостаточно
одной нестабильности - они возникают последовательно, одна за
другой, в конечно счете и приводя к невозможности решения задачи.

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

Какие варианты “неправильной” передачи наиболее распространены? Во первых, передаваемые параметры могут являться базовыми элементами (переменные, константы, указатели на массивы и т.п.), во вторых, передаваемые аргументы могут быть сложными структурами или экземплярами классов. В разных случаях возможны разные варианты обработки.

Для базовых элементов наиболее распространёнными вариантами нестабильности первого рода являются: передача граничных или запрещенных входных данных. Простейших вариантов из CRT довольно много: например strcpy(NULL, …) или memcpy() на пересекающиеся области памяти. Практически никакие CRT функции не делают ни ассертирования, ни корректной обработки таких нестабильностей, поэтому падения тут особо часто распространены. Win API вызовы гораздо более защищенные, хотя во многих случаях уронить систему при передаче неправильного указателя довольно легко.

Более сложные примеры возможной нестабильности уже требуют проникновения в детали вызываемой функции. Допустим, мы реализовали алгоритм для расчёта минимальной ограничивающей сферы по множеству точек. Сходу можно придумать варианты нестабильностей первого рода:
  • множество точек, переданное на вход в функцию - пустое;
  • множество точек состоит из одной точки;
  • множество точек таково, что все точки находятся в одном месте пространства;
  • множество точек таково, что внутренние переменные и буфера (зависит от алгоритма и деталей его реализации) могут переполнится.

Как минимум, первые 2 варианта легко ассертируются на входе в функцию. Но, учитывая, что время работы данного алгоритма сильно больше, чем начальные проверки, я бы рекомендовал сделать проверки и адекватный отклики в остальных случаях. Например, в случае передачи множества точек, состоящего из одной точки, построить сферу радиуса epsilon с центром в указанной точке, и т.д.

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

То, что нестабильности надо ассертировать - как мне кажется, вообще вопросов не доложно вызывать. Но вот задача выбора простого ассертирования vs проверки и обработки всегда проблематична. Особенно если речь заходит про time-critical функции, которые вызываются много раз на фрейме. В таких случаях приходится анализировать не только саму вызываемую функцию, но еще и рассматривать потенциальные варианты, откуда могли придти невалидные данные.

Вообще если говорить про источник ошибочных данных, то можно выделить следующие основные классы:
  • Ошибочные данные, которые прошли из внешних источников (файл, TCP/IP пакет и т.п.). Сюда же относятся: прерывания, эвенты от внешнего железа и другие асинхронные события;
  • Ошибочные данные, которые были введены пользователем;
  • Ошибочные данные, которые были получены в результате ошибки алгоритма;
  • Ошибочные данные, которые были получены в результате отсутствия обработки вызываемых функций (типа вызвали CreateFile(), не проверили INVALID_HANDLE_VALUE).

При этом первый и второй вариант группируются в понятие “внешний источник”, третий и четвертый - в “ошибки программы” (такие ошибки программ приводят к появлению нестабильностей третьего рода).

Если аргумент на входе функции является косвенно или явно, аргументом внешнего источника, такое должно быть в обязательном порядке обработано на входе в функцию. Ассертировать такие аргументы даже нет смысла (хотя и можно для удобства отладки), поскольку это не программистская ошибка. А вот обеспечить логгирование - обязательно надо.

Лирическое отступление - программы, которые падают при чтении битого файла - не редкость. Но мне известно еще и программы, которые падают при отсутствии в системе звуковой карты или DVD привода!!!

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

Вообще из всех рассмотренных вариантов источников ошибочных данных единственный “приятный” вариант ошибки - это третий пункт (ошибки алгоритма). Поскольку только он потенциально (да и то не всегда) является воспроизводимым при последовательных запусках приложения. Все остальное ошибки как правило глубоко запрятаны в системе и когда именно они станут причиной аварийного завершения программы - никому неизвестно.

Две проблемы с передачей аргументов стоят особняком: это аргументы с обязательным требованием на время жизни и возможность версионирования данных. Первая проблема описана здесь, один из способов решения второй проблемы можно увидеть в WinAPI вариантах - это sizeof(struct) или первое поле с mask на заполненные/используемые поля.

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

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


cross-posted to blog.gamedeff.com
Метки: ,
 
 
Местонахождение: office
 
 
 
_dub_dubor on Февраль, 26, 2009 10:45 (UTC)
Если более коротко: "функция не должна падать при любых входных данных"
cd_ripercd_riper on Февраль, 26, 2009 11:15 (UTC)
Резюмируя -- будь параноиком, всегда допускай невозможное.
Дронтdrontik on Февраль, 26, 2009 13:09 (UTC)
Зачем проверять валидность входных данных в runtime, если можно это сделать в build time? Явно ведь игра не файлики из 3d max читает и не bmp из файлов, а вполне конкретные свои форматы, сгенеренные своими же тулзами. И если во входных данных ошибка - ее должны детектить тулзы, а не игра. Ну и не генерить файлы для игры, которые потом не прочитаются или не будут работать.

Ассерты для отладки в процессе разработки - всегда пожалуйста. В релизе - не должно быть ассертов. Игроку не интересно, в какой строчке какой функции произошла ошибка (а уж на консолях это тупо запрещено, выдавать такое). Игра должна либо gracefully проигнорировать ошибку и, например, свалить в main menu, либо так же плавно и молча закрыться.
Дядя Димаddima on Февраль, 26, 2009 13:16 (UTC)
Валидность входных данных из файлов данных - тут я согласен, что такое проверяется тулзами до того, как запустится игра (исключение - возможность модостроения).
А что с файлами сейвгеймов? А что с пользовательским вводом? А что с повреждением инсталляции. Кстати, обработка поврежденного сейва прописана во многих TCR.
Ассерты в релизе, ессно, должны быть или отключены совсем, или просто тихо мирно срать в лог (PC Only).
Дронт: calmdrontik on Февраль, 26, 2009 13:44 (UTC)
Для модостроения надо тоже тулзы для валидации предусматривать.

С поврежденными сейвами на консолях все довольно просто. Он либо поврежден, либо не поврежден. Его нельзя "слегка" испортить. То же, в общем-то, про остальные файлы - они либо corrupt, либо dirty disk, либо в них ровно то, что мы ожидаем увидеть.

Немного другая ситуация - с содержимым HDD (кеш ня ящике, Game Data на PS3) - там надо валидировать. Но, опять же, это не параметры в функции. Надо сначала валидировать сам файл целиком и решить, будем мы это грузить, или скажем игроку "жопа стряслась". А не так, что начали грузить, а на каком-нить там хедере текстуры (условно) - облажались.
Тогда "повреждение инсталляции" будет видно сразу.

Вот пользовательский ввод (включая мультиплеер) - это совсем другая тема. Раз ввод готовим не мы - то и обрабатывать должны корректно всегда. Это тоже в требованиях есть.
Но это не вопрос ассертов... мы же не должны падать, если игрок нажал что-то не то. Это просто корректная обработка входных данных. (Классический пример - поведение игры на X260, если вместо геймпада в нее вдруг воткнули агрегат от guitar hero).
Дядя Димаddima on Февраль, 26, 2009 13:56 (UTC)
ну я уже говорил - у меня есть более классические примеры от наших разработчиков. Например, падение программы, если нет установленного DVD привода.
У меня у самого было падение программы в очень экзотической ситуации, оказалось, что мой код универсального файлового менеджера падает, если содержимое переменной TEMP указывает на несуществующий диск ))))))).

Кстати, ассерты не ограничиваются только этим вопросом - есть еще второй и третий тип нестабильности, там тоже нужны ассерты.

Ну а почему приходится про это говорить? Потому что сплошь и рядом попадается:
FILE* f = fopen();
assert(f);
fread(f, ...);
Дронтdrontik on Февраль, 26, 2009 14:49 (UTC)
Ну, игры, которые падают без звуковой карты - это хорошо знакомый косяк... FMOD этим страдал одно время.

Пойнт про guitar hero был в том, что "некорректный инпут" бывает весьма разнообразным, и не всегда означает "у игрока какие-то проблемы".
Там ведь в чем суть. У гитары одна из кнопок в "обычном" состоянии всегда нажата, игрок ее ОТжимает. И если в игре есть, например, код, который скипает видеоролик, если зажата любая кнопка на геймпаде - то с гитарой вы ролика не увидите, а на ящике - увидите TCR fail.
Очевидно, зажатая кнопка - это не "невалидные входные данные", это скорее неожиданные для программиста входные данные, которые вполне валидны.
Дядя Димаddima on Февраль, 26, 2009 14:57 (UTC)
Ну это смотря с какой стороны посмотреть.
Можно ведь например перекинуть проблему на программиста подсистемы "input", который или забыл или неоттестировал логику под названием "если пришел гитархер, то рапортовать наружу Unknown device и all button zero".
Дронт: worrieddrontik on Февраль, 26, 2009 15:04 (UTC)
А так нельзя :) #031 почитайте...
Игра должна корректно работать с этой долбаной гитарой. В идеале - как минимум меню должны с нее корректно управляться. Игру должно быть можно запустить, выйти из нее. Если имеются в наличии одновременно гитара и нормальный контроллер - гитара не должна препятствовать игре с нормального. И т.п.

В том-то и суть, что это пример задачи для дизайнера UI, а не для программиста подсистемы "input". Намного более высокоуровневая, чем кажется на первый взгляд.
daradiboga on Февраль, 26, 2009 16:11 (UTC)
Какой показательный ответ :))
Дядя Димаddima on Февраль, 26, 2009 17:11 (UTC)
Не поверишь, именно так оно работало для PS2. Если сейчас в TRC что-то поменялось, конечно, надо доработать.
Там были впрочем более веселые ошибки, связанные с user input. На SmashCars SCEJ получили багу - если 10 раз за секунду (!) вытащить и вставить DualShock2 в гнездо, то игра перестает реагировать на джойстик.
Ремарка: чтобы игра восстановилась, надо вытащить джойстик, подождать немного, и вставить его обратно.
daradiboga on Февраль, 26, 2009 17:13 (UTC)
Я поверю легко во все что угодно.
Показательно же, что задача вообще не решается программистом, это дизайнерская задача.
Но если в руках молоток - то все вокруг похоже на гвозди. :)
Дядя Димаddima on Февраль, 26, 2009 19:25 (UTC)
Мы вообще об чем? О том, что каждая задача имеет множество аспектов? Дизайнерский в том числе, да. Но я как бы немного другое рассматриваю :)
Дронт: worrieddrontik on Февраль, 26, 2009 20:19 (UTC)
Мы о том, что не все задачи должны рассматривать программисты. И вопросы "как обрабатывать ошибки на входе" не всегда программистами вообще решаемы. Иногда - просто нет универсального ответа у программиста, дизайнер должен решать.
Дронт: worrieddrontik on Февраль, 26, 2009 20:17 (UTC)
В данном примере насчет гитары - речь про Xbox360.
Про методы тестирования SCEJ я тоже мог бы всякого порассказать =)