From 030d0fd14974d38b34e6cb623f35115aeed6e4de Mon Sep 17 00:00:00 2001 From: TermiNexus Date: Tue, 29 Sep 2026 16:26:44 +0800 Subject: [PATCH] =?UTF-8?q?feat(tpc):=20PROF=20=E8=BE=93=E5=87=BA=E6=8C=89?= =?UTF-8?q?=E7=BC=96=E8=AF=91=E5=90=AF=E5=8A=A8=E6=97=B6=E9=97=B4=E5=88=86?= =?UTF-8?q?=E7=9B=AE=E5=BD=95=EF=BC=8C=E4=B8=8D=E5=86=8D=E6=B8=85=E7=90=86?= =?UTF-8?q?=E6=97=A7=E7=9B=AE=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 需求(2026-09-29):PROF 按启动时间分组新建文件夹生成,不清理旧的 PROF。 改动: - Phase2Translator:新增 `StartProfRun()`(在 PROF 根目录下按 `YYYYmmdd_HHMMSS` 新建本次编译专属子目录,同名补 `_N`;失败回落 PROF 根目录)与 `GetProfRunDir()`(主进程用已选定目录,worker 用继承的环境变量 `TPC_PROF_RUN_DIR` 复用同一目录)。原 `_CleanProfFiles()` 删除——它的语义 是"每次编译删掉 PROF 下全部 *.prof",与"保留历次"直接冲突。 - worker_prof_*.prof 落 `PROF/<启动时间>/`。 - main_prof_{pid}.prof 与 main_time.txt 同样落该目录(Projectrans.py)。 - Config.main 启动时调用 StartProfRun 并打印本次输出目录。 子目录名经环境变量传子进程:ProcessPoolExecutor 用 spawn 起新解释器,进程内 模块态不共享,环境变量随进程继承 ⇒ 同一次编译的所有 worker 落在同一目录, 不会各建各的。 --- Projectrans.py | 13 +++--- lib/Projectrans/Config.py | 8 ++-- lib/Projectrans/Phase2Translator.py | 71 ++++++++++++++++++++++------- 3 files changed, 66 insertions(+), 26 deletions(-) diff --git a/Projectrans.py b/Projectrans.py index e9f1806..30ae344 100644 --- a/Projectrans.py +++ b/Projectrans.py @@ -22,8 +22,8 @@ __all__ = [ if __name__ == '__main__': # 主进程性能剖析:整次编译过程(Phase1 声明生成 + Phase2 翻译/编译/链接) - # 自动导出 cProfile dump 到 ./PROF/main_prof_{pid}.prof(与 worker 子进程 - # worker_prof_*.prof 同目录,可用 PROF/1.py 合并分析)。主进程 profile 主要 + # 自动导出 cProfile dump 到 ./PROF/<启动时间>/main_prof_{pid}.prof(与 worker + # 子进程 worker_prof_*.prof 同目录)。主进程 profile 主要 # 覆盖 Phase1/依赖扫描/编译调度/链接等主进程侧耗时;worker 内部 CPU 分布 # 仍由 _ProfileWorker 的 worker_prof_*.prof 覆盖(多进程下主进程 cProfile # 看不到子进程内部热点,两者互补)。 @@ -31,7 +31,7 @@ if __name__ == '__main__': import os as _os import sys as _sys import time as _time - from lib.Projectrans.Phase2Translator import PROF_DIR as _PROF_DIR + from lib.Projectrans.Phase2Translator import GetProfRunDir as _GetProfRunDir _main_prof: _cProfile.Profile = _cProfile.Profile() _main_prof.enable() @@ -48,9 +48,10 @@ if __name__ == '__main__': _timing_msg: str = f'[TIMING] 编译{_timing_status},总耗时 {_elapsed_s:.2f}s' print(_timing_msg, file=_sys.stderr) try: - _os.makedirs(_PROF_DIR, exist_ok=True) - _main_prof.dump_stats(_os.path.join(_PROF_DIR, f'main_prof_{_os.getpid()}.prof')) - with open(_os.path.join(_PROF_DIR, 'main_time.txt'), 'w', encoding='utf-8') as _tf: + _prof_dir: str = _GetProfRunDir() + _os.makedirs(_prof_dir, exist_ok=True) + _main_prof.dump_stats(_os.path.join(_prof_dir, f'main_prof_{_os.getpid()}.prof')) + with open(_os.path.join(_prof_dir, 'main_time.txt'), 'w', encoding='utf-8') as _tf: _tf.write(f'{_timing_status}\t{_elapsed_s:.2f}\n') except Exception: pass diff --git a/lib/Projectrans/Config.py b/lib/Projectrans/Config.py index d453349..d0fd48f 100644 --- a/lib/Projectrans/Config.py +++ b/lib/Projectrans/Config.py @@ -248,10 +248,10 @@ def main() -> None: if hotspot_sites_file: _vlog().info(f"调用点热点统计: {hotspot_sites_file}(按真实调用次数降序落盘,含 main 的模块启动时清空一次);每函数计时: {os.path.join(os.path.dirname(hotspot_sites_file), 'hotspot_time.txt')}") - # 每次编译自动清理子进程性能剖析输出(PROF 目录 *.prof,硬编码路径) - from lib.Projectrans.Phase2Translator import _CleanProfFiles, PROF_DIR - _CleanProfFiles() - _vlog().info(f"PROF 目录已清理: {PROF_DIR}") + # 每次编译按启动时间新建独立的 PROF 子目录(保留旧目录,不清理;见 + # Phase2Translator.StartProfRun 的说明)。worker 子进程经环境变量复用同一目录。 + from lib.Projectrans.Phase2Translator import StartProfRun + _vlog().info(f"PROF 本次输出目录: {StartProfRun()}") if args.clean: if output_dir and os.path.exists(output_dir): diff --git a/lib/Projectrans/Phase2Translator.py b/lib/Projectrans/Phase2Translator.py index 427e870..7127580 100644 --- a/lib/Projectrans/Phase2Translator.py +++ b/lib/Projectrans/Phase2Translator.py @@ -588,27 +588,65 @@ def _parallel_translate_batch(batch: list, src_root: str, temp_dir: str, output_ # 子进程性能剖析:主进程 cProfile 只能看到进程池等待(WaitForMultipleObjects/ # as_completed),看不到 worker 进程内部 CPU 分布(vast parse 等热点)。 # 固定启用(不设环境变量开关),worker 每个任务导出 cProfile dump -# (worker_prof_{pid}_{seq}.prof)到 PROF_DIR(D:\Users\TermiNexus\Desktop\TransPyC\PROF), -# 可用 pstats 合并分析。每次编译开始时由 _CleanProfFiles 清理该目录 *.prof。 +# (worker_prof_{pid}_{seq}.prof),主进程导出 main_prof_{pid}.prof + main_time.txt。 +# 输出落在 PROF_DIR/<启动时间>/ 下的**本次编译专属目录**(2026-09-29 起)。 # ================================================================== import functools as _functools +import time as _time -# 硬编码 PROF 输出目录(2026-08-18 起不再用 TPC_WORKER_PROF 环境变量控制) +# 硬编码 PROF 根目录(2026-08-18 起不再用 TPC_WORKER_PROF 环境变量控制) PROF_DIR: str = r'D:\Users\TermiNexus\Desktop\TransPyC\PROF' +# 本次编译的 run 目录:按启动时间在 PROF_DIR 下新建子目录,**不清理旧目录** +# (2026-09-29 用户要求保留历次 profile 以便跨次对比,旧行为是每次编译删掉 +# PROF 根目录下全部 *.prof ⇒ 只能看最近一次)。 +# 子目录名经环境变量传给 worker:ProcessPoolExecutor 用 spawn 起新解释器, +# 进程内模块态不共享;环境变量随子进程继承 ⇒ 同一次编译的所有 worker 落在 +# 同一个 run 目录,不会各建各的目录。 +_PROF_RUN_ENV: str = 'TPC_PROF_RUN_DIR' +_prof_run_dir: str | None = None -def _CleanProfFiles() -> None: - """清理 PROF_DIR 下全部 *.prof(每次编译开始调用)。""" + +def StartProfRun() -> str: + """每次编译开始时调用一次:创建本次编译专属的 PROF 子目录,返回其路径。 + + 目录名 = PROF 根目录下 `YYYYmmdd_HHMMSS`(编译启动时刻);同名(同一秒内 + 二次启动)补 `_N` 序号。创建失败时回落到 PROF 根目录,保证不因 profiling + 影响编译本身。成功后把目录写进环境变量,供 worker 子进程复用。 + """ + global _prof_run_dir + if _prof_run_dir: + return _prof_run_dir + _name: str = _time.strftime('%Y%m%d_%H%M%S') + _dir: str = os.path.join(PROF_DIR, _name) + _seq: int = 1 + while os.path.isdir(_dir): + _seq += 1 + _dir = os.path.join(PROF_DIR, f'{_name}_{_seq}') try: - if os.path.isdir(PROF_DIR): - for _n in os.listdir(PROF_DIR): - if _n.endswith('.prof'): - try: - os.remove(os.path.join(PROF_DIR, _n)) - except OSError: - pass + os.makedirs(_dir, exist_ok=True) except OSError: - pass + return PROF_DIR + _prof_run_dir = _dir + os.environ[_PROF_RUN_ENV] = _dir + return _dir + + +def GetProfRunDir() -> str: + """当前进程写 profile 应落的目录。 + + 主进程 = StartProfRun 选定的 run 目录;worker = 继承来的环境变量指向的目录。 + 两者都取不到(未先调 StartProfRun,如 worker 被单独拉起)时回落到 PROF 根目录, + 宁可文件落在根目录也不丢 dump。 + """ + global _prof_run_dir + if _prof_run_dir: + return _prof_run_dir + _env_dir: str = os.environ.get(_PROF_RUN_ENV, '') + if _env_dir: + _prof_run_dir = _env_dir + return _env_dir + return PROF_DIR def _ProfileWorker(_fn): @@ -635,9 +673,10 @@ def _ProfileWorker(_fn): _seq: int = getattr(_wrapper, '_seq', 0) + 1 _wrapper._seq = _seq try: - if not os.path.isdir(PROF_DIR): - os.makedirs(PROF_DIR, exist_ok=True) - _out: str = os.path.join(PROF_DIR, f'worker_prof_{os.getpid()}_{_seq}.prof') + _out_dir: str = GetProfRunDir() + if not os.path.isdir(_out_dir): + os.makedirs(_out_dir, exist_ok=True) + _out: str = os.path.join(_out_dir, f'worker_prof_{os.getpid()}_{_seq}.prof') _prof.dump_stats(_out) except Exception: pass