В статье
рассматриваются вопросы пакетной компиляции ASPX страниц и технические детали
данного процесса.
Наверно
многие замечали, что первое обращение к ASPX странице занимает значительно
больше времени, чем последующие и при этом заметно повышенное потребление
ресурсов Веб-сервера. Это ASP.NET производит т.н. динамическую компиляцию или
компиляцию "на-лету" (on-the-fly). Она происходит автоматически, когда браузер
запрашивает страницу с сервера. Очевидно, что хотелось бы иметь средство
избежать подобных задержек. Далее я рассмотрю, что же предлагает ASP.NET для
борьбы с этим, а так же приведу некоторые детали того, как происходит обработка
запросов к страницам с точки зрения динамической компиляции.
Немного
теории
Как
известно, ASP.NET в отличие от ASP производит компиляцию (а не интерпретацию)
ASPX страниц. При такой компиляции для каждой страницы или группы страниц
создается сборка (assembly) - динамическая библиотека dll.
Замечу, что
речь идет именно о страницах ASPX и серверных скриптах внутри этих страниц, а не
о т.н. code behind файлах, которые должны быть заранее скомпилированы и
размещены в папке BIN или глобальном кеше сборок (GAC - Global Assembly Cache).
Использование code behind файлов является предпочтительным способом, так как
позволяет добиться разделения кода и представления. К сожалению это не исключает
компиляции ASPX страниц.
Сборка
содержит MSIL (MicroSoft Intermediate Language) код, который в свою очередь
перед фактическим исполнением компилируется в исполняемый код. Единица
компиляции - вызываемый метод. Это т.н. JIT-компиляция. Соственно говоря, этим
уже занимается не ASP.NET, а.NET Framework, так как с точки зрения Framework
скомпилированная ASPX страница - это обычная сборка, содержащая несколько
классов, поддерживающих определенные интерфейсы.
После
JIT-компиляции исполняемый код метода кешируется и последующие обращения к этому
методу происходят напрямую.
Таким
образом имеются две области, в которых происходят задержки при обращении к ASPX
странице:
- компиляция "на-лету"
- JIT-компиляция
Последнюю мы
рассматривать не будем, а вот в процесс компиляции "на-лету" можно вмешаться.
Первый
запрос страницы
ASP.NET
определяет, что требуется компиляция страницы, в случае, если для нее нет
сохраненной сборки или страница была изменена. Далее производятся две основные
операции:
- разбор (parsing) страницы и
генерация исходного кода. Данный код можно увидеть при включенной отладке (см.
ниже "Как устроена страница").
- компилиция кода страницы и
сохранение полученной сборки
Если
страница уже была ранее скомпилирована, т.е. для нее есть соответствующая
сборка, то данная сборка используется в качестве обработчика запрошенной
страницы.
Давайте
представим себе реальную ситуацию, когда Веб-сайт содержит несколько сотен или
тысяч страниц. Что происходит, когда пользователь запрашивает страницу с
сервера? Правильно - компиляция этой страницы. Теперь представьте, что сайт
нагружен. Очевидно, что в этом случае время отклика сервера возрастает, а это не
есть хорошо. Кстати - это касается и Веб-сервисов, но это немного другая
история, так как здесь вмешиваются механизмы Reflection и сериализации.
Что же можно
сделать, чтобы оптимизировать процесс динамической компиляции?
ASP.NET
предлагает средство, называемое "пакетная компиляция" (batch compilation).
Пакетная
компиляция
За процесс
компиляции отвечает элемент конфигурации <compilation> файла
web.config:
<configuration>
<system.web>
<compilation>
<compilation debug="true|false"
batch="true|false"
batchTimeout="число секунд"
defaultLanguage="язык"
maxBatchSize="максимальное число страниц на пакетную компиляцию"
maxBatchGeneratedFileSize="максимальный размер
сгенерированного исходного файла в KB"
tempDirectory="директория для временных файлов при компиляции">
</compilation>
Атрибут |
Значение |
Описание |
Debug |
|
Компиляция с
отладочной информацией. По умолчанию
false |
|
true |
Компиляция с
отладочной информацией. Сохраняются исходные файлы на языке, указанном в
атрибуте defaultLanguage. Замечание: при включенной отладочной
информации пакетная компиляция не
производится |
|
false |
Компиляция без
отладочной информации. |
defaultLanguage |
|
Язык, в который
разбирается страница. По умолчанию vb, но Visual Studio.NET устанавливает его в
c# |
batch |
|
Пакетная
компиляция |
|
true |
Пакетная компиляция
включена |
|
false |
Пакетная компиляция
выключена |
batchTimeout |
|
Указывает тайм-аут, в
секундах, для пакетной компиляции. Если компиляция не может быть выполнена за
этот период, то компилятор возвращается к стандартному режиму компиляции для
текущей страницы |
maxBatchGeneratedFileSize |
|
Максимальный размер
сгенерированного исходного файла в KB |
maxBatchFileSize |
|
Максимальное кол-во
страниц на одну пакетную компиляцию |
tempDirectory |
|
Директория для
временных файлов при компиляции |
Чтобы
включить эту опцию, надо установить атрибут batch равным
true , а
debug равным
false .
Что же
произойдет в этом случае ? При запросе любой страницы из каталога, ASP.NET
скомпилирует все страницы и создаст одну сборку. Т.е. вместо множества небольших
dll (одна dll для одной страницы) получится одна большая dll для всех страниц в
этом каталоге.
Здесь надо
заметить, что пакетная компиляция производится на основе каталога, а не для
всего Веб-приложения.
Очевидно
преимущество такого подхода - не надо компилировать и разбирать страницу за
страницей при первом к ним обращении. Но как говорится, бесплатный сыр бывает
только в мышеловках. И данный подход - не исключение. Отрицательная сторона
заключается в том, что такая компиляция занимает больше ресурсов сервера и может
увеличить время отклика страниц, участвующих в такой компиляции. Но на это есть
свое противоядие - "пред-пакетная" компиляция (pre-batch compilation). Т.е.
перед тем, как кто-либо запросит любую страницу с сервера, Вы сами обращаетесь к
любой странице, тем самым инициируя процесс пакетной компиляции и заставляя
ASP.NET создать и закешировать все страницы сайта. Отсюда мораль - всегда перед
передачей Веб-приложения в "производство" запрашивайте страницы из всех
виртуальных каталогов.
Технические
детали
Теперь
немного о технических деталях. Любая страница ASPX при обращении к ней
автоматически компилируется и попадает в:
%systemroot%/Microsoft.NET/Framework/<версия>/Temporary
ASP.NET Files/<сайт>/nnnnn/mmmm
(nnnnn и
mmmm - случайные последовательности).
Часть пути
"%systemroot%/Microsoft.NET/Framework/<версия>/Temporary ASP.NET Files"
может быть изменена с помощью атрибута tempDirectory
На моем
компьютере - это:
C:\WINDOWS\Microsoft.NET\Framework\v1.1.4322\Temporary
ASP.NET Files\TestProject\b5afd0e9\e951ffc7
В
зависимости от значения атрибута batch , там будет
находиться один или более файлов вида aabbcc.dll, а так же настройки для
связывания (Test.aspx.mmmmm.xml).
Хитрость
заключается в том, что в случае с batch=true , там будет всего
одна dll, но в настройках связывания (напоминаю - это файлы вида
Test.aspx.mmmmm.xml) будет указана одна и та же dll для всех страниц. Название
dll выбирается случайным образом, аналогично выбору имени временного файла.
Также эта
директория (а точнее поддиректория assembly) используется для т.н. "теневого"
копирования используемых в Веб-приложении dll, что позволяет ASP.NET не
блокировать их (вспомните ASP и COM).
Что
происходит, если меняется одна страница
А теперь -
самое интересное. Вы включили batch=true , произвели
предкомпиляцию и наслаждаетесь быстрой работой сайта. В этот момент оказывается,
что необходимо исправить одну страницу, скажем Test.aspx. Что происходит при
следующем запросе этой страницы?
ASP.NET
компилирует ее и создает отдельную сборку со случайным именем, например
xxyyzz.dll. Потом меняется настройка связывания, так что теперь ASP.NET знает:
"код выполнения страницы Test.aspx находится не в aabbcc.dll, а в xxyyzz.dll".
При этом в старой сборке остается код для страницы Test.aspx, но он не
используется.
Как
устроена страница или магия в действии
Если
заглянуть в xxyyzz.dll с помощью утилиты ildasm (входит в состав
.NET Framework SDK; также устанавливается с Visual Studio.NET), то можно
убедиться, что в ней находится класс Test_aspx, который наследован от того
класса, который вы использовали в качестве code behind (Test.aspx.cs, если
таковой имелся, что не является обязательным; это класс, про который директива
@Page в начале Вашей странице сообщает: Codebehind="Test.aspx.cs"
Inherits="TestProject.Test"), который в свою очередь наследован от
System.Web.UI.Page - базовый обработчик ASPX страниц (т.е. класс,
поддерживающий интерфейс IHttpHandler и участвующий в
конвеере запросов IIS/ASP.NET).
Для лучшего
понимания процесса надо установить debug=true .
Замечу, что
при этом перестанет работать пакетная компиляция, но это дает нам возможность
увидеть, как наша страница выглядит в C#.
Тем, кто не
собирается заниматься подобными изысканиями могу сообщить, что в основном там
следующее:
- создание зависимости на исходный
файл (Test.aspx), служащей для определения факта его изменения
- создание всех серверных элементов
управления и инициализация базового класса (напомню, базовый класс - это Ваш
code behind Test.aspx.cs). Собственно здесь и появляется та магическая связь
между идентификаторами серверных элементов управления и Вашим кодом (а точнее
полями Вашего класса)
- построение т.н. "дерева" элементов
управления
Если
запустить Task Manager, то можно заметить, как процесс csc.exe появляется и
исчезает. Это запускается компилятор C#, который собственно и производит
компиляцию исходного файла, полученного в результате разбора страницы.
После чего
данная сборка используется конвеером ASP.NET для обработки запроса на ресурс
Test.aspx.
Заключение
Несмотря на
то, что при программировании ASP.NET не требуется знание работы среды
исполнения, но в реальных проектах часто требуется тонкая настройка Веб-сервера,
особенно под нагрузкой. Надеюсь, что информация в данной статье поможет
разработчикам лучше понять работу ASP.NET и, возможно, решить некоторые
проблемы, в частности, с ошибками по правам доступа, кешированием компонентов и
т.п.
|