Все измерения выполнены на платформе 8.3.27.1989 приложенной обработкой-бенчмарком; цифры в статье — из одного сводного прогона (10 точек от 5 до 50 тысяч строк) и файлов, записанных в этом прогоне. На других версиях платформы поведение может отличаться — обработка позволяет воспроизвести замеры на своей.
Постановка задачи
Задача: выгрузить плоскую таблицу — большое количество однотипных строк с несколькими десятками колонок (строки, числа, даты) — в файлы XLSX и ODS с одинаковыми данными. Табличный документ формируется один раз по макету, затем записывается в файлы штатными средствами платформы:
ТабличныйДокумент.Записать(ИмяФайлаXLSX, ТипФайлаТабличногоДокумента.XLSX);
ТабличныйДокумент.Записать(ИмяФайлаODS, ТипФайлаТабличногоДокумента.ODS);
При нагрузочном тестировании выгрузки выявлена особенность: второй вызов — запись ODS — занимает кратно больше времени, чем запись тех же данных в XLSX, и на него приходится основная часть времени выгрузки.
Готовых объяснений этой особенности найти не удалось — пришлось разбираться самостоятельно. Статья повторяет путь исследования по порядку: замеры зависимости времени записи от размера документа, поиск причины деградации и два решения.
Как деградирует запись ODS с ростом документа
Методика
Замеряется только запись: документ фиксированного содержания формируется на нужное число строк, время вызова Записать() фиксируется в коде через ТекущаяУниверсальнаяДатаВМиллисекундах(). Каждая точка — отдельный документ, размеры от меньшего к большему; данные генерируются с фиксированным зерном, поэтому прогоны воспроизводимы.
Штатный «Замер производительности» конфигуратора для таких сравнений неприменим: он инструментирует каждую строку встроенного языка, но не платформенные вызовы, и искажает соотношение вариантов — код из множества строк встроенного языка под инструментированием дорожает кратно, одиночный платформенный вызов почти нет. Все цифры в статье получены фиксацией времени в коде.
Результаты
Документ строится стандартным способом — выводом области макета с подстановкой параметров, данные содержат строковые и числовые колонки (профиль типичного отчёта). Время платформенной записи ODS по размерам:
| Строк | Запись ODS, с | Цена строки, мс |
| 5 000 | 16,62 | 3,32 |
| 10 000 | 49,39 | 4,94 |
| 15 000 | 76,82 | 5,12 |
| 20 000 | 108,91 | 5,45 |
| 25 000 | 182,01 | 7,28 |
| 30 000 | 271,65 | 9,06 |
| 35 000 | 397,77 | 11,36 |
| 40 000 | 555,28 | 13,88 |
| 45 000 | 808,98 | 17,98 |
| 50 000 | 1115,77 | 22,32 |
Число строк выросло в 10 раз, размер получающегося файла — тоже в 10 раз (с 9,9 до 99,1 МБ), а время записи — в 67 раз; удельная цена строки поднялась с 3,32 до 22,32 мс, в 6,7 раза. Рост не просто быстрее линейного — он ускоряется с размером: на первой половине диапазона цена строки прибавляет проценты от точки к точке, на второй — разы. Документ на 50 тысяч строк платформа записывает 18,6 минуты.
Для сравнения тем же способом замерена запись XLSX того же ряда документов: удельная цена строки практически не меняется с размером — запись XLSX линейна.
Этим объясняется, почему эффект не проявляется при обычном тестировании на малом количестве данных: на десятках и сотнях строк ODS записывается за секунды, и разница с XLSX не бросается в глаза. Деградация становится заметной только на больших объёмах — поэтому и поймана она была именно нагрузочным тестированием.
От чего на самом деле зависит скорость записи
Причина деградации не была очевидна. Выяснить её удалось методом проб и ошибок: изменяли по одному фактору и сравнивали время записи и внутреннее устройство получающихся файлов.
Первая гипотеза — способ формирования документа (ложный путь)
Сначала предположили, что дело в способе формирования: вывод заранее подготовленных неизменяемых строк давал линейную запись и единичные автоматические стили в content.xml файла, а стандартный вывод области макета с подстановкой параметров — рост времени и множество стилей. Гипотеза выглядела правдоподобно, но не пережила контрольного эксперимента: при одинаковом способе вывода и разном содержимом ячеек деградация появлялась и исчезала независимо от способа. Способ формирования влияет на постоянную стоимость строки, но не он причина нелинейности.
Контрольный эксперимент: содержимое числовых колонок
Решающий эксперимент устроен так (он же — пара линий 1–2 приложенного бенчмарка). Два документа формируются одинаково: вывод области макета с подстановкой параметров на каждую строку, один макет, одни и те же строковые колонки. Единственное отличие — содержимое восьми числовых колонок: в первом документе они заполнены буквенными строками, во втором — числами.
| Строк | Буквы: запись, с | мс/стр | Числа: запись, с | мс/стр |
| 5 000 | 7,13 | 1,43 | 16,62 | 3,32 |
| 10 000 | 13,81 | 1,38 | 49,39 | 4,94 |
| 15 000 | 20,87 | 1,39 | 76,82 | 5,12 |
| 20 000 | 27,56 | 1,38 | 108,91 | 5,45 |
| 25 000 | 34,77 | 1,39 | 182,01 | 7,28 |
| 30 000 | 42,90 | 1,43 | 271,65 | 9,06 |
| 35 000 | 48,26 | 1,38 | 397,77 | 11,36 |
| 40 000 | 56,35 | 1,41 | 555,28 | 13,88 |
| 45 000 | 62,94 | 1,40 | 808,98 | 17,98 |
| 50 000 | 70,17 | 1,40 | 1115,77 | 22,32 |
Документ с буквенным содержимым записывается строго линейно: цена строки 1,38–1,43 мс по всему диапазону, разброс в пределах процентов. Документ с числами деградирует. На 50 тысячах строк разница между документами — 70 секунд против 1116, в 16 раз — при полностью одинаковом способе формирования. Деградацию вызывает числоподобное содержимое ячеек.
Вид содержимого, вызывающего деградацию
Дополнительные наблюдения уточняют картину:
- Типизированные числа — основной источник деградации.
- Числоподобные тексты. Замена чисел заранее отформатированными строками вида «12345,67» не помогает: деградация воспроизводится в полном объёме — платформа распознаёт содержимое, похожее на число, и обрабатывает такую ячейку по «числовому» пути.
- Количество числоподобных колонок. Зависимость на 10 000 строк (0/1/2/4/8 числовых колонок из 77): 1,4 / 1,7 / 2,4 / 3,1 / 4,9 мс/строку. Эффект накапливается плавно, пропорционально числу числоподобных ячеек; порогового значения нет.
- Объём данных как таковой роли не играет: удлинение текстов, увеличивающее файл в несколько раз, меняет время записи на десятки процентов, тогда как добавление числовых колонок при сопоставимом размере файла меняет его в разы.
Анализ содержимого файла
Формат ODS — zip-контейнер, автоматические стили ячеек лежат в content.xml. Сравним файлы, записанные в сводном прогоне на точке 10 000 строк:
| Файл (10 000 строк) | Ячеечных стилей в content.xml |
| Платформа, документ с буквами | 137 |
| Платформа, документ с числами | 80 132 |
Восемь числовых колонок на 10 000 строк — это 80 000 числоподобных ячеек: платформа завела отдельный стиль практически на каждую. У буквенного документа тех же размеров стилей 137 — в 585 раз меньше.
Проверяется самостоятельно: обработка сохраняет файлы каждого замера в указанный каталог — достаточно распаковать файл и посчитать вхождения style:family="table-cell" в content.xml. Данные генерируются с фиксированным зерном, поэтому на тех же размерах получатся те же числа.
Модель наблюдаемого поведения
При записи ODS платформа типизирует ячейки по содержимому. Ячейкам с числоподобным содержимым она создаёт индивидуальные записи стилей — практически по стилю на ячейку; их совокупность растёт с размером документа, и обработка каждой следующей строки дорожает. Отсюда нелинейность, ускоряющаяся с размером: чем больше в документе числоподобных ячеек, тем быстрее растёт стоимость. Запись XLSX тех же документов к содержимому нечувствительна и остаётся линейной.
Оговорка: внутреннее устройство платформы закрыто, модель реконструирована по наблюдаемому поведению. Она согласуется со всеми измеримыми фактами: линейностью буквенных документов при любом способе формирования, деградацией на числоподобном содержимом, пропорциональностью числу числоподобных ячеек и количеством стилей в файле.
Решение 1: собственная запись ODS из табличного документа
Универсальное решение — процедура, записывающая готовый ТабличныйДокумент в файл ODS без платформенного Записать():
ЗаписатьТабличныйДокументВФайлODS(ТабДок, ИмяФайла, ПереноситьОформление = Истина)
Контейнер по спецификации ODF 1.2
ODS-файл — zip-контейнер:
mimetype— тип документа; по спецификации размещается первым файлом и без сжатия (платформенная запись размещает его не первым и сжатым — LibreOffice это допускает);META-INF/manifest.xml— перечень файлов контейнера;content.xml— данные и автоматические стили;styles.xml,meta.xml— стили документа и метаданные.
Тело content.xml формируется через ЗаписьXML в файл, затем контейнер собирается ЗаписьZipФайла.
Дедупликация стилей
Стиль ячейки в файле определяется её фактическим оформлением — шрифтом, фоном, рамками, выравниванием — и не зависит от содержимого. По свойствам ячейки вычисляется ключ; соответствие «ключ → имя стиля» обеспечивает один стиль на все одинаково оформленные ячейки документа. В файле собственной записи из сводного прогона (10 000 строк, документ с числами) — 11 ячеечных стилей против 80 132 у платформенной записи того же документа; файл на четверть меньше (76,0 МБ против 99,1 МБ на 50 тысячах строк — размеры обеих записей выводятся в таблице бенчмарка).
Чтение оформления агрегатными свойствами области
Поячеечное чтение свойств (десятки ячеек на строку) дорого — реализация с поячеечным разбором работала медленнее платформенной записи. Использованы агрегатные свойства области: запрошенное у области целиком свойство возвращает значение, когда оно одинаково у всех ячеек, и Неопределено — когда различается:
ОбластьСтроки = ТабДок.Область(НомерСтроки, 1, НомерСтроки, КоличествоКолонок);
Однородна = ОбластьСтроки.ЦветФона <> Неопределено
И ОбластьСтроки.Шрифт <> Неопределено; // и ещё 6 агрегатов
Для однородно оформленных строк свойства читаются один раз с первой ячейки; поячеечный разбор остаётся только для строк с неоднородным оформлением.
Замеры
Собственная запись линейна при любом содержимом ячеек: 8,13–8,37 мс/строку на всех десяти точках сводного прогона. На малых размерах она дороже платформенной — выше постоянная стоимость строки (8,2 мс против 3,3 у платформы на 5 тысячах строк). С ростом документа платформенная запись дорожает, и между 25 и 30 тысячами строк кривые пересекаются; на 50 тысячах собственная запись быстрее платформенной в 2,7 раза (411,75 с против 1115,77).
Ограничение решения — стоимость чтения из табличного документа: агрегатное чтение свойств сканирует все ячейки области, и эта линейная по ширине строки цена платится за каждую строку. Ограничение снимается отказом от табличного документа — см. решение 2.
Решение 2: потоковая запись ODS непосредственно из данных
Для выгрузки табличный документ — промежуточная сущность: данные уже есть в выборке, и content.xml записывается напрямую из них, в том же цикле по выборке. Табличный документ остаётся только для XLSX.
Программный интерфейс из трёх функций
// До цикла: контекст из областей макета - шапка и имена колонок области "Строка"
Контекст = НачатьФайлODS(Макет.ПолучитьОбласть("Шапка"), Макет.ПолучитьОбласть("Строка"));
// В цикле по данным: соответствие "имя параметра колонки - значение"
ЗаписатьСтрокуODS(Контекст, ЗначенияСтроки);
// После цикла
ЗавершитьФайлODS(Контекст, ИмяФайла);
// при ошибке выгрузки - ОтменитьФайлODS(Контекст)
НачатьФайлODS выводит шапку из макета с оформлением, открывает временный content.xml через ФайловыйПоток и запоминает имена колонок. ЗаписатьСтрокуODS записывает одну строку таблицы. ЗавершитьФайлODS дописывает завершение и собирает контейнер. Стили строк известны заранее из области макета; дедупликация не требуется — в файле прямой записи из сводного прогона всего 2 ячеечных стиля.
Результат
Строка ODS записывается в общем проходе по данным, ячейки табличного документа не читаются вовсе; стоимость строки — последовательная запись XML-элементов в файловый поток. В сводном прогоне потоковая запись линейна (4,25–4,46 мс/строку), обгоняет платформенную уже между 5 и 10 тысячами строк, а на 50 тысячах быстрее её в 5,3 раза (212,26 с против 1115,77) и в 1,9 раза быстрее собственной записи из табличного документа (у той сверху стоимость чтения из документа).
Бенчмарк: воспроизведение замеров
К публикации приложена внешняя обработка. Она самодостаточна (не обращается к объектам конфигурации, открывается на любой базе) и воспроизводит ключевые эксперименты статьи на синтетическом макете 77 колонок. Все замеры ложатся в одну сводную таблицу и на один график — четыре линии на общих точках:
- Платформа (буквы) — вывод области макета с подстановкой параметров, числовые колонки заполнены буквенными строками: числоподобного содержимого в документе нет.
- Платформа (числа) — тот же способ вывода, тот же макет; единственное отличие — числовые колонки заполнены числами. Пара линий 1–2 — контролируемый эксперимент из раздела о причинах.
- Собственный писатель — запись того же документа с числами собственной процедурой с переносом оформления.
- Прямая запись — потоковая запись ODS из тех же данных без табличного документа.
Параметры: максимальный размер, число точек замера, длина строковых данных, каталог для файлов замера. Кнопка «Замерить все сценарии» выполняет все четыре линии последовательно; повторных замеров нет (собственный писатель замеряется один раз, на документе с числами). Результаты сохраняются в JSON.
Кроме секунд в таблице выводится удельная цена строки «мс/стр»: на графике абсолютного времени умеренная деградация неотличима от прямой, по удельной цене она видна сразу.
Сводный прогон, из которого взяты все цифры статьи (10 точек, максимум 50 000 строк), — на графике и на скриншоте формы ниже:

