четверг, 13 октября 2011 г.

ASP.NET MVC MiniProfiler

Всем доброго времени суток. Сегодня я хочу рассказать про ASP.NET MVC MiniProfiler. А первое, что я хочу рассказать, это то, что, вопреки распространённому мнению и названию, этот профайлер вполне можно применять для отладки обычных (не MVC) ASP.NET приложений. Дальше я на примере покажу как это сделать. А пока обо всем по порядку.

Что такое MiniProfiler?

Это способ отладить ASP.NET-приложение на production (или, если вам больше нравится, “приложение, находящееся в промышленной эксплуатации”). И, что важно, не сильно влияя на производительность (то, что это работает на сайтах платформы StackExchange как бы намекает).

Вкратце процесс подключения выглядит так:

  1. Добавляется ссылка на dll.
  2. Подключается в “Master Page” или любое другое подходящее место одна строчка по инициализации UI.
  3. В Global.asax.cs прописываются вызовы по старту и остановке профайлера.
  4. Либо добавляются вызовы Step(string) там, где вам интересно, либо “сбоку” добавляются фильтры (вроде ProfilingActionFilter), чтобы не модифицировать код.

Опять же вкратце, на выходе получаем в виде небольшой разворачивающейся “закладки” на странице следующие возможности:

  • Суммарное время.
  • Время на дочерние действия (пункт 4) и запросы к БД.
  • Показать/скрыть “тривиальные” запросы (с маленькими затратами времени).
  • Переслать ссылку на отчет (данные хранятся в кэше).

Заинтересовались? Тогда продолжим…

Варианты установки и совместимость с Entity Framework

Есть два варианта установки – через NuGet или с Github. В первом варианте вы получаете более простую установку, во втором – потенциально более свежую версию. Правда, если вы хотите при этом отслеживать запросы в Entity Framework, да еще и с Sql Server Compact Edition, как пришлось сделать мне при подготовке доклада для встречи User Group, то жизнь становится намного интереснее.

Вероятно вы в курсе того, что есть, скажем так, некоторые проблемы с обратной совместимостью в Entity Framework 4.1 Update, касающиеся как раз возможности профилирования. А update этот, с другой стороны, полезен из-за проблем с производительностью в 4.1

Поэтому, если вы не используете Entity Framework 4.1 Update, для профилирования запросов Entity Framework достаточно в Package Manager Console выполнить:

PM> Install-Package MiniProfiler
PM> Install-Package MiniProfiler.EF

… и дальше эту главу можно не читать :)

В моем же случае пришлось установить BETA-версию EF 4.2, суммарно все действия по установке выглядели так:

PM> Install-Package SqlServerCompact  
PM> Install-Package EntityFramework.SqlServerCompact
PM> Install-Package EntityFramework.Preview
PM> Install-Package MiniProfiler
PM> Install-Package MiniProfiler.EF

На странице проекта описаны обходные пути решения проблемы и для этого стоит скачать последнюю версию с Github и попробовать воспользоваться этими советами. Лично мне в моей конфигурации не помогло.

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

Подключение к проекту

Рассмотрим сначала вариант подключения к обычному ASP.NET проекту (в конце статьи я дам ссылку на архив с примером). После подключения (любым из указанным выше способов) нужных dll подключаем скрипты для UI в Master Page:

<%= MiniProfiler.RenderIncludes().ToString() %>

Затем в Global.asax.cs прописываются вызовы по старту и остановке профайлера а также подключение к Entity Framework, если это необходимо:

using MvcMiniProfiler;
...
protected void Application_BeginRequest()
{
if (AllowProfiling)
MiniProfiler.Start();
}
 
protected void Application_EndRequest()
{
if (AllowProfiling)
MiniProfiler.Stop();
}
 
private bool AllowProfiling
{
get { return Request.IsLocal; }
}
 
