From 7917be0fa1f825acaf2012fae36c57d11d6b81e3 Mon Sep 17 00:00:00 2001 From: TermiNexus Date: Tue, 18 Aug 2026 05:27:53 +0800 Subject: [PATCH] =?UTF-8?q?perf(handles):=20stub=20=E7=B4=A2=E5=BC=95?= =?UTF-8?q?=E5=A2=9E=E9=87=8F=E9=87=8D=E5=BB=BA=20+=20vtable=20on-demand?= =?UTF-8?q?=20pyi=20=E7=BC=93=E5=AD=98=E7=B1=BB=E7=BA=A7=E5=8C=96=EF=BC=8C?= =?UTF-8?q?=E6=B6=88=E9=99=A4=20worker=20=E9=87=8D=E5=A4=8D=E8=A7=A3?= =?UTF-8?q?=E6=9E=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - HandlesImports._build_stub_index:以已扫描文件集合替代文件数签名,无新增文件时仅一次集合比较;新增文件只增量解析,消除每编译完一个文件即全量重扫(prof 847 次全量重建 61s) - HandlesImports._TryLoadStructFromStub:索引命中直接返回,仅在 miss 时才触发增量扫描 - HandlesExprCall._try_register_vtable_ondemand:函数局部 pyi_cache 提升为类级 _vtable_pyi_cache / _vtable_methods_cache,437 次 open+ast.parse 同批 pyi 降为每 pyi 一次(prof 38.5s) --- lib/core/Handles/HandlesExprCall.py | 34 +++++++--- lib/core/Handles/HandlesImports.py | 100 +++++++++++++++------------- 2 files changed, 81 insertions(+), 53 deletions(-) diff --git a/lib/core/Handles/HandlesExprCall.py b/lib/core/Handles/HandlesExprCall.py index 768f9c4..f857585 100644 --- a/lib/core/Handles/HandlesExprCall.py +++ b/lib/core/Handles/HandlesExprCall.py @@ -26,7 +26,15 @@ def _safe_ptr_type(ty: ir.Type) -> ir.Type: return ty -class ExprCallHandle(BaseHandle): +class ExprCallHandle(BaseHandle): + # vtable on-demand 缓存(类级,跨 worker 任务共享,进程内单线程无竞争)。 + # 原实现 pyi_cache 是 _try_register_vtable_ondemand 函数局部变量,每次调用都 + # 重建 → 437 次调用 = 437 次 open+ast.parse 同一批 pyi(prof 38.5s)。 + # vpi 以 sha1 命名、单次编译内只增不减,路径不变则内容不可变,进程内缓存安全。 + # _vtable_pyi_cache:pyi 文件绝对路径 → {类名: ClassDef 节点} + # _vtable_methods_cache:(sha1, class_name) → (methods, is_cvtable, is_novtable, parent_name) + _vtable_pyi_cache: dict[str, dict[str, ast.ClassDef]] = {} + _vtable_methods_cache: dict[tuple[str, str], tuple[list[str], bool, bool, str | None]] = {} def _append_eh_msg_out_arg(self, CallArgs: list[ir.Value], func: ir.Function | None, Gen: LlvmGeneratorMixin) -> None: if not func: return @@ -2184,18 +2192,19 @@ class ExprCallHandle(BaseHandle): project_root = import_handler._find_project_root() struct_sha1_map = getattr(Gen, '_struct_sha1_map', {}) - # 缓存已读取的 .pyi 文件(sha1 -> class_map) - pyi_cache: dict[str, dict[str, ast.ClassDef]] = {} - + # 类级缓存已读取的 .pyi 文件(绝对路径 -> class_map),跨 worker 任务共享。 + # 原实现为函数局部缓存,每次调用重建 → 437 次 open+ast.parse 同一批 pyi。 def load_pyi(sha1: str) -> dict[str, ast.ClassDef] | None: - if sha1 in pyi_cache: - return pyi_cache[sha1] pyi_path = os.path.join(project_root, 'temp', f'{sha1}.vpi') if not os.path.isfile(pyi_path): # 回退读取旧缓存 .pyi pyi_path = os.path.join(project_root, 'temp', f'{sha1}.pyi') if not os.path.isfile(pyi_path): return None + abs_pyi = os.path.abspath(pyi_path) + cmap = self._vtable_pyi_cache.get(abs_pyi) + if cmap is not None: + return cmap with open(pyi_path, 'r', encoding='utf-8') as f: pyi_code = f.read() tree = ast.parse(pyi_code) @@ -2203,7 +2212,7 @@ class ExprCallHandle(BaseHandle): for node in ast.iter_child_nodes(tree): if isinstance(node, ast.ClassDef): cmap[node.name] = node - pyi_cache[sha1] = cmap + self._vtable_pyi_cache[abs_pyi] = cmap return cmap def collect_class_methods(class_name: str, sha1: str, visited: set[str]) -> tuple[list[str], bool, bool, str | None]: @@ -2212,6 +2221,13 @@ class ExprCallHandle(BaseHandle): Returns: (methods, is_cvtable, is_novtable, parent_name) """ + # 结果缓存(类级,键 (sha1, class_name)):同一类的 vtable 方法收集 + # 跨 worker 任务重复调用时直接复用(原 437 次顶层调用 + 递归每次都重走)。 + key = (sha1, class_name) + cached = self._vtable_methods_cache.get(key) + if cached is not None: + # 返回新 list:避免调用方(Gen.class_methods)后续修改污染缓存 + return (list(cached[0]), cached[1], cached[2], cached[3]) if class_name in visited: return ([], False, False, None) visited.add(class_name) @@ -2320,7 +2336,9 @@ class ExprCallHandle(BaseHandle): if mn not in covered_names: merged.append(m) - return (merged, is_cvtable, is_novtable, parent_name) + result = (merged, is_cvtable, is_novtable, parent_name) + self._vtable_methods_cache[key] = result + return result methods, is_cvtable, is_novtable, parent_name = collect_class_methods(ClassName, class_sha1, set()) diff --git a/lib/core/Handles/HandlesImports.py b/lib/core/Handles/HandlesImports.py index 92c29ec..8cce6cc 100644 --- a/lib/core/Handles/HandlesImports.py +++ b/lib/core/Handles/HandlesImports.py @@ -26,7 +26,10 @@ class ImportHandle(BaseHandle): # 一次扫描所有 .stub.ll 文件构建索引,避免 _TryLoadStructFromStub 每次调用都全量扫描 # (原 609 次调用 × ~100 stub 文件 × ~50 行 = 5.5M 正则匹配 → 一次扫描 ~5000 匹配) _stub_index: dict[str, list[tuple[str, str, str, bool]]] = {} - _stub_index_signature: tuple[int, int] | None = None # (output_stub_count, temp_stub_count) + # 已解析进索引的 stub 文件绝对路径集合(增量重建用)。 + # 单次编译进程内 stub 只增不减(output 逐步填充),记录已扫描文件后, + # 签名变化只需增量解析新增文件,避免每编译完一个文件就全量重扫(原 847 次全量重建)。 + _stub_index_scanned: set[str] = set() # 类级缓存:跨 worker 任务共享(同一进程内,worker 单线程无竞争)。 # 键均为 (source_sha1, class_name),不同文件同名类(不同 sha1)不冲突。 # classify:_EmitExternalClassDeclLlvm 开头的 bases/decorator_list 分类遍历结果 @@ -49,7 +52,7 @@ class ImportHandle(BaseHandle): self._loaded_pyi_paths.clear() self._emitted_module_decls.clear() # stub 索引在文件间保留(同一 worker 进程内),但标记需检查是否过期 - # 不清除 _stub_index,通过 _stub_index_signature 在下次使用时校验新鲜度 + # 不清除 _stub_index / _stub_index_scanned,通过增量扫描校验新鲜度 def _find_project_root(self) -> str: if self._project_root_cache is not None: @@ -2043,7 +2046,9 @@ class ImportHandle(BaseHandle): """扫描所有 .stub.ll 文件构建 class_name → 条目列表索引 一次扫描替代 _TryLoadStructFromStub 的 609 次全量扫描。 - 通过文件数签名检测新增 stub,支持增量编译。 + 增量策略:记录已解析文件集合 _stub_index_scanned,单次编译进程内 + stub 文件只增不减(output 逐步填充),只增量解析新增文件; + 无新增文件时仅做一次目录集合比较即返回(省去原签名失效后的全量重扫)。 """ ProjectRoot = self._find_project_root() temp_dir = os.path.join(ProjectRoot, 'temp') @@ -2051,49 +2056,50 @@ class ImportHandle(BaseHandle): if not os.path.isdir(temp_dir) and not os.path.isdir(output_dir): return - # 文件数签名:检测 stub 文件新增(增量编译时 output 逐步填充) - output_count = sum(1 for f in os.listdir(output_dir) if f.endswith('.stub.ll')) if os.path.isdir(output_dir) else 0 - temp_count = sum(1 for f in os.listdir(temp_dir) if f.endswith('.stub.ll')) if os.path.isdir(temp_dir) else 0 - signature = (output_count, temp_count) - if self._stub_index_signature == signature and self._stub_index: - return # 索引未过期 - - # 重建索引 - self._stub_index.clear() - for scan_dir, is_output in [(output_dir, True), (temp_dir, False)]: + # 收集当前 stub 文件集合(绝对路径,仅文件名不读内容) + current_files: set[str] = set() + for scan_dir in (output_dir, temp_dir): if not os.path.isdir(scan_dir): continue for filename in os.listdir(scan_dir): - if not filename.endswith('.stub.ll'): - continue - stub_path = os.path.join(scan_dir, filename) - # 预检查文件存在性:os.listdir 与 open 之间文件可能被 --clean 或 - # 并发写入删除(独立 profile 脚本曾触发 FileNotFoundError) - if not os.path.isfile(stub_path): - continue - file_sha1 = filename.replace('.stub.ll', '') - try: - with open(stub_path, 'r', encoding='utf-8') as f: - content = f.read() - for line in content.splitlines(): - stripped = line.strip() - match = re.match(r'%"?((?:[a-f0-9]+\.)?([\w\[\]]+))"?\s*=\s*type\s*(.*)', stripped) - if match: - full_name = match.group(1) - short_name = match.group(2) - struct_body = match.group(3).strip() - # 索引键:short_name 和 full_name 都映射到同一条目 - self._stub_index.setdefault(short_name, []).append((full_name, struct_body, file_sha1, is_output)) - if full_name != short_name: - self._stub_index.setdefault(full_name, []).append((full_name, struct_body, file_sha1, is_output)) - except FileNotFoundError: - # 竞态:isfile 检查后文件被删除,跳过即可(不应触发 strict raise) - continue - except Exception as _e: - if _config_mode == "strict": - raise - _vlog().warning(f"构建 stub 索引失败: {_e}", "Exception") - self._stub_index_signature = signature + if filename.endswith('.stub.ll'): + current_files.add(os.path.join(scan_dir, filename)) + + # 无新增文件 → 索引最新,直接返回 + if current_files <= self._stub_index_scanned: + return + + # 只增量解析新增文件(首次调用时全量) + for stub_path in sorted(current_files - self._stub_index_scanned): + filename = os.path.basename(stub_path) + # 预检查文件存在性:os.listdir 与 open 之间文件可能被 --clean 或 + # 并发写入删除(独立 profile 脚本曾触发 FileNotFoundError) + if not os.path.isfile(stub_path): + continue + is_output = stub_path.startswith(os.path.join(output_dir, '')) + file_sha1 = filename.replace('.stub.ll', '') + try: + with open(stub_path, 'r', encoding='utf-8') as f: + content = f.read() + for line in content.splitlines(): + stripped = line.strip() + match = re.match(r'%"?((?:[a-f0-9]+\.)?([\w\[\]]+))"?\s*=\s*type\s*(.*)', stripped) + if match: + full_name = match.group(1) + short_name = match.group(2) + struct_body = match.group(3).strip() + # 索引键:short_name 和 full_name 都映射到同一条目 + self._stub_index.setdefault(short_name, []).append((full_name, struct_body, file_sha1, is_output)) + if full_name != short_name: + self._stub_index.setdefault(full_name, []).append((full_name, struct_body, file_sha1, is_output)) + except FileNotFoundError: + # 竞态:isfile 检查后文件被删除,跳过即可(不应触发 strict raise) + continue + except Exception as _e: + if _config_mode == "strict": + raise + _vlog().warning(f"构建 stub 索引失败: {_e}", "Exception") + self._stub_index_scanned.add(stub_path) def _TryLoadStructFromStub(self, class_name: str, Gen: LlvmGeneratorMixin, source_sha1: str | None = None) -> None: # 缓存键包含 Gen 实例标识,确保每个模块的 Gen 都能独立加载 struct 定义, @@ -2108,9 +2114,13 @@ class ImportHandle(BaseHandle): # 先设置缓存防止递归重入,但如果加载失败则清除缓存允许后续重试 self._struct_Load_cache[cache_key] = True - # 使用 stub 索引替代全量扫描(609 次 × 100 文件 → 1 次扫描 + O(1) 查询) - self._build_stub_index() + # 使用 stub 索引替代全量扫描(609 次 × 100 文件 → 1 次扫描 + O(1) 查询)。 + # 命中索引直接返回,不触发 _build_stub_index(避免每次调用做目录扫描); + # 仅在 miss 时才增量扫描(无新增文件时仅一次集合比较)。 entries = self._stub_index.get(class_name, []) + if not entries: + self._build_stub_index() + entries = self._stub_index.get(class_name, []) if not entries: del self._struct_Load_cache[cache_key]