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