private void Application_Start(object sender, EventArgs e)
{
MiniProfilerEF.Initialize(false);
}

Естественно, это самый простой способ – отображать информацию только в случае запросов к localhost. Для отладки на production “по-живому” потребуется немного модифицировать код с учетом того, что выводить результаты профилирования нужно только для определённых пользователей:

protected void Application_AuthenticateRequest(Object sender, EventArgs e)
{
if(!CurrentUserIsAllowedToSeeProfiler())
{
MvcMiniProfiler.MiniProfiler.Stop(discardResults: true);
}
}

Важно: сразу хочу сказать, что если вы воспользуетесь этим подходом, то вам на данный момент стоит однозначно установить версию с Github, из-за недавно исправленного бага.

После этого, как я уже говорил, можно добавить дополнительные фильтры для получения информации по вызовам методов контроллеров. Также может быть полезно увидеть время поиска представления (Find) и время, затраченное на его создание (Render).

Внешний вид

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

MiniProfiler1

По щелчку на ней можно увидеть краткую информацию по вызовам. Разумеется, если вы сейчас вплотную занимаетесь профилированием и добавили много вызовов вида MiniProfiler.Current.Step(“Проблема здесь быть может”); то список будет больше. Список также увеличится, если подключена информация по представлениям и используется много partial view.

MiniProfiler2

Кстати, вы сразу можете увидеть, что в первый раз представление ищется значительно медленнее (причина в обработке исключений при поиске в нескольких папках).

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

  1. Выводим и обращаем внимание на столбец “with children”.
  2. Вспоминаем, что время фиксируется не на каждую строчку кода, поэтому местами вполне естественно будут пробелы.
  3. Вспоминаем, что время могут “отбирать” параллельно запущенные процессы.

Обратите внимание – у нас есть пара запросов из Entity Framework, посмотрим?

MiniProfiler3

Еще одна приятная возможность – выделение повторяющихся запросов. Вообще-то не очень нужная вещь – мы ведь не допускаем таких глупых ошибок? :) А если серьезно, то бывает полезно для обнаружения методов, данные которых полезно кэшировать (пример – запрос привилегий из БД). Показывать отдельные картинки не буду – поверьте мне на слово, вы заметите эти запросы пользуясь MiniProfiler’ом.

И, как я уже говорил, всегда можно открыть/переслать коллеге ссылку на отчет, который выглядит так:

MiniProfiler4

Как я уже говорил, по умолчанию отчет хранится в кэше (один день). Это позволяет не мудрить с записью на диск или в БД, а если все это будет происходить на production и серверу не будет хватать ресурсов – то выкидывание из кэша этого отчета я считаю вполне оправданным. Далее я расскажу, как сохранять его в базу данных

Настройки

Настройки условно делятся на те, которые есть “из коробки” и те, которые иногда имеет смысл добавить самостоятельно.

Стандартные настройки можно использовать с помощью MiniProfiler.Settings, пример использования можно посмотреть на сайте проекта в Sample.Mvc/Global.asax.cs. Альтернативный вариант – параметры при вызове RenderIncludes.

Наиболее интересные настройки перечислю здесь:

  • Storage – позволяет задавать хранилище для отчетов
  • PopupRenderPosition – размещение закладок (слева/справа)
  • PopupMaxTracesToShow – максимальное количество закладок на экране
  • Exclude… – игнорирование сборок, классов и методов
  • Results_Authorize – дополнительная проверка, чтобы снизить вероятность попадания отладочной информации не в те руки…

Теперь перейдем к тому, что может потребоваться сделать самостоятельно.

Во-первых, вы можете написать свои глобальные фильтры и/или обертки для профилирования критичный участков кода. Пример можно посмотреть в исходниках.

