Инструменты 1С · описание оптимизации · v8unpack-accel 2.0.0 · 29.09.2026

Ускорение распаковки 1С через v8unpack

Как распаковка крупных конфигураций 1С без Конфигуратора перестала падать и вставать на сотни часов, а потом ускорилась ещё в несколько раз. Пакет v8unpack не меняли: все исправления подключаются во время запуска отдельными модулями-обёртками, результат побайтно тот же.

Работа шла на типовых и доработанных конфигурациях 1С: УТ 11.5, УНФ 3.0.14, БП 3.0, бухгалтерия для Беларуси и расширения к ним, в том числе коннектор Битрикс24. Инструмент — saby-integration/v8unpack 1.2.13, Python 3.11, Windows 11.

CPU распаковки .cfe 3 МБ
−68%
2.0.0 против 1.0.0: 20,98 → 6,62 с, медиана 5 раундов
УТ 11.5, 883 МБ, целиком
8 мин 25 с
было 46,5 мин (1.0.0); 68 177 файлов побайтно те же
Тяжёлая форма 2 МБ, разбор
537 с → 0,06 с
CPU, исходный v8unpack против 2.0.0
Результат распаковки
тот же
все 35 прогонов .cfe — одно дерево

1. Зачем

Чтобы разобрать доработки клиента, нужен исходный код конфигурации и расширений. Конфигуратор для этого тяжёл: нужна база, загрузка файла и выгрузка в XML. v8unpack -E превращает бинарный .cf, .cfe или .epf в дерево JSON и BSL с понятными именами без платформы 1С. Для чтения кода и поиска доработок этого достаточно.

На расширениях по несколько мегабайт штатный v8unpack работает. На типовых конфигурациях в 0,8–1,7 ГБ он падал на гонках Windows, зависал на опции descent или вставал на третьем проходе с прогнозом в сотни часов. Ниже — что именно мешало и как это обошли.

2. Шесть причин, по которым распаковка вставала

СимптомПричинаРешение
Третий проход идёт по 100–400 с на форму, прогноз — сотни часов; процесс занимает ~50 МБ, py-spy стоит в _add_to_current_valueКвадратичное накопление длинных строк в JsonContainerDecoder: строка копится посимвольно в атрибуте объекта, и CPython копирует её целиком на каждый символv8unpack_fastparse.py: накопление через локальную переменную и выборка куска до кавычки через str.find
Распаковка встаёт на крупном контейнере, содержимое при этом то жеdescent — не глубина, а номер версии файла для инкрементальной сборки. Любое значение включает OrganizerFileCE с побайтным сравнением O(N²)Не передавать --descent; обёртки по умолчанию его не передают
WinError 32, 5, 87 при параллельной распаковкеГонки Windows: блокировка при os.rename, драйвер-фильтр антивируса, сбой пула после падения воркераСерийный пул вместо multiprocessing и ретраи os.replace/os.remove на WinError 32/5 (до 60 попыток)
Расширение коннектора падает в decode_stage_1Поток, закончившийся в режиме READ_TEXT_FILE, — валидный текстовый файл, но decode_file бросает исключение. В 1.2.13 не исправленоТолерантный decode_file: при READ_TEXT_FILE вернуть накопленные данные
Объекты «неизвестный тип вложенных метаданных 102f6202…» пропускаютсяВид метаданных «Цвет палитры» (PaletteColor) платформы 8.5.1 v8unpack не знает, в том числе 1.2.13v8unpack_newtypes.py регистрирует тип на лету; 130 цветов УНФ распакованы
Машина тормозит во время распаковки, хотя приоритет «понижен»SetPriorityClass через ctypes без restype/argtypes передаёт псевдодескриптор 32-битным числом и молча возвращает 0Явные типы c_void_p; процесс работает с BelowNormal

