Проблема памяти
При работе со сложными отчетами, которые используют внутренний скрипт, я заметил значительный расход оперативной памяти. Для вебсервисов это может быть критичным.
Почему так происходит? После компиляции скрипта отчета в памяти остается небольшая библиотека (сборка), которая сохраняется движком фреймворка для последующих вызовов. В конце-концов «сборщик мусора» удалит их, но перед этим все же происходит расходование памяти.
Выход из сложившейся ситуации – использовать отдельный домен приложения для отчета со скриптом. Используя домен приложения, мы можем удалять сборки из памяти, точнее не сами сборки, а выгружать домены, в которых запускаются эти сборки. Таким образом, изолировав отчет и сопутствующую библиотеку со скриптом, мы легко очищаем ресурсы просто выгрузив ненужный домен.
Плата за это – необходимость в создании сборки, которая загружается во второй домен, некоторые неудобства вызова методов второго домена из первого.
Использование доменов
Любое .Net приложение уже имеет один домен по умолчанию. Решено создать еще один домен и поместить в него сборку с отчетом. В сборке, помимо объекта отчета, также будут методы работы с отчетом: запуск, экспорт.
Как мне показалось, проще всего взаимодействовать со вторым доменом посредством маршализации и прокси. Обычно маршализацию используют для взаимодействия между процессами, но это справедливо и для доменов. Маршалинг позволяет клиенту в одном домене вызывать методы объектов в другом домене. Мы будем обращаться к объекту в домене через прокси.
Взаимодействие между доменами происходит так же, как и между разными процессами. Чтобы получить доступ к коду другого домена, обязательно нужен прокси. Прокси – это заместитель объекта. Он перенаправляет вызовы из одного домена в другой.
Давайте рассмотрим схемы работы с отчетом в одном и в двух доменах:
Из рисунка видно, что в одном процессе запускается один домен. И в нем мы запускаем отчет со скриптом. При этом скрипт отчета компилируется в сборку и загружается в домен. Если мы вызываем много разных отчетов со скриптом, количество таких сборок значительно возрастает, что приводит к расходованию памяти.
Теперь рассмотрим вариант с двумя доменами:
В данном случае отчет запускается в отдельном домене, но все в том же процессе. При этом сборка скрипта отчета загружается во второй домен. В первом домене доступны все методы из второго благодаря использованию прокси. Когда мы завершаем работу с отчетом и закрываем его, домен выгружается вместе со всеми сборками. Таким образом освобождается память.
Реализация
Создаем приложение WindowsForms. Это приложение со стандартным классом потребуется нам для создания второго класса и вызова методов из загруженной в него сборки.
Добавляем на форму две кнопки: запуск отчета и экспорт отчета в PDF.
Добавляем метод создания нового домена:
1 2 3 4 5 6 |
public AppDomain NewDomain() { AppDomain domain = AppDomain.CreateDomain("NewDomain"); return domain; } |
И для комплекта метод выгрузки домена:
1 2 3 4 5 6 7 |
public void UnloadDomain(AppDomain domain) { AppDomain.Unload(domain); } |
Добавим в солюшен проект с библиотекой классов. Назовем его NewDomain. Итак, у нас есть проект сборки, которую мы загрузим в новый домен.
Обязательно наследуем класс от MarshalByRefObject. Для работы с FastReport .Net потребуется добавить в проект ссылку на библиотеку FastReport.dll.
Теперь можно создать экземпляр объекта Report:
1 |
public Report report1 = new Report();
|
В него мы загрузим отчет со скриптом. Для этого создаем метод загрузки отчета:
1 2 3 4 5 6 7 |
public void LoadReport(string path) { report1.Load(path); } |
И методы запуска и экспорта отчета:
1 2 3 4 5 6 7 |
public void ShowReport() { report1.Show(); } |
1 2 3 4 5 6 7 8 9 |
public void ExportToPDF() { FastReport.Export.Pdf.PDFExport pdf = new FastReport.Export.Pdf.PDFExport(); pdf.Export(report1); } |
Это все, что потребуется нам для работы с отчетом. Собираем сборку и кладем в папку с исполняемым файлом приложения. Либо добавляем ссылку на сборку в проект приложения.
Переходим к проекту с приложением.
Ранее мы написали метод добавления нового домена, теперь нужно загрузить в этот домен созданную выше сборку. Для работы со сборкой нам потребуется прокси:
1 2 3 4 5 6 |
public dynamic CreateProxy(AppDomain domain) { dynamic proxyOfChildDomainObject = domain.CreateInstanceFromAndUnwrap("NewDomain.dll", "NewDomain.NewDomainClass"); return proxyOfChildDomainObject; } |
Здесь мы создаем экземпляр прокси класса для нашей сборки. Метод CreateInstanceFromAndUnwrap создает новый экземпляр заданного типа, определенного в указанном файле сборки. В качестве параметров указываем имя файла сборки и полное имя класса.
Итак, мы можем создать новый домен и прокси для работы со сборкой. Теперь добавим код для первой кнопки:
1 2 3 4 5 6 7 8 9 |
private void button1_Click(object sender, EventArgs e) { AppDomain domain = NewDomain(); dynamic proxy1 = CreateProxy(domain); proxy1.LoadReport(Environment.CurrentDirectory + "/Matrix.frx"); proxy1.ShowReport(); UnloadDomain(domain); } |
Рассмотрим подробнее. В первой строчке создаем новый домен приложения с помощью метода NewDomain(). Далее создаем прокси для нашей сборки во втором домене. Теперь можно работать с методами сборки из второго домена. Загружаем отчет. И запускаем его в режиме предварительного просмотра. После просмотра отчета домен выгружается.
Похожий код используем для второй кнопки. Только вместо показа отчета, вызываем метод экспорта отчета в PDF. При этом будет показано окно настроек экспорта и диалоговое окно сохранения.
1 2 3 4 5 6 7 8 9 |
private void button2_Click(object sender, EventArgs e) { AppDomain domain = NewDomain(); dynamic proxy1 = CreateProxy(domain); proxy1.LoadReport(Environment.CurrentDirectory + "/Matrix.frx"); proxy1.ExportToPDF(); UnloadDomain(domain); } |
Вот и все. Приложение готово.
Подведем итоги
Использование нескольких доменов хоть и несколько замедляет работу с отчетами, но в итоге может существенно сэкономить память при работе с отчетами со встроенным скриптом. Скриптовые сборки будут удаляться из памяти вместе с выгрузкой домена, что может быть критично для высоконагруженных систем, типа вебсервисов.
На мой взгляд, наиболее эффективным способом работы с отчетами со скриптом будет запуск такого отчета в отдельном домене приложений с пересозданием этого домена через N построений отчета. Таким образом, можно регулировать нагрузку на память, выгружая накопившиеся сборки. Число N нужно подбирать экспериментальным путем для определения баланса затрат ресурсов на создание домена и очисткой памяти.