Производительность и частые ошибки

Производительность генерации документа зависит от получения данных, обработки шаблона, сохранения исходного файла и создания PDF или JPG. Оптимизируйте каждый этап отдельно. Повторная генерация не исправит медленный провайдер, а увеличение времени PHP-запроса не ускорит создание PDF и JPG.

Для работы с документом используйте класс Bitrix\DocumentGenerator\Document. Для массовой актуализации документов используйте класс Bitrix\DocumentGenerator\Service\ActualizeQueue, а Bitrix\DocumentGenerator\Integration\TransformerManager управляет асинхронным созданием производных файлов.

Перед выполнением PHP-примеров подключите модуль documentgenerator. Переменные $documentId и $userId должны содержать идентификаторы документа и пользователя из вашего сценария.

Измерить генерацию до оптимизации

Сначала определите медленный этап. Для одного и того же шаблона и исходного объекта измерьте:

  • время получения данных и полного вызова getFile(), update() или actualize(),

  • пиковое потребление памяти PHP,

  • количество и суммарное время запросов источника данных,

  • размер шаблона, исходного файла, PDF и JPG,

  • время от постановки задачи преобразования до появления производных файлов,

  • число переданных на актуализацию документов и задержку до изменения каждого документа.

Сравнивайте результаты на одинаковых данных. Отдельно проверяйте первый и повторный запуски. Кеш приложения и уже созданные файлы могут изменить время выполнения.

Пример. Код измеряет время и пиковую память при актуализации одного документа без повторного преобразования:

use Bitrix\DocumentGenerator\Document;

$document = Document::loadById($documentId);

if ($document === null)
{
    throw new \RuntimeException('Документ не найден');
}

if (!$document->hasAccess($userId))
{
    throw new \RuntimeException('Нет доступа к исходным данным');
}

$startedAt = microtime(true);
$memoryBefore = memory_get_usage(true);

$result = $document->actualize($userId, false);

$duration = microtime(true) - $startedAt;
$peakMemory = memory_get_peak_usage(true) - $memoryBefore;

if (!$result->isSuccess())
{
    throw new \RuntimeException(implode('; ', $result->getErrorMessages()));
}

// Передайте идентификатор документа, $duration и $peakMemory в журнал приложения

Измерение памяти показывает изменение внутри текущего PHP-процесса, а не точную стоимость одного документа. Используйте его для сравнения одинаковых запусков и дополняйте данными профилировщика и SQL-трекера.

Оптимизировать получение данных

Класс DataProviderManager находит значения по кодам полей из шаблона и получает данные связанных объектов через вложенные провайдеры. Менеджер повторно использует экземпляр провайдера с теми же классом, исходным значением и параметрами, а базовый класс DataProvider сохраняет уже рассчитанные значения полей. Составные поля, разные исходные объекты и множественные связи все равно могут увеличить число обращений к источнику данных.

Не загружать объект для каждого поля

Собственный провайдер не должен выполнять одинаковый запрос для каждого поля. Загрузите исходный объект один раз, сохраните его в свойстве $data провайдера и возвращайте значения полей из уже полученных данных. Такой объект считается загруженным, когда $data не равно null.

Если поле ведет к связанному объекту, передавайте вложенному провайдеру идентификатор или подготовленные данные по его контракту. Не создавайте новый запрос только для форматирования. Строки, даты, имена и другие значения форматируют классы Value.

Проверьте оптимизацию по числу запросов на один документ. Количество запросов не должно расти пропорционально числу простых полей одного объекта.

Ограничить дерево полей шаблоном

Каждый составной плейсхолдер задает путь по дереву провайдеров. Удалите из рабочего шаблона неиспользуемые поля и не добавляйте скрытые блоки «на будущее». Генератор все равно должен разобрать структуру шаблона и подготовить значения найденных полей.

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

Ограничить множественные значения

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

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

Измерьте время обработки и размер файла на характерном, среднем и предельном числе элементов. Если предельный документ не помещается в доступную память или время запроса, разделите результат на несколько документов либо перенесите генерацию в управляемый фоновый процесс.

Оптимизировать шаблон и исходный файл

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

Уменьшить изображения в шаблоне

Подготовьте изображения в разрешении, которое нужно в готовом документе. Уменьшение размеров изображения только средствами редактора DOCX не сокращает исходный файл, если внутри архива остается полноразмерное содержимое.

После изменения сравните размер шаблона и документа, пиковую память и время обработки. Проверяйте также качество PDF и JPG, если приложение создает производные форматы.

Разделить большие повторяемые блоки

Не помещайте в один документ неограниченный список строк, изображений или вложенных объектов. Большой повторяемый блок увеличивает объем XML внутри DOCX и стоимость последующего преобразования.

Если предметная задача допускает разбиение, сформируйте несколько документов по стабильному признаку и сохраните связь каждого результата с исходным объектом. Ограничьте число параллельных процессов. Каждый процесс потребляет память, обращается к источникам данных и отправляет задания преобразователю.