Плюс эксплуатационные правила: временный каталог (TEMP, TMP, TMPDIR) переносится на диск с запасом до импорта v8unpack. Целевая папка и temp исключаются из антивируса и индексации Windows. Перед распаковкой целостность .cf проверяет check_cf.py. Долгий прогон запускается отделённо через WMI.

3. Главное ускорение: разбор длинных строк

Формат контейнера 1С — скобочный текст. Тексты модулей форм и длинные описания лежат в нём строками по мегабайту и больше. Разборщик v8unpack накапливал такую строку так:

self.current_value += value   # value — один символ

Левый операнд живёт в атрибуте объекта, у строки две ссылки, и CPython не может дописать её на месте: на каждый символ создаётся новая строка и копируется вся накопленная. Время растёт квадратично от длины.

Патч сохраняет автомат разбора и меняет только способ накопления:

current = self.current_value
self.current_value = None   # у строки остаётся одна ссылка
current += value            # CPython расширяет её на месте
self.current_value = current

В режиме чтения строки текст до ближайшей кавычки берётся одним куском через str.find, а не циклом по символам. Эквивалентность проверяет v8unpack_fastparse_check.py: 400 случайных файлов decode_stage_1, расхождений нет, исключения совпадают. Проверено на v8unpack 1.2.6 и 1.2.13.

Перепроверка 29.09 на самой тяжёлой форме УНФ (2 МБ): исходный разборщик — 537 с CPU, с этим патчем (1.0.0) — 0,53 с. В 2.0.0 разбор идёт уже не посимвольно, а через json.loads: 0,06 с на ту же форму (раздел 8).

4. Сравнение скорости и нагрузки на CPU

Сравнение парами с чередованием на одном файле — расширении 3 МБ (1483 файла), 5 раундов. Машина всё время была загружена чужими процессами, поэтому сравнение идёт по CPU процесса, а не по времени по часам. Методика, все прогоны и проверки — в отчёте о замерах.

Показатель (медиана 5 раундов)1.0.02.0.0Изменение
CPU прогона целиком, с20,986,62−68%
— user, с12,252,91−76%
— kernel, с8,923,31−63%
Распаковка бинарника0,490,47−4%
Вложенные контейнеры1,150,89−23%
Разбор объектов7,923,53−55%
Организатор10,360,63−94%
Время по часам, с (шум фона)37,511,9−68%
Пик памяти, МБ4144+7%

CPU всех раундов: 1.0.0 — 25,22, 20,06, 20,70, 20,98, 21,31 с; 2.0.0 — 6,77, 6,62, 5,98, 5,48, 7,50 с.

Дерево результата во всех 35 прогонах одно и то же (1483 файла, 598 каталогов, 30 946 055 байт, tree_sha 9539208d0b4b…) — в том числе с любым выключенным ускорением. Оно же было у штатного v8unpack и у 1.0.0 в контрольном замере 28.09.

Что дало каждое ускорение

В каждом варианте выключено ровно одно ускорение 2.0.0. Чем выше столбец, тем больше CPU без него — тем больше оно даёт.

Штатный v8unpack против 1.0.0

Контрольный замер 28.09 на том же файле: штатный v8unpack -E с параллельным пулом — 33,46 с CPU всего дерева процессов и 218 МБ памяти, 1.0.0 — 23,51 с и 36 МБ. Серийный режим экономит 30% CPU и 84% памяти: не нужны шесть процессов с копией интерпретатора и данных в каждом.

Крупная конфигурация

УТ 11.5.27.93 (883 МБ, 68 177 файлов): 1.0.0 — 46,5 мин (журнал 28.09), 2.0.0 — 8 мин 25 с (29.09). Организатор — 29 мин против 31 с, разбор объектов — 10,5 против 4,5 мин. Дерево 2.0.0 побайтно равно распаковке 1.0.0. Условия прогонов разные, поэтому это ориентир. Таблица по этапам — в отчёте.

5. Замеры на реальных конфигурациях

