Улучшаем опыт отладки приложений

До работы над Visual Studio я не так часто сидел в отладчике, так как проекты не были такими большими, мой код не так сильно зависел от кода соседних команд, и все что мне нужно я быстро мог понять из тестов/кода, и только, когда уже ничего не помогало, я шел отлаживать приложение. Основная причина для этого была одна – так для меня было намного быстрее и легче работать, так я быстрее находил проблемы.

С Visual Studio такое не пройдет, кода очень много, влияение чужого кода огромное, не мало legacy кода, который еще писался до того, как разработчики начали усиленно думать о том, что код должен быть понятен не только компьютеру, но и разработчикам. В общем, отлаживать приложения приходиться теперь намного больше. А еще я познакомился вплотную с отладкой дампов (dump) памяти.

Храните исходники опубликованных версий и файлы символов

Предполагаю, что вы знаете, что такое Program Database Files (PDB). Если нет, то предлагаю вам обратиться к MSDN порталу Managing Symbols and Source Code. Если кратко, PDB файлы несут в себе информацию о том, как называется метод/функция выполняемая по опредленному адресу (в случае managed модуля – мы можем получить эту информацию из самого модуля, его методанных, а вот в native модулях такой информации нет), а так же: в каком исходном файле эта функция была описана, на какой строке выполняемая инструкция была описана и т.п. То есть, если вы хотите отладить приложение, то PDB файлы вам просто необходимы, чтобы хотя бы посмотреть вразумительный call stack для native модулей, а так же для всех модулей, чтобы использовать исходные файлы для отладки.

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

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

Что если исходные файлы отличаются?

Думаю, что хоть раз в жизни вы должны были видеть такое:

Это значит, что исходные файлы отличаются от тех, при помощи которых вы компилировали ваше приложение. Опять же PDB файлы содержать в себе checksum (обычные хеши) информацию об исходных файлах. Если вы знаете, что, скорее всего, изменения незначительные, например, вы просто поменяли одну строку в файле, то вы можете сказать отладчику о том, что вы готовы рискнуть, для этого нужно открыть контекстное меню этой точки останова (правой кнопкой на точке) – выбрать меню Location… и выставить "Allow the source code to be different from original version"

Такое же поведение можно выставить и в настройках отладчика для всех случаев на странице настроек Debugger->General, для этого нужно снять "Require source files to exactly match the original version".

Отображение объектов в отладчике

Как часто нам хочется видеть что-то более вразумительное в Watch списках, чем название классов?

Теперь, чтобы найти нужный Sample объект в этом массиве, нам будет необходимо раскрыть каждый и посмотреть на его свойства. Но, на самом деле, в .NET есть несколько удобных аттрибутов, которые помогают нам облегчать отладку самих объектов, они меняют вид, как объекты представлены в Watch списках. Для этого я рекоменую вам ознакомиться со статьей Enhancing Debugging with the Debugger Display Attributes. В моем случае это будет выглядеть так:

На сколько становится удобнее, не правда ли? В случае native кода достичь такого эффекта будет несколько сложнее, как это сделать можно узнать здесь, под заголовком Native Code, Displaying Custom Data.

Тяжелые свойста у объектов

Я стал теперь смотреть с опаской на свойства типа:

Случаи могут быть разные. В методе CreateFoo вы можете вызывать какой-то COM объект, который может подниматься секунд 20, и из-за чего у меня сначала подвиснет, а потом и отвалится отладчик. В общем, теперь, когда я пишу такие свойства, я точно должен быть уверен, что метод CreateFoo должен вызываться не очень долго.

Есть, правда, и другой минус в этом. Посмотрите на этот пример:

После того, как поймаете breakpoint, вы можете нажать, либо F5, либо F10 – и в этих случаях вы получите разные результаты, в одном NullReferenceException, в другом выполненный код.

Debug.Assert в managed коде

Очень часто я теперь стараюсь перед тем, как бросить какое-то исключение, сначала выкинуть Assert:

Делаю я это по одной простой причине. Запускать отладчик на каждый прогон какого-то сценария – это слишком ресурсоемко и долго. Но вот, если все же возникнит исключение, то я хотел бы иметь шанс для того, чтобы приаттачить отладчик до момента вызова исключения, чтобы разобраться, как такое получилось.

И как видите, я всегда стараюсь добавить какой-нибудь message в Debug.Assert, хотя бы продублировать само выражение, так как без него достаточно сложно определить, какой из нескольких assert сыграл в этом методе:

Заключение

Ну и в заключение я просто рекомендую вам обратиться к разделу Using Breakpoints and Tracepoints. Очень многие вещи я раньше не использовал. Особенно мне полюбилось выводить trace информацию при помощи How to: Specify a Tracepoint/Breakpoint Action.

Комментарии (5)

Алекс ( ) #
gravatar
О, вот за аттрибутик DebuggerDisplay огромное спасибо!
Мурад ( ) #
gravatar
Всегда было лень писать эти свойства для отладчика. Я вообще стараюсь не отлаживать код. Использовать тесты гораздо лучше.
Дмитрий ( ) #
gravatar
"После того, как поймаете breakpoint, вы можете нажать, либо F5, либо F10 – и в этих случаях вы получите разные результаты, в одном NullReferenceException, в другом выполненный код."

Не могли бы вы пояснить, в чём разница такого поведения?
Мурад ( ) #
gravatar
Дмитрию

В первом случае _foo непроинициализирована (null).

Во втором случае отладчик запросит свойство Foo, и тем самым _foo окажется проинициализированной.

По идее так.
Denis Gladkikh ( ) #
gravatar
Дмитрий, в коде мы нигде не запрашиваем свойство Foo, то есть поле _foo в нашем коде нигде не проинициализировано, но в случае когда мы подключены отладчиком - он запрашивает у объекта свойство Foo - тем самым инициализируя поле _foo. Конечно, интересен вопрос - в чем разница, когда нажимаешь F5 и F10 - ведь и в том, и в том случае отладчик уже должен был запросить свойство Foo и поле _foo должно быть проинициализировано. Тут я, предполагаю, что просто особенности отладчика - после нажатия F5 он пытается восстановить все состояния до того момента, как он подключился (это чисто мое предположение).
Добавить комментарий
Если вы хотите получать уведомления о новых комментариях к данному топику, укажите, пожалуйста, email и отметьте соответствующий пункт в форме. Если вы хотите добавить код в тексте комментария, то заключите его внутри тега [code]...[/code], более того можно уточнить язык, на котором написан данный код при помощи [code cs]...[/code], где вместо cs могут быть cs, html, xml, java, js, php, sql, cpp, css.

 

busy