Во-вторых, вероятно, может потребоваться для пользователей с включенным режимом профилирования делать отступ слева/справа у основной части приложения, чтобы закладки не мешали что-либо видеть. Я попробовал добавить через jQuery прозрачности – результат не понравился, после чего  просто сделал отступ. Уточню – в моём случае было достаточно много ajax-вызовов и, как следствие, много закладок (причём большинство из них интересны). Единичная же закладка мешает значительно меньше.

В-третьих, есть небольшие баги, которые в принципе легко обходятся (по крайней мере те, которые мне встречались). Например, у меня использовались всплывающие окна, пропадающие по клику на любое место страницы. А в MiniProfiler в тот же момент сидел баг в виде вызова метода jQuery .click. Наблюдалась весёлая картина, когда после щелчка по индикатору появлялись детали и тут же пропадали :) Если кому-то будет интересно, могу написать как сделать временный workaround на jQuery.

Профилирование первоначальной загрузки приложения

Как вы вероятно знаете, на Application_Start текущий HttpContext недоступен, а MiniProfiler его использует. Поэтому в реальном проекте для профилирования Bootstrapper’а (у нас так принято называть класс, отвечающий за инициализацию приложение, а в Application_Start он просто вызывается) пришлось немного допилить код.

Вообще-то решение получилось не совсем красивым, поэтому я добавил в web.config настройку по его включению/отключению. В Global.asax.cs я добавил следующий класс:

private static class BootstrapperOnFirstRequest
{
    private static volatile bool _initializedAlready;
    private static readonly Object _lock = new Object();
 
    public static void Initialize()
    {
 
        lock (_lock)
        {
            if (_initializedAlready)
                return;
 
            Bootstrapper.Initialize();
            _initializedAlready = true;
        }
    }
}

После этого осталось модифицировать Application_BeginRequest:

MiniProfiler.Start(ProfileLevel.Verbose);
 
if (AllowBootstrapperProfile)
    BootstrapperOnFirstRequest.Initialize();

После этого можно после рестарта приложения точно также наглядно видеть на что тратится время при инициализации. Я просто “засеял” этот метод вызовами метода Step.

Резюме

Если у вас есть достаточно серьезное ASP.NET приложение, а вы до сих пор не пользуетесь MiniProfiler или другим его аналогом – я бы на вашем месте срочно исправлялся.

Как показывает практика, после первого профилирования в проекте сразу появляется пара-тройка багов или запросов на улучшение. А основное, конечно, это поиск проблемных мест с точки зрения производительности. И, еще раз подчеркну – решение очень хорошо приспособлено для работы на production.

По большому счёту, я согласен со словами автора на странице проекта:

Simple. Fast. Pragmatic. Useful.

P.S. Если кому-то интересен исходный код примера, который использовался при написании этой статьи, я выложил его в Google Docs. Единственное “но” – удалил папку packages, чтобы не раздувать размер. Думаю, её несложно будет восстановить, создав новый проект и скопировав эту папку оттуда, а потом установив пакеты, перечисленные в главе “варианты установки”.

Если у вас есть замечания, пожелания или новые темы – пишите в комментариях или на olegaxenow.reformal.ru. Постараюсь учесть.

4 комментария:

  1. Спасибо за статью, пока не пробовал, но полезно.

    ОтветитьУдалить
  2. Спасибо за статью. Информативно!

    ОтветитьУдалить
  3. Давно уже использую на проде. Весь код уже покрыт шагами профайлера. Перфоманс с тех пор улучшили кардинально.

    Дописал чтобы еще показывал сколько памяти использует приложение. И чтобы работал в классическом режиме IIS под WebForms. Раньше не работало, не знаю как сейчас.

    ОтветитьУдалить
  4. To @Анонимные: всегда пожалуйста!

    To @Nikolay Sergeev:
    Правильный success story :)

    Классический режим c WebForms не использую, поэтому не интересовался.

    P.S. А остальным хочу сказать, что даже без "покрытия всего кода шагами" (это уже fine-tuning) профайлера он очень полезен.

    ОтветитьУдалить