Разберем распространенную проблему архитектуры распределенных вычислений в 1С: когда тяжелый аналитический отчет формируется во внешней «базе для отчетов» через COM-соединение, а результат требуется отобразить в рабочей сессии пользователя. При попытке сериализовать результирующий ТабличныйДокумент в XML или JSON и десериализовать его в базе-приемнике платформа 1С аварийно завершает работу с системной ошибкой «Недостаточно памяти».
Проанализируем природу этой ошибки, выясним технические ограничения платформы и рассмотрим рабочие способы обхода ограничений при передаче больших массивов данных.
Для понимания корня проблемы выясним, как платформа 1С работает с памятью и структурированными документами:
ТабличныйДокумент — это не плоская таблица, а сложный объект со множеством свойств оформления. Каждая отдельная ячейка хранит стили шрифтов, границы, цвета заливки, форматирование, типы данных и параметры расшифровки. При чтении сериализованного файла размером 1–2 ГБ коэффициент расширения данных в оперативной памяти составляет от 4 до 10 раз. Соответственно, для развертывания такого документа приложению требуется единовременный непрерывный блок оперативной памяти объемом от 6 до 15 ГБ и более.COMConnector выполняется в 32-битном адресном пространстве, максимальный объем виртуальной памяти для процесса жестко ограничен 2 ГБ (независимо от разрядности Windows и физического объема ОЗУ). Из-за фрагментации памяти выделить крупный непрерывный сегмент невозможно уже при потреблении 1,2–1,5 ГБ, что мгновенно провоцирует сбой.Если тяжелый отчет строится на базе системы компоновки данных (СКД), наиболее эффективным решением является отказ от передачи готового табличного документа целиком. Вместо этого в базе-источнике выполняется компоновка, а через COM-канал передаются отдельные сериализованные элементы результата, которые база-приемник сразу выводит в свой ТабличныйДокумент.
Посмотрим на практическую реализацию этого механизма:
&НаСервереБезКонтекста
Процедура СформироватьОтчетЧерезСКД(РезультатТабДок)
СтрокаСоединения = "Srvr=""ReportServer"";Ref=""ReportBase"";";
Соединитель = Новый COMObject("V83.COMConnector");
Соединение = Соединитель.Connect(СтрокаСоединения);
COMОтчет = Соединение.Отчеты.АнализПродаж.Создать();
COMКомпоновщикМакета = Соединение.NewObject("КомпоновщикМакетаКомпоновкиДанных");
COMМакетКомпоновки = COMКомпоновщикМакета.Выполнить(COMОтчет.СхемаКомпоновкиДанных, COMОтчет.СхемаКомпоновкиДанных.НастройкиПоУмолчанию);
COMПроцессорКомпоновки = Соединение.NewObject("ПроцессорКомпоновкиДанных");
COMПроцессорКомпоновки.Инициализировать(COMМакетКомпоновки);
ПроцессорВывода = Новый ПроцессорВыводаРезультатаКомпоновкиДанныхВТабличныйДокумент;
ПроцессорВывода.УстановитьДокумент(РезультатТабДок);
ПроцессорВывода.НачатьВывод();
Пока Истина Цикл
COMЭлементРезультата = COMПроцессорКомпоновки.Следующий();
Если COMЭлементРезультата = Неопределено Тогда
Прервать;
КонецЕсли;
// Сериализуем единичный элемент в строку XML
COMЗаписьXML = Соединение.NewObject("ЗаписьXML");
COMЗаписьXML.УстановитьСтроку();
Соединение.СериализаторXDTO.ЗаписатьXML(COMЗаписьXML, COMЭлементРезультата);
СтрокаXML = COMЗаписьXML.Закрыть();
// Десериализуем в текущей базе и сразу выводим
ЧтениеXML = Новый ЧтениеXML;
ЧтениеXML.УстановитьСтроку(СтрокаXML);
ЭлементРезультата = СериализаторXDTO.ПрочитатьXML(ЧтениеXML);
ПроцессорВывода.ВывестиЭлемент(ЭлементРезультата);
КонецЦикла;
ПроцессорВывода.ЗакончитьВывод();
Соединение = Неопределено;
КонецПроцедуры
В данном подходе в памяти никогда не находится полный XML-снимок всего документа. Обработка идет поточно, что кардинально снижает нагрузку на оперативную память.
Если отчет построен на базе устаревшего ПостроительОтчета или алгоритма прямого заполнения макета, разбить процесс на элементы СКД невозможно. В этом случае применим порционную (построчную) передачу областей табличного документа.
Рассмотрим, как передавать сформированный документ строками:
// На стороне базы-приемника через COM-соединение
COMТабДок = Соединение.СформироватьОтчетПостроителем();
КоличествоСтрок = COMТабДок.ВысотаТаблицы;
Ширина = COMТабДок.ШиринаТаблицы;
ТабличныйДокументРезультат = Новый ТабличныйДокумент;
ЧтениеXML = Новый ЧтениеXML;
Для НомерСтроки = 1 По КоличествоСтрок Цикл
// Выделяем одну строку как отдельную область
ОбластьCOM = COMТабДок.ПолучитьОбласть(НомерСтроки, 1, НомерСтроки, Ширина);
ЗаписьXML = Соединение.NewObject("ЗаписьXML");
ЗаписьXML.УстановитьСтроку();
Соединение.СериализаторXDTO.ЗаписатьXML(ЗаписьXML, ОбластьCOM);
СтрокаXML = ЗаписьXML.Закрыть();
ЧтениеXML.УстановитьСтроку(СтрокаXML);
ОбластьСтроки = СериализаторXDTO.ПрочитатьXML(ЧтениеXML, Тип("ТабличныйДокумент"));
ТабличныйДокументРезультат.Вывести(ОбластьСтроки);
КонецЦикла;
Обратите внимание: при цикле Для НомерСтроки = 1 По КоличествоСтрок не следует делать ручное увеличение шага внутри тела итерации, иначе строки будут обрабатываться через одну.
Если задача требует сохранения единой структуры документа без ее дробления, проанализируем настройки окружения:
&НаСервереБезКонтекста позволяет использовать все ресурсы серверного пула памяти. Для передачи документа на клиент его нельзя возвращать как мутабельное значение напрямую в тонком клиенте — используйте временное хранилище (ПоместитьВоВременноеХранилище и ПолучитьИзВременногоХранилища).V83.COMConnector не делил память с основным процессом 1С, настройте в диспетчере компонентов Windows его запуск в виде серверного COM+-приложения. Тогда он запустится в отдельном системном процессе dllhost.exe.Когда пользователю требуется отчет размером в сотни мегабайт или гигабайты (например, полный разрез номенклатуры, складов, остатков и покупателей), открывать его на экране не имеет практического смысла. Ни один пользователь не в состоянии визуально изучить сотни тысяч строк в интерфейсе 1С.
В таких сценариях наилучшей практикой является прямая генерация текстового файла (CSV) с помощью объекта ЗаписьТекста прямо на стороне сервера отчетов:
Запись = Новый ЗаписьТекста("C:\Temp\ExportReport.csv", КодировкаТекста.UTF8);
Запись.ЗаписатьСтроку("Номенклатура;Склад;Покупатель;Остаток;Резерв");
Выборка = Запрос.Выполнить().Выбрать();
Пока Выборка.Следующий() Цикл
Стр = Строка(Выборка.Номенклатура) + ";"
+ Строка(Выборка.Склад) + ";"
+ Строка(Выборка.Покупатель) + ";"
+ Строка(Выборка.Остаток) + ";"
+ Строка(Выборка.Резерв);
Запись.ЗаписатьСтроку(Стр);
КонецЦикла;
Запись.Закрыть();
Объект ЗаписьТекста пишет данные на диск буферизованно малыми порциями. Расход памяти остается стабильно низким на протяжении всей выгрузки независимо от размера итогового файла, а пользователь получает готовый файл для загрузки в специализированные аналитические системы или Excel.