From 333bcad9384cf4e7146aef2732a2393b23fa3c2d Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Sat, 22 Aug 2026 15:59:57 +0300 Subject: [PATCH] =?UTF-8?q?fix(meta-validate):=20batch=20=D0=B2=D1=8B?= =?UTF-8?q?=D0=BF=D0=BE=D0=BB=D0=BD=D1=8F=D0=B5=D1=82=D1=81=D1=8F=20=D0=B2?= =?UTF-8?q?=20=D0=BE=D0=B4=D0=BD=D0=BE=D0=BC=20=D0=BF=D1=80=D0=BE=D1=86?= =?UTF-8?q?=D0=B5=D1=81=D1=81=D0=B5=20=D0=B2=D0=BC=D0=B5=D1=81=D1=82=D0=BE?= =?UTF-8?q?=20=D0=BF=D1=80=D0=BE=D1=86=D0=B5=D1=81=D1=81=D0=B0=20=D0=BD?= =?UTF-8?q?=D0=B0=20=D0=BE=D0=B1=D1=8A=D0=B5=D0=BA=D1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit py-порт в batch-режиме запускал сам себя через subprocess на каждый объект, и на разбор в единицы миллисекунд приходилось ~146 мс старта интерпретатора с импортом lxml. На корпусном прогоне это давало 92 мс на объект против 40 мс у PS — при том, что обычно py быстрее в разы. Причина асимметрии: PS вызывает себя через & в том же процессе, платя свой дорогой старт один раз на батч. Теперь py делает то же самое: файл компилируется один раз и выполняется в свежем globals() на каждый объект. Изоляция состояния получается той же, что у & в PowerShell (script-переменные не переживают вызов), а старт и компиляция платятся однократно. Прогон по acc_8.3.24 и erp_8.3.24 (33 895 объектов): 274 c против ~2900 c, вывод побайтово тот же и совпадает с PS-портом строка в строку. Батч с битым объектом в середине по-прежнему доходит до конца и возвращает ненулевой код, -OutFile пишет те же файлы на объект. Заодно уточнён комментарий про кэш состава конфигурации: «порождает процесс на объект» было верно только для py, тогда как в PS кэш не переживает объект из-за новой области видимости. Co-Authored-By: Claude Opus 5 (1M context) --- .../meta-validate/scripts/meta-validate.ps1 | 5 ++- .../meta-validate/scripts/meta-validate.py | 42 +++++++++++++++---- 2 files changed, 36 insertions(+), 11 deletions(-) diff --git a/.claude/skills/meta-validate/scripts/meta-validate.ps1 b/.claude/skills/meta-validate/scripts/meta-validate.ps1 index 13b607b62..6ee0bbdc8 100644 --- a/.claude/skills/meta-validate/scripts/meta-validate.ps1 +++ b/.claude/skills/meta-validate/scripts/meta-validate.ps1 @@ -1493,8 +1493,9 @@ if ($childObjNode) { # нет в составе -> платформа отвергнет загрузку всегда -> ERROR # в составе, файла нет -> полная загрузка упадёт, частичная пройдёт -> WARN # ChildObjects конфигурации — плоский список <Вид>Имя, поэтому ищем подстроку в границах секции -# без копирования и без разбора XML: batch-режим порождает процесс на объект, и полная карта (~100 мс) -# оплачивалась бы каждым объектом. +# без копирования и без разбора XML. Кэш живёт только в пределах одного объекта: в batch-режиме +# каждый следующий разбирается заново (в PS — новая область видимости скрипта при вызове через &, +# в py — вовсе новый процесс), так что полная карта (~100 мс) оплачивалась бы каждым объектом. $script:cfgText = $null $script:cfgChildStart = -1 diff --git a/.claude/skills/meta-validate/scripts/meta-validate.py b/.claude/skills/meta-validate/scripts/meta-validate.py index 83098600e..e767d87c3 100644 --- a/.claude/skills/meta-validate/scripts/meta-validate.py +++ b/.claude/skills/meta-validate/scripts/meta-validate.py @@ -3,13 +3,17 @@ import argparse import os import re -import subprocess import sys from lxml import etree -sys.stdout.reconfigure(encoding="utf-8") -sys.stderr.reconfigure(encoding="utf-8") +# В batch скрипт выполняется повторно в том же процессе, и поток может оказаться подменённым +# (у StringIO нет reconfigure) — кодировка к этому моменту уже выставлена первым прогоном. +try: + sys.stdout.reconfigure(encoding="utf-8") + sys.stderr.reconfigure(encoding="utf-8") +except AttributeError: + pass # Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet] # регистр не различают, в argparse совпадение точное. @@ -50,17 +54,36 @@ out_file = args.OutFile path_list = [p.strip() for p in args.ObjectPath.split('|') if p.strip()] if len(path_list) > 1: + # Каждый объект проверяется этим же скриптом в СВЕЖЕМ globals() — так же, как PS-порт вызывает + # себя через & (тот же процесс, новая область видимости). Отдельный процесс на объект стоил бы + # старта интерпретатора с импортом lxml (~146 мс) при разборе в единицы миллисекунд; здесь и + # старт, и компиляция файла платятся один раз на весь батч. batch_ok = 0 batch_fail = 0 + with open(__file__, encoding="utf-8") as _f: + _code = compile(_f.read(), __file__, "exec") + _saved_argv = sys.argv for single_path in path_list: - cmd = [sys.executable, __file__, "-ObjectPath", single_path, "-MaxErrors", str(max_errors)] + argv = ["meta-validate.py", "-ObjectPath", single_path, "-MaxErrors", str(max_errors)] if detailed: - cmd.append("-Detailed") + argv.append("-Detailed") if out_file: base, ext = os.path.splitext(out_file) obj_leaf = os.path.splitext(os.path.basename(single_path))[0] - cmd += ["-OutFile", f"{base}_{obj_leaf}{ext}"] - rc = subprocess.call(cmd) + argv += ["-OutFile", f"{base}_{obj_leaf}{ext}"] + sys.argv = argv + rc = 0 + try: + exec(_code, {"__name__": "__main__", "__file__": __file__}) + except SystemExit as e: + rc = e.code if isinstance(e.code, int) else 0 + except Exception as e: + # Падение одного объекта не должно рвать батч — в варианте с отдельным процессом + # это обеспечивалось изоляцией процессов. + print(f"[ERROR] {single_path}: {type(e).__name__}: {e}", file=sys.stderr) + rc = 1 + finally: + sys.argv = _saved_argv if rc == 0: batch_ok += 1 else: @@ -1417,8 +1440,9 @@ if child_obj_node is not None: # нет в составе -> платформа отвергнет загрузку всегда -> ERROR # в составе, файла нет -> полная загрузка упадёт, частичная пройдёт -> WARN # ChildObjects конфигурации — плоский список <Вид>Имя, поэтому ищем подстроку в границах секции -# без копирования и без разбора XML: batch-режим порождает процесс на объект, и полная карта (~100 мс) -# оплачивалась бы каждым объектом. +# без копирования и без разбора XML. Кэш живёт только в пределах одного объекта: в batch-режиме +# каждый следующий разбирается заново (в py — новый процесс, в PS — новая область видимости скрипта), +# так что полная карта (~100 мс) оплачивалась бы каждым объектом. _cfg_text = None _cfg_child_lower = None