perf(Phase2Translator): 文本读缓存改按路径直存(零 stat),正确性靠写/删点显式失效
- _ReadTextCached 不再 os.stat:原实现每次调用都要 stat 判新旧,等于把省下的 1.2 万次 open 换成 1.2 万次 stat(同为系统调用热点),命中路径仍有系统开销。 改为 OrderedDict[路径 → 文本],命中零系统调用;newline 非 None 的逐字节读不进缓存。 - _InvalidateTextCache 改 O(1)(按路径直接 pop),并在 14 处文本写点 + 1 处 stale 文件删除(os.remove)处调用 —— 读侧不再 stat 后,写/删点显式失效是唯一正确性保证。 - 失效为何必须显式(时间戳判据在同 tick 内会失效):_compile_one 前的 type 定义 重排(行集合不变仅次序变,长度逐字节相同)、_recombine_ll 的旧 sha1 前缀替换 (16 位 hex 等长替换)。 - 安全性前提:TPC 单次运行内被读文件内容不变。已核对:self.sig_files / self.stub_files 指向的都是 Phase1 产物(temp_dir / _GLOBAL_CACHE_DIR,Phase2 期间不再改写);Phase2 自产的 .ll/.stub.ll/.deps.json/.vpi 每次写、删都走上面的失效。 基线 PROF/20260930_224857:_io.open 12,740 次/self 28.8s、read 11,902 次/30.4s、 TextIOWrapper.__exit__ 16.6s(128 个模块却上万次读,同一份产物被多阶段反复打开)。
This commit is contained in:
1 parent
1748940d30
commit
e292e2390d
1 file changed
+35
-35
@@ -54,57 +54,55 @@ def _CachedIsFile(path: str) -> bool:
|
||||
# read 11,902 次 / self 30.4s、TextIOWrapper.__exit__ 16.6s —— 128 个模块
|
||||
# 却产生上万次读,主要是同一份 .stub.ll / .ll / deps.json 被
|
||||
# _compile_one / _recombine_ll / 依赖解析等重复打开)。
|
||||
# 键 = (路径, mtime_ns, 大小):文件被写过或替换后**自动失配**,因此无需手工失效
|
||||
# (比"路径→文本"的裸缓存安全)。只缓存 newline=None 的默认换行翻译读,
|
||||
# 与直接 open(...).read() 语义逐字相同。
|
||||
# 键 = **路径**(不做 stat / 时间戳校验)。性能前提(用户口径):TPC 单次运行内
|
||||
# 非本进程写出的文件内容不变 —— 因此不需要"每次读都 stat 一次"来判新旧;
|
||||
# 若再 stat,等于把省下的 1.2 万次 open 换成 1.2 万次 stat(仍是系统调用热点)。
|
||||
# 正确性前提:**本进程写出/改写/删除过的文件,一律在该动作后调用
|
||||
# _InvalidateTextCache**(写点见文件内全部 open(..., 'w') 与 os.remove 处)。
|
||||
# 只缓存 newline=None 的默认换行翻译读,与直接 open(...).read() 语义逐字相同。
|
||||
# 内存:按总字节预算 + LRU 淘汰;单文件超过 _TEXT_CACHE_FILE_MAX 不入缓存
|
||||
# (合并后的 .ll 可达数十 MB,避免把内存吃光)。
|
||||
# ---------------------------------------------------------------
|
||||
_TEXT_CACHE_MAX_BYTES: int = 64 * 1024 * 1024
|
||||
_TEXT_CACHE_FILE_MAX: int = 32 * 1024 * 1024
|
||||
_TextReadCache: OrderedDict[tuple[str, int, int], str] = OrderedDict()
|
||||
_TextReadCache: OrderedDict[str, str] = OrderedDict()
|
||||
_TextReadCacheBytes: int = 0
|
||||
|
||||
|
||||
def _ReadTextCached(path: str, newline: str | None = None) -> str:
|
||||
"""读文本文件(带 LRU 缓存);newline 非 None 或 stat 失败时退化为直接读。"""
|
||||
"""读文本文件(按路径缓存,零 stat);newline 非 None 时退化为直接读(不进缓存)。"""
|
||||
global _TextReadCacheBytes
|
||||
if newline is None:
|
||||
try:
|
||||
_st = os.stat(path)
|
||||
except OSError:
|
||||
_st = None
|
||||
if _st is not None and _st.st_size <= _TEXT_CACHE_FILE_MAX:
|
||||
_key = (path, _st.st_mtime_ns, _st.st_size)
|
||||
_hit = _TextReadCache.get(_key)
|
||||
if _hit is not None:
|
||||
_TextReadCache.move_to_end(_key)
|
||||
return _hit
|
||||
with open(path, 'r', encoding='utf-8') as _f:
|
||||
_text = _f.read()
|
||||
_TextReadCache[_key] = _text
|
||||
_TextReadCacheBytes += len(_text)
|
||||
while _TextReadCacheBytes > _TEXT_CACHE_MAX_BYTES and len(_TextReadCache) > 1:
|
||||
_, _old = _TextReadCache.popitem(last=False)
|
||||
_TextReadCacheBytes -= len(_old)
|
||||
return _text
|
||||
with open(path, 'r', encoding='utf-8', newline=newline) as _f:
|
||||
return _f.read()
|
||||
if newline is not None:
|
||||
# 逐字节保真的读(newline='' 等)语义特殊:不进缓存,避免与默认换行翻译混用
|
||||
with open(path, 'r', encoding='utf-8', newline=newline) as _f:
|
||||
return _f.read()
|
||||
_hit: str | None = _TextReadCache.get(path)
|
||||
if _hit is not None:
|
||||
_TextReadCache.move_to_end(path)
|
||||
return _hit
|
||||
with open(path, 'r', encoding='utf-8') as _f:
|
||||
_text: str = _f.read()
|
||||
if len(_text) <= _TEXT_CACHE_FILE_MAX:
|
||||
_TextReadCache[path] = _text
|
||||
_TextReadCacheBytes += len(_text)
|
||||
while _TextReadCacheBytes > _TEXT_CACHE_MAX_BYTES and len(_TextReadCache) > 1:
|
||||
_, _old = _TextReadCache.popitem(last=False)
|
||||
_TextReadCacheBytes -= len(_old)
|
||||
return _text
|
||||
|
||||
|
||||
def _InvalidateTextCache(path: str) -> None:
|
||||
"""丢弃 path 的文本读缓存条目:本进程**改写**该文件后必须调用。
|
||||
"""丢弃 path 的文本读缓存条目:本进程**写出/改写/删除**该文件后必须调用。
|
||||
|
||||
只靠 (mtime_ns, size) 判新旧在 Windows 上不安全:文件时间是系统时钟派生,
|
||||
粒度约 15.6ms,「先读 → 同一 tick 内原地改写 → 再读」且改写前后字节数相同
|
||||
(实例:_compile_one 前的 type 定义重排,行集合不变仅次序变,长度逐字节相同)
|
||||
时键完全一致,会命中过期文本。故写侧必须显式失效,不做时间戳兜底。
|
||||
这是本缓存唯一的正确性保证(读侧不再 stat 判新旧):漏掉任一写点,后续读会
|
||||
拿到该路径的旧文本。典型必失效场景:先读 → 同一 tick 内原地改写 → 再读,
|
||||
且改写前后字节数相同(实例:_compile_one 前的 type 定义重排,行集合不变仅次序变;
|
||||
_recombine_ll 的旧 sha1 前缀替换,16 位 hex 等长替换)。
|
||||
"""
|
||||
global _TextReadCacheBytes
|
||||
if not _TextReadCache:
|
||||
return
|
||||
for _k in [k for k in _TextReadCache if k[0] == path]:
|
||||
_TextReadCacheBytes -= len(_TextReadCache.pop(_k))
|
||||
_old = _TextReadCache.pop(path, None)
|
||||
if _old is not None:
|
||||
_TextReadCacheBytes -= len(_old)
|
||||
|
||||
|
||||
def _FindAsanRuntimeDir() -> str | None:
|
||||
@@ -2420,6 +2418,8 @@ class Phase2Translator:
|
||||
for fpath in stale_files:
|
||||
try:
|
||||
os.remove(fpath)
|
||||
# 删除也是"本进程改变了该路径":不失效会让后续读拿到已删文件的旧文本
|
||||
_InvalidateTextCache(fpath)
|
||||
except Exception as e:
|
||||
_vlog().warning(f"Phase2Translator: 忽略异常 {e}", exc_info=e)
|
||||
_vlog().info(f"[清理] 删除 {len(stale_files)} 个过时文件")
|
||||
|
||||
Reference in new issue
Block a user