КонфигурацияРазмерРежимРезультат
БП 3.0818 МБштатный параллельный пулпадал на WinError 32/5/87; серийно — 127 764 файла
БП Беларусь264 МБсерийный, TEMP на D:, без fastparse, v8unpack 1.2.61 ч 27 мин (первый разбор 56,6 мин), 1,7 ГБ дерева, без ошибок; один объект шёл 26 мин
УНФ 3.0.14.1591 ГБсерийный, без fastparseтретий проход встал, прогноз — сотни часов
УНФ 3.0.14.1591 ГБ+ fastparse, продолжение с третьего проходатретий проход 17 мин 47 с (17 тыс. объектов ~7 мин, 11,5 тыс. с формами ~10 мин); 4,5 ГБ, 58 видов объектов
УТ 11.5.27.93883 МБсерийный + fastparse + newtypes, v8unpack 1.2.13 (1.0.0)46,5 мин целиком (первый разбор 10,5 мин), 68 177 файлов, без ошибок
УТ 11.5.27.93883 МБv8unpack-accel 2.0.0, v8unpack 1.2.138 мин 25 с целиком (организатор 31 с), 68 177 файлов и 24 075 каталогов побайтно как у 1.0.0
Коннектор Битрикс24 для УТ3 МБто жеоколо минуты, 1547 файлов
Коннектор Битрикс24 для УНФ3 МБ1.2.13 + патчи1483 файла, совпадает с 1.2.6, кроме служебного файла

Прогоны шли на рабочей машине с фоновой нагрузкой.

Две полные распаковки прошли через одни и те же этапы и записали длительность каждого в журнал:

ЭтапБП Беларусь, 264 МБ
1.2.6, без fastparse
УТ 11.5, 883 МБ
1.2.13, с fastparse
Разбор объекта56,6 мин10,5 мин
Организуем код22,6 мин29,0 мин
Всего87,3 мин46,5 мин
Всего на 1 МБ исходного файла19,8 с3,2 с

Это ориентир, а не контрольный опыт: конфигурации, версии v8unpack и фон разные. Контрольный замер на одном и том же файле — в разделе 4.

Зашифрованный zip с дистрибутивом лучше распаковывать 7-Zip: питоновский ZipCrypto даёт около 40 МБ/мин, то есть ~20 минут на 883 МБ, 7-Zip справляется за секунды.

6. Хронология

ДатаЧто сделали
24.07Первый патч: толерантный decode_file, чтобы распаковать расширение коннектора (1512 файлов). Пул ещё параллельный, descent=100 по умолчанию.
31.07БП 3.0 (818 МБ): гонки Windows в параллельном пуле. Появились устойчивый и серийный варианты, найдена ловушка descent.
26.08Серийная обёртка в шаблоне проектов 1С: descent по умолчанию не передаётся.
25.09Лаунчер: TEMP на другом диске до импорта, пониженный приоритет, зашитые пути. БП Беларусь (264 МБ, 1,7 ГБ дерева) — 1 ч 27 мин.
26.09УНФ 1 ГБ встала на третьем проходе. py-spy показал квадратичное накопление строк. Появились fastparse, newtypes, resume и исправление приоритета, v8unpack обновлён до 1.2.13.
28.09Инструменты перенесены в проект УТ, гайд обновлён до 1.2.0. УТ 11.5 (883 МБ) распакована за 46,5 мин. Проект выделен отдельно, поставка зафиксирована как 1.0.0. Контрольный замер: против штатного v8unpack CPU −30%, память −84%; найдены четыре места для ускорения.
29.09Анализ исходников v8unpack и профилей: ещё пять ускорений, среди них разбор через json.loads и организатор без копирования. Все внедрены в 2.0.0 отдельными модулями. Приёмка: разбор сверен с исходным v8unpack, деревья .cfe и УТ 11.5 побайтно те же. УТ 11.5 — 8 мин 25 с.

7. Состав и запуск