Не пересоздавать документ без изменения данных

Сохраняйте идентификатор сформированного документа в данных приложения. При повторном запросе загрузите его через Document::loadById() и получите существующий файл методом getFile(false).

Метод createByTemplate() не проверяет, создавался ли документ для той же пары шаблона и исходного объекта. Повторный вызов может создать дубль. Для изменения сохраненного документа используйте update() или actualize(), а конкурентные запуски координируйте средствами приложения.

Сократить стоимость преобразования

Модуль transformer создает PDF и JPG асинхронно в очереди documentgenerator_create. Исходный документ может быть готов раньше производных файлов.

Создавать только нужный результат

Если приложению достаточно исходного DOCX или TXT, передайте false первым аргументом getFile() подготовленного объекта $document:

$result = $document->getFile(false);

if (!$result->isSuccess())
{
    throw new \RuntimeException(implode('; ', $result->getErrorMessages()));
}

Тот же принцип действует при изменении документа. Передайте false в $sendToTransformation метода update() или actualize(), если PDF и JPG не нужны для новой версии.

Не вызывайте update() или actualize() только ради отсутствующего PDF или JPG. Загрузите сохраненный документ и вызовите getFile(true). Метод отправит на преобразование только недостающие форматы.

Не ставить одинаковое задание повторно

Первый результат getFile(true) может не содержать pdfUrl и imageUrl, пока асинхронная задача не завершилась. Отсутствие ссылок без признака ошибки не означает, что нужно немедленно повторить вызов.

Продолжайте серверный сценарий по событию documentgenerator:onDocumentTransformationComplete. Для клиентского сценария используйте уведомление Push and Pull или периодически загружайте документ и вызывайте getFile(false). Повторная проверка с аргументом false не отправляет новое задание.

Если несколько процессов могут запросить один формат одновременно, храните в приложении признак ожидаемого преобразования по идентификатору документа. Снимайте его после завершения или ошибки задачи.

Измерить асинхронный этап

Время PHP-вызова getFile(true) показывает только обработку исходного файла и постановку задачи. Для оценки преобразования сохраните время постановки и вычислите задержку при получении события завершения.

Отдельно наблюдайте:

  • медианное и предельное время преобразования,

  • долю задач с ошибкой,

  • возраст незавершенных задач,

  • зависимость времени от размера исходного файла.

Рост только времени преобразования указывает на очередь или сервис transformer. Рост времени до постановки задачи требует проверки провайдеров, шаблона и хранилища.

Выбрать способ актуализации

Метод Document::actualize() повторно получает данные провайдера, обрабатывает тело и заменяет исходный файл. Прямой вызов работает в текущем PHP-запросе.

Актуализировать один документ немедленно

Используйте прямой вызов, когда результат нужен в текущем сценарии и время генерации укладывается в его ограничения. Порядок загрузки документа, проверки доступа и вызова actualize() описан в разделе «Актуализировать данные провайдера» статьи Документы.

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

Передать массовую актуализацию в очередь

Для массовой актуализации документов используйте сервис Bitrix\DocumentGenerator\Service\ActualizeQueue. Получите сервис из ServiceLocator, создайте объект Bitrix\DocumentGenerator\Service\ActualizeQueue\Task и передайте его в addTask().

До постановки проверьте право пользователя на исходный объект. Очередь передает идентификатор пользователя в actualize(), но не вызывает Document::hasAccess().

Режим задачи определяет момент запуска:

Режим

Константа

Поведение

Очередь

Task::ACTUALIZATION_POSITION_QUEUE

Сохраняет задание. Агент ActualizeQueue::process() выбирает первые задания по времени добавления

Фоновая задача

Task::ACTUALIZATION_POSITION_BACKGROUND

Сохраняет задание и добавляет его обработку в фон текущего запроса

Немедленная обработка ожидающего задания

Task::ACTUALIZATION_POSITION_IMMEDIATELY

Находит ранее сохраненное задание этого документа и обрабатывает его в текущем запросе. Если задания нет, актуализацию не запускает

По умолчанию задача использует режим очереди. При штатной установке модуля агент запускается раз в 300 секунд и обрабатывает до пяти заданий за один запуск. Администратор может изменить расписание агента и размер пачки.

Пример. Код сохраняет одно задание для обработки агентом:

use Bitrix\DocumentGenerator\Document;
use Bitrix\DocumentGenerator\Service\ActualizeQueue\Task;
use Bitrix\Main\DI\ServiceLocator;

$document = Document::loadById($documentId);

if ($document === null)
{
    throw new \RuntimeException('Документ не найден');
}

if (!$document->hasAccess($userId))
{
    throw new \RuntimeException('Нет доступа к исходным данным');
}

