perf(handles): stub 索引增量重建 + vtable on-demand pyi 缓存类级化,消除 worker 重复解析

- 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)
This commit is contained in:
TermiNexus committed 2026-08-18 05:27:53 +08:00
1 parent 043c65ee52
commit 7917be0fa1
2 files changed
+81 -53

No files matched your search

+26 -8
View File
@@ -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())
+55 -45
View File
@@ -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]