ФайлЧто делаетКогда
run_unpack.pyлаунчер: TEMP рядом с результатом до импорта, зашитые пути, замер времени; патчи берёт импортом v8unpack_serialкрупные .cf
v8unpack_serial.pyсерийный пул, ретраи WinError 32/5, толерантный decode_file, пониженный приоритет; подключает все ускорения, печатает версию.cf, .cfe, .epf из командной строки
v8unpack_fastparse.pyавтомат разбора: длинные строки и значения вне кавычек кускомподключаются автоматически, выключаются V8UNPACK_DISABLE
v8unpack_accum.pyсклейки строк через список: base64, текстовые файлы, раскладка модулей
v8unpack_fastbrace.pyразбор скобочного формата через json.loads, необычное — исходному автомату
v8unpack_jsonwrite.pyзапись JSON своим сериализатором одной строкой
v8unpack_io.pyвложенные контейнеры в памяти, кэш каталогов
v8unpack_organizer.pyорганизатор без копирования decode_stage_3
v8unpack_newtypes.pyнеизвестные виды метаданных (PaletteColor, 8.5.1)подключается автоматически
v8unpack_resume.pyпродолжение с третьего прохода и журнал медленных объектовпосле обрыва; нужен psutil
v8unpack_fastparse_check.pyсверка всей цепочки разбора с исходным JsonContainerDecoderпосле обновления v8unpack
check_cf.pyпроверка целостности контейнераперед распаковкой крупного файла
python tools/check_cf.py "<файл.cf>"
python tools/run_unpack.py "<файл.cf>" "<каталог_результата>" "<каталог_temp_на_D>"
python tools/v8unpack_serial.py "<файл.cfe>" "<каталог_результата>"
python tools/v8unpack_resume.py "<TEMP>\tmpXXXX" "<каталог_результата>" "<журнал>"
python tools/v8unpack_fastparse_check.py "<TEMP>\tmpXXXX\decode_stage_1" 400

Отделённый запуск, который переживает закрытие сессии и не наследует её дескрипторы:

Invoke-CimMethod -ClassName Win32_Process -MethodName Create -Arguments @{
  CommandLine='cmd /c "python tools\run_unpack.py <src> <out> <temp> > out.log 2> err.log"'}

Проходы распаковки: извлечение контейнера → decode_stage_1 → «Разбираем вложенные объекты» → «Организуем код». После обрыва на третьем проходе первые два не повторяются: decode_stage_1 во временном каталоге уже готов.

Любое ускорение 2.0.0 выключается переменной окружения — на случай, если после обновления v8unpack что-то разойдётся. Результат при любом наборе побайтно одинаковый, первая строка вывода показывает версию и включённые ускорения:

set V8UNPACK_DISABLE=fastbrace,inplace
python tools\v8unpack_serial.py "<файл.cfe>" "<каталог_результата>"
v8unpack-accel 2.0.0; ускорения: fastparse, accum, jsonwrite, io

Версии и поставки

Поставки лежат в releases/v8unpack-accel/. 1.0.0 — базовая, в том виде, в каком инструменты разнесены по рабочим проектам. 2.0.0 — все ускорения: те же tools/, девять патчей к 1.0.0 (по одному на изменение), манифест sha256 и доказательства приёмки. Изменения по версиям — CHANGELOG.md. Одна команда проверяет хеши, переигрывает патчи поверх 1.0.0 и сверяет результат с рабочей копией:

python releases\v8unpack-accel\verify_release.py --replay --working

8. Ускорения 2.0.0 и что ещё можно ускорить

После 1.0.0 распаковка упиралась уже не в одну квадратичную строку, а в сумму трёх вещей. Первая — посимвольный автомат разбора на Python. Вторая — запись JSON кусками по нескольку байт. Третья — десятки тысяч лишних файловых операций: запись, переименование, повторное чтение, копирование. Контрольный замер 28.09 и анализ исходников 29.09 нашли восемь мест. Все они вошли в 2.0.0 отдельными модулями, каждый проверен на побайтное совпадение результата.