$queue = ServiceLocator::getInstance()->get(
    'documentgenerator.service.actualizeQueue'
);

$task =
    (new Task($documentId))
        ->setUserId($userId)
        ->setPosition(Task::ACTUALIZATION_POSITION_QUEUE)
;
$queue->addTask($task);

Очередь хранит не более одного отложенного задания на идентификатор документа. Повторный вызов addTask() в режиме очереди не добавляет вторую запись и не обновляет пользователя или время первого задания. Если нужно сохранить последнее состояние источника, дождитесь обработки существующего задания или организуйте объединение изменений в своем модуле.

Перед постановкой сохраняйте идентификатор документа и причину актуализации в журнале приложения. После обработки проверьте документ. Идентификатор должен сохраниться, updateTime — измениться, а исходный файл — содержать актуальные данные.

Восстановить обработку очереди

Штатная очередь не хранит статус выполнения и ошибки actualize(). Она удаляет задание до загрузки и актуализации документа. Если документ не найден, уже обновлен после добавления задания или актуализация вернула ошибку, автоматический повтор не выполняется.

Если ожидаемый документ не обновился:

  1. Проверьте, запускаются ли агенты продукта. При штатных параметрах первая пачка содержит до пяти заданий.

  2. Сопоставьте журнал постановки с updateTime документа. Очередь не предоставляет отдельный журнал завершенных и ошибочных задач.

  3. Устраните причину ошибки до повторного запуска. Немедленный повтор с теми же данными создаст ту же ошибку.

  4. Повторно добавьте задание после исправления. Не добавляйте его до устранения причины.

  5. Сравните число переданных документов с числом документов, у которых изменились время и содержимое.

Не удаляйте записи очереди и файлы документа напрямую. Ручная очистка внутренних данных может скрыть незавершенное задание и оставить запись документа со ссылкой на отсутствующий файл.

Сохранять согласованность файлов

Запись документа, исходный файл, PDF, JPG и публичная ссылка связаны, но система создает и удаляет их отдельными операциями.

Для замены исходного файла используйте update() или actualize(). Для удаления документа используйте операцию продукта или модуля, который его создал. Такая операция запускает жизненный цикл удаления и очищает связанные файлы.

Не удаляйте FILE_ID, PDF_ID или IMAGE_ID напрямую через реализацию Storage. Запись документа продолжит ссылаться на отсутствующее содержимое. Не очищайте файлы по одному только возрасту. Сначала подтвердите, что файл не связан с активным документом, шаблоном или незавершенным преобразованием.

Если сохранение файла завершилось, а запись документа не создалась, API документа не предоставляет надежный способ найти все оставшиеся данные. Зафиксируйте ошибку Result и состояние хранилища. Затем проверьте оставшийся файл вручную или средствами обслуживания конкретного хранилища.

Диагностировать частые ошибки

Начинайте диагностику с этапа, на котором появился признак. Ошибки шаблона и полей разбирайте по статье Шаблоны, поля и форматирование, ошибки создания и повторного запуска — по статье Документы, проблемы хранилища и преобразования — по статье Файлы, форматы и преобразование, отказы в доступе — по статье Права доступа.

В этой статье проверяйте признаки, связанные с нагрузкой и очередью.

Признак

Что проверить

Действие

Выросло время обработки исходного файла

Число запросов, размер шаблона и длину множественных полей

Оптимизируйте провайдер или разделите большой документ

Выросло только время создания PDF и JPG

Задержку очереди transformer и размер исходного файла

Не запускайте повторную генерацию, дождитесь завершения преобразования

Растут время и память вместе со списком

Число элементов и размер повторяемого блока

Ограничьте набор по правилу приложения или разделите его на несколько файлов

Документ из очереди не обновился

Запуск агента, журнал постановки и значение updateTime документа

Устраните причину и повторно добавьте задание

Выбрать повторный запуск

Выбирайте повторный запуск по сохраненному состоянию. Для нового и обновляемого документа используйте правила раздела «Обработать ошибки» статьи Документы. Если не созданы только PDF или JPG, следуйте разделу «Повторно запустить преобразование» статьи Файлы, форматы и преобразование.

Если операция уже выполняется в другом процессе, дождитесь ее завершения или отмените средствами приложения до нового запуска. Всегда проверяйте Result::isSuccess() и сохраняйте сообщения ошибок вместе с идентификатором документа и названием этапа.

Проверить результат оптимизации

После изменений выполните финальную проверку на том же наборе документов.

  1. Повторите исходные измерения. Сравните время генерации, число запросов, пиковую память, размеры файлов и время ожидания PDF и JPG.

  2. Проверьте сохранность данных. Сравните значения обязательных полей, число строк повторяемых блоков и содержимое файлов до и после оптимизации.

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

Оптимизация завершена, если нагрузка снизилась без потери данных, повторной генерации и нарушения связей между записью документа и файлами.

Связанные материалы