Инструменты 1С · описание оптимизации · v8unpack-accel 2.0.0 · 29.09.2026
Как распаковка крупных конфигураций 1С без Конфигуратора перестала падать и вставать на сотни часов, а потом ускорилась ещё в несколько раз. Пакет v8unpack не меняли: все исправления подключаются во время запуска отдельными модулями-обёртками, результат побайтно тот же.
Чтобы разобрать доработки клиента, нужен исходный код конфигурации и расширений. Конфигуратор для этого тяжёл: нужна база, загрузка файла и выгрузка в XML. v8unpack -E превращает бинарный .cf, .cfe или .epf в дерево JSON и BSL с понятными именами без платформы 1С. Для чтения кода и поиска доработок этого достаточно.
На расширениях по несколько мегабайт штатный v8unpack работает. На типовых конфигурациях в 0,8–1,7 ГБ он падал на гонках Windows, зависал на опции descent или вставал на третьем проходе с прогнозом в сотни часов. Ниже — что именно мешало и как это обошли.
| Симптом | Причина | Решение |
|---|---|---|
Третий проход идёт по 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.13 | v8unpack_newtypes.py регистрирует тип на лету; 130 цветов УНФ распакованы |
| Машина тормозит во время распаковки, хотя приоритет «понижен» | SetPriorityClass через ctypes без restype/argtypes передаёт псевдодескриптор 32-битным числом и молча возвращает 0 | Явные типы c_void_p; процесс работает с BelowNormal |
Плюс эксплуатационные правила: временный каталог (TEMP, TMP, TMPDIR) переносится на диск с запасом до импорта v8unpack. Целевая папка и temp исключаются из антивируса и индексации Windows. Перед распаковкой целостность .cf проверяет check_cf.py. Долгий прогон запускается отделённо через WMI.
Формат контейнера 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).
Сравнение парами с чередованием на одном файле — расширении 3 МБ (1483 файла), 5 раундов. Машина всё время была загружена чужими процессами, поэтому сравнение идёт по CPU процесса, а не по времени по часам. Методика, все прогоны и проверки — в отчёте о замерах.
| Показатель (медиана 5 раундов) | 1.0.0 | 2.0.0 | Изменение |
|---|---|---|---|
| CPU прогона целиком, с | 20,98 | 6,62 | −68% |
| — user, с | 12,25 | 2,91 | −76% |
| — kernel, с | 8,92 | 3,31 | −63% |
| Распаковка бинарника | 0,49 | 0,47 | −4% |
| Вложенные контейнеры | 1,15 | 0,89 | −23% |
| Разбор объектов | 7,92 | 3,53 | −55% |
| Организатор | 10,36 | 0,63 | −94% |
| Время по часам, с (шум фона) | 37,5 | 11,9 | −68% |
| Пик памяти, МБ | 41 | 44 | +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 без него — тем больше оно даёт.
Контрольный замер 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. Условия прогонов разные, поэтому это ориентир. Таблица по этапам — в отчёте.
| Конфигурация | Размер | Режим | Результат |
|---|---|---|---|
| БП 3.0 | 818 МБ | штатный параллельный пул | падал на WinError 32/5/87; серийно — 127 764 файла |
| БП Беларусь | 264 МБ | серийный, TEMP на D:, без fastparse, v8unpack 1.2.6 | 1 ч 27 мин (первый разбор 56,6 мин), 1,7 ГБ дерева, без ошибок; один объект шёл 26 мин |
| УНФ 3.0.14.159 | 1 ГБ | серийный, без fastparse | третий проход встал, прогноз — сотни часов |
| УНФ 3.0.14.159 | 1 ГБ | + fastparse, продолжение с третьего прохода | третий проход 17 мин 47 с (17 тыс. объектов ~7 мин, 11,5 тыс. с формами ~10 мин); 4,5 ГБ, 58 видов объектов |
| УТ 11.5.27.93 | 883 МБ | серийный + fastparse + newtypes, v8unpack 1.2.13 (1.0.0) | 46,5 мин целиком (первый разбор 10,5 мин), 68 177 файлов, без ошибок |
| УТ 11.5.27.93 | 883 МБ | v8unpack-accel 2.0.0, v8unpack 1.2.13 | 8 мин 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 справляется за секунды.
| Дата | Что сделали |
|---|---|
| 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 с. |
| Файл | Что делает | Когда |
|---|---|---|
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
После 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 расхождений |
jsonwrite | json.dump с отступами кодирует на чистом Python и пишет в файл миллионы кусков по отдельности | Свой сериализатор: рекурсия в один список, строки кодирует C-функция, одна запись в файл. Формат побайтно как у json.dump | 2,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 — за владельцем |
.cf через v8unpack обратно не собирается — только Конфигуратор. v8unpack меняет только BSL и текст, структуру форм и метаданных — нет.v8unpack_fastparse_check.py, 400 файлов) и полную распаковку контрольного файла со сравнением дерева. Если что-то разошлось, ускорение выключается V8UNPACK_DISABLE.CommonModule.obj.100.bsl, 1.2.13 — без .100. Старые и новые деревья по путям напрямую не сравниваются.dummy.zip (второй контейнер .cf) v8unpack пересобирает при каждой распаковке: содержимое то же, время в записях архива — нет. Хеш этого файла от прогона к прогону разный при любой версии.index или descent — тоже штатный путь..cf, ещё несколько гигабайт занимает temp.