МодульЧто былоЧто сталоЗамер
fastbraceАвтомат разбора идёт по файлу посимвольно на Python: вызов метода на каждый символ или кусокСкобочный текст переводится в JSON операциями уровня C (split по чётности кавычек, replace, translate) и разбирается json.loads. Необычные файлы уходят в исходный автоматразбор в 2,8–5,5 раза быстрее; 11 130 файлов — 0 расхождений
jsonwritejson.dump с отступами кодирует на чистом Python и пишет в файл миллионы кусков по отдельностиСвой сериализатор: рекурсия в один список, строки кодирует C-функция, одна запись в файл. Формат побайтно как у json.dump2,7× на 4060 JSON-файлах УТ, 0 расхождений
inplaceОрганизатор копирует каждый файл decode_stage_3 в результат и переписывает все модули и формыdecode_stage_3 переносится в результат одним переименованием; на месте обрабатываются только модули и формы, неизменённые не перезаписываютсяУТ 11.5: 29 мин → 31 с
accumМодуль BSL копится в строке внутри словаря: полная копия на каждую строку модуля. То же с base64 в формах и текстовыми файламиКуски копятся в списке и склеиваются один размодуль 3,2 МБ: 30 с → 0,08 с
ioВложенный контейнер пишется на диск, переименовывается, читается заново и удаляется; makedirs на каждый записанный JSONВложенный контейнер разбирается из памяти; созданные каталоги запоминаютсяменьше файловых операций; нет переименований сразу после записи — источника WinError 32
fastparseЧисла и UUID вне кавычек читаются по одному символуКусок до ближайшего разделителя берётся регулярным выражениемкорневой файл 640 КБ: 0,45 → 0,03 с; работает в запасном пути fastbrace

Вклад каждого модуля в прогон целиком — в разделе 4. Подробности и все сверки — releases/v8unpack-accel/v2.0.0/PATCHES.md.

Как json.loads разбирает скобочный формат

Правила автомата v8unpack в основном режиме короткие:

Строки с удвоенными кавычками выделяются чётностью: text.split('"') даёт чередование «вне строки / внутри строки». Во фрагментах «вне» разделители превращаются в скобки и кавычки JSON, фрагменты «внутри» экранируются и склеиваются обратно через \". Всё это делают C-функции над целыми строками, Python-цикла по символам нет.

Base64-картинки в формах и текстовые файлы воспроизведены отдельно. Файл целиком из base64, объект сразу после } без запятой, пустые строки и любая ошибка json.loads уходят в исходный автомат — результат тот же, просто медленнее. Такие файлы — около 2% всех файлов decode_stage_1.

Что ещё можно ускорить

НаправлениеОценкаПочему не сделано
Ограниченный параллелизм (2–4 процесса) на разборе вложенных объектов и формахгипотеза, не замерено: на свободной машине сокращает время третьего прохода (у 2.0.0 на УТ — 4,5 из 8,4 мин), CPU не уменьшаетпроверялся только на загруженной машине: 4 процесса дали +24% CPU и ни секунды выигрыша. Нужен замер на свободной машине и крупный .cf без сбоев
Меньше файловых операцийоколо половины оставшегося CPU — время ядра Windows на открытие, запись и закрытие файловчисло выходных файлов задаёт v8unpack; дальше — только объединять запись метаданных объекта (.id.json и .json), что меняет формат результата
Исключения антивируса для temp и результатаснимают ожидание проверки файлов; CPU распаковки не меняют (проверено)на машине замеров защита в реальном времени выключена; эффект по времени надо мерить с включённой
Сборка .epf/.cfe (build)jsonwrite уже дал −11% CPUсборка редкая и быстрая; её результат недетерминирован (новые uuid4() в versions), поэтому сверять можно только содержимое
Исправления в самом v8unpack (upstream)всё перечисленное выше без обёртокрешение о публикации и PR — за владельцем

9. Ограничения