Выводы
- Причина нелинейной деградации платформенной записи ODS — числоподобное содержимое ячеек: типизированные числа и тексты вида «12345,67». Платформа создаёт таким ячейкам индивидуальные стили — практически по стилю на ячейку, — и стоимость записи растёт с размером документа: на сводном прогоне удельная цена строки выросла в 6,7 раза при увеличении документа в 10 раз. Способ формирования документа влияет только на постоянную стоимость строки.
- Запись XLSX тех же документов линейна: деградация — свойство именно записи ODS.
- Дедупликация стилей по фактическому оформлению делает собственную запись ODS линейной при любом содержимом (8,2 мс/строку на всех точках, 11 стилей в файле, файл на четверть меньше); ограничение — стоимость чтения из табличного документа.
- Для данной задачи, чтобы не обрабатывать ячейки табличного документа, применена потоковая запись
content.xmlнепосредственно из данных — это экономит около половины времени относительно собственной записи из табличного документа. Решение не универсально: применимо, когда данные проходят через код выгрузки, а структура и оформление строк известны из макета; произвольный готовый табличный документ им не записать — для этого предназначена собственная запись из решения 1.
Замеры выполнены на платформе 8.3.27.1989.
Проверено на следующих конфигурациях и релизах:
- 1С:Библиотека стандартных подсистем, редакция 3.1, релизы 3.1.12.268
Вступайте в нашу телеграмм-группу Инфостарт