Files
TransPyC/lib/Projectrans/Config.py
T
TermiNexus 84d91d53e5 perf(hotspot): 计时串行化并改为自身耗时(exclusive)统计
--hotspot-sites 的每函数计时原为裸 rdtsc 的 inclusive 耗时,导致 _parse_* 一条调用链各项数值几乎相同、无法定位自身成本,且短函数测量不可信。

新增 _EmitReadTimestamp:x86_64 用 lfence 加 readcyclecounter 串行化读(业界通行的 lfence;rdtsc),非 x86_64 编译期降级为裸 readcyclecounter;入口与每个 ret 前共用。选 intrinsic 组合而非内联汇编,避免约束串转义与渲染风险。

用深度栈实现自身耗时:全局 __fhs_tdepth 与 [512 x i64] __fhs_tstart/__fhs_tchild,self = inc 减去本帧内被插桩子调用 inc 之和;递归靠每调用独立 slot 与深度索引天然正确;深度溢出或终结指令非 ret 的块明确跳过不记账(仅计进入次数),不伪造数值。入口段用 select 而非条件分支,避免改动 CFG。

hotspot_time.txt 行为 函数名 self inc 进入次数(仍按 self 降序),段头注明单位 tick 与 self 口径;hotspot_sites.txt 格式保持不变;帮助文本同步更新。未插桩的被调方(含 libc)耗时计入调用者 self,属该方法固有性质并已注明。
2026-09-14 13:42:14 +08:00

353 lines
18 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from __future__ import annotations
import os
import sys
import json
import glob as glob_module
import argparse
import shutil
import subprocess
from typing import Any
from lib.core.VLogger import get_logger as _vlog
from lib.Projectrans.Phase1Generator import Phase1Generator
from lib.Projectrans.Phase2Translator import Phase2Translator
# ============================================================
# 全局 SHA1 映射内存存储器
#
# Phase1 生成 sha1_map 后写入此处(save_sha1_map),
# Phase2 从此处读取(Load_sha1_map / Phase2Translator._LoadSha1Map)。
# _sha1_map.txt 文件仅作为人类可读输出,不再作为机器分析数据源。
# ============================================================
_g_sha1_map_store: dict[str, str] = {}
def get_sha1_map_store() -> dict[str, str]:
"""返回全局 SHA1 映射内存存储器(供 Phase2Translator 直接访问)"""
return _g_sha1_map_store
def clear_sha1_map_store() -> None:
"""清空全局 SHA1 映射内存存储器"""
_g_sha1_map_store.clear()
def save_sha1_map(temp_dir: str, sha1_map: dict[str, str]) -> None:
"""将 SHA1 映射表保存到内存存储器,并输出人类可读的 _sha1_map.txt
机器分析使用内存存储器(_g_sha1_map_store),
_sha1_map.txt 仅作为人类可读输出,不再被机器读取。
"""
# 写入内存存储器(机器分析数据源)
_g_sha1_map_store.clear()
for sha1, rel in sha1_map.items():
_g_sha1_map_store[sha1] = rel
# 写入文件(仅人类可读输出)
map_path: str = os.path.join(temp_dir, '_sha1_map.txt')
with open(map_path, 'w', encoding='utf-8') as f:
for sha1, rel in sorted(sha1_map.items()):
f.write(f"{sha1}:{rel}\n")
def Load_sha1_map(temp_dir: str) -> dict[str, str]:
"""从内存存储器加载 SHA1 映射表
temp_dir 参数保留用于签名兼容性,但不再读取文件。
机器分析使用内存存储器(_g_sha1_map_store)。
"""
result: dict[str, str] = {}
for sha1, rel in _g_sha1_map_store.items():
result[sha1] = rel
return result
def Load_project_config(project_file: str) -> dict[str, Any]:
"""从 project.json 加载配置"""
with open(project_file, 'r', encoding='utf-8') as f:
return json.load(f)
def resolve_paths(config: dict[str, Any], project_file: str) -> dict[str, Any]:
"""将配置中的相对路径转换为绝对路径"""
project_dir: str = os.path.dirname(os.path.abspath(project_file))
result: dict[str, Any] = dict(config)
def resolve(p: str | None) -> str | None:
if p is None:
return None
if os.path.isabs(p):
return p
return os.path.normpath(os.path.join(project_dir, p))
if 'source_dir' in result:
result['source_dir'] = resolve(result.get('source_dir'))
elif 'sources' in result:
resolved_sources: list[str] = []
for src in result['sources']:
if '*' in src or '?' in src:
for matched in glob_module.glob(os.path.join(project_dir, src), recursive=True):
if os.path.isfile(matched):
resolved_sources.append(matched)
else:
resolved_sources.append(resolve(src))
result['sources'] = [s for s in resolved_sources if os.path.isfile(s)]
# 直接使用 output_dir / temp_dir(与新版本 TPV 一致,不再支持 build_dir)
result['temp_dir'] = resolve(config.get('temp_dir', './temp'))
result['output_dir'] = resolve(config.get('output_dir', './output'))
if 'includes' in result:
result['includes'] = [resolve(inc) for inc in result['includes']]
result['_project_dir'] = project_dir
return result
def main() -> None:
parser: argparse.ArgumentParser = argparse.ArgumentParser(
description='TransPyC 工程翻译器 - 基于 project.json 的两阶段翻译',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog='''
示例:
# 从 project.json 读取配置并执行(默认查找当前目录)
python ProjectTrans.py
# 指定 project.json 文件
python ProjectTrans.py --project ./TestProject/project.json
# 使用命令行参数覆盖 project.json 配置
python ProjectTrans.py --src ./src --temp ./decl --output ./out --phase all
# 仅运行阶段一
python ProjectTrans.py --phase 1
# 仅运行阶段二
python ProjectTrans.py --phase 2
# 清理临时目录
python ProjectTrans.py --clean
# 编译并立即执行
python Projectrans.py --project ./TestProject/project.json --clean --run
# 删除 includes.binary 预编译缓存并重新编译所有 includes
python Projectrans.py --project ./TestProject/project.json --rebuild-includes
'''
)
parser.add_argument('--project', help='project.json 路径(默认查找当前目录)')
parser.add_argument('--src', help='源文件目录(覆盖 project.json)')
parser.add_argument('--temp', help='声明接口临时目录(覆盖 project.json)')
parser.add_argument('--output', help='输出目录(覆盖 project.json)')
parser.add_argument('--phase', choices=['1', '2', 'all'], help='阶段: 1=生成声明, 2=翻译+编译, all=全部')
parser.add_argument('--cc', help='LLVM 编译器命令(覆盖 project.json)')
parser.add_argument('--clean', action='store_true', help='清理 output 和 temp 目录')
parser.add_argument('--run', action='store_true', help='编译成功后立即执行生成的可执行文件')
parser.add_argument('--rebuild-includes', action='store_true', help='删除 includes.binary 预编译缓存并重新编译所有 includes')
parser.add_argument('--clear-cache', action='store_true', help='清除 .transpyc_cache 全局缓存(.vpi/.stub.ll/.doc.json)')
parser.add_argument('--asan', action='store_true', help='ASan 模式:对全部 .ll 做 opt -passes=asan 插桩,并显式链接 clang64 的 ASan 运行库(mingw64 的 clang 没有 x86_64-w64-windows-gnu 目标的 ASan runtime,不能用 -fsanitize=address)')
parser.add_argument('--nowarn-register', action='store_true', help='抑制"外部函数返回类型未登记"类 WARN 输出(透传给自举编译的 TransPyV.exe)')
parser.add_argument('--hotspot-sites', nargs='?', const='', default=None, help='调用点热点统计:为每个匹配白名单的直接 call 点插入计数器,运行时 Ctrl+C 各模块按真实次数降序写入 <path>(默认为 hotspot_sites.txt);同时为每个函数定义加计时器,按自身耗时(self)降序写入同目录 hotspot_time.txt,行为 `<函数名> <self> <inc> <进入次数>`(单位 tick,x86_64 上为序列化 lfence; rdtsc);self 已扣除被插桩子调用的 inc,但不含未插桩被调方(间接调用、strcmp/memcpy 等无定义外部函数),它们计入调用者的 self')
parser.add_argument('--hotspot-callees', default=None, help='--hotspot-sites 的白名单被调函数基名(逗号分隔),默认内存/字符串族 strcmp,strncmp,strchr,strlen,strstr,memcpy,memset,memmove,strcpy,strcat')
parser.add_argument('--hotspot-sites-all', action='store_true', help='--hotspot-sites 对全部直接 call 点插桩(忽略白名单;体积/开销大,用于深度排查)')
args: argparse.Namespace = parser.parse_args()
# ASan 模式(--asan):直接经 args 传给 Phase2Translator(compile_ll_to_obj
# 先 opt -passes=asan 插桩,链接显式 -l: clang64 的 libclang_rt.asan_dynamic*)。
# 不再用 TPC_ASAN 环境变量透传——环境变量在 worker 子进程/外部调用链下不可靠。
if args.asan:
_vlog().info('ASan 模式已启用(opt -passes=asan + clang64 libclang_rt.asan_dynamic*)', 'asan')
project_file: str | None = args.project
if not project_file:
candidates: list[str] = ['project.json', './project.json']
for c in candidates:
if os.path.exists(c):
project_file = c
break
if project_file and os.path.exists(project_file):
_vlog().info(f"读取配置文件: {project_file}")
config: dict[str, Any] = Load_project_config(project_file)
project_dir: str = os.path.dirname(os.path.abspath(project_file))
_vlog().info(f"项目目录: {project_dir}")
else:
_vlog().error("未找到 project.json,请使用 --project 指定")
sys.exit(1)
resolved: dict[str, Any] = resolve_paths(config, project_file)
src: str | list[str] | None = args.src or resolved.get('source_dir') or resolved.get('sources')
temp_dir: str | None = args.temp or resolved.get('temp_dir')
output_dir: str | None = args.output or resolved.get('output_dir')
phase: str = args.phase or 'all'
cc_cmd: str = args.cc or resolved.get('compiler', {}).get('cmd', 'llc')
cc_flags: list[str] = resolved.get('compiler', {}).get('flags', ['-filetype=obj'])
slice_level: int = resolved.get('options', {}).get('slice_level', 3)
project_include_dirs: list[str] = resolved.get('includes', [])
# includes 只认项目配置:同步到 Phase2Translator.PROJECT_INCLUDE_DIRS,
# 供 builtins.vp 扫描等静态路径在 phase1/2 全程使用(禁止仓库硬编码回退)
try:
from lib.Projectrans.Phase2Translator import Phase2Translator
Phase2Translator.PROJECT_INCLUDE_DIRS = [os.path.abspath(d) for d in project_include_dirs if os.path.isdir(os.path.join(project_dir, d)) or os.path.isdir(d)]
except Exception:
pass
target_triple: str | None = resolved.get('target', {}).get('triple')
target_datalayout: str | None = resolved.get('target', {}).get('datalayout')
# 调用点热点统计(--hotspot-sites):None=未启用;''=启用但未给路径(默认
# hotspot_sites.txt);其余为显式路径。一律绝对化,保证运行时(exe 工作目录为
# output_dir)各模块处理器追加写入的路径与预期一致。
hotspot_sites_file: str | None = args.hotspot_sites
if hotspot_sites_file is not None:
if hotspot_sites_file == '':
hotspot_sites_file = 'hotspot_sites.txt'
hotspot_sites_file = os.path.abspath(hotspot_sites_file)
# 白名单覆盖:逗号分隔 → 去空白后的基名列表(None=用 BaseGen 默认白名单)
hotspot_callees: list[str] | None = None
if args.hotspot_callees:
hotspot_callees = [c.strip() for c in args.hotspot_callees.split(',') if c.strip()]
if isinstance(src, list):
if len(src) == 1 and os.path.isdir(src[0]):
src = src[0]
else:
src = src[0] if len(src) == 1 else os.path.dirname(os.path.commonpath(src)) if all(os.path.exists(p) for p in src) else src[0]
_vlog().info(f"源目录: {src}")
_vlog().info(f"临时目录: {temp_dir}")
_vlog().info(f"输出目录: {output_dir}")
_vlog().info(f"阶段: {phase}")
_vlog().info(f"编译器: {cc_cmd} {' '.join(cc_flags)}")
if target_triple:
_vlog().info(f"目标三元组: {target_triple}")
if target_datalayout:
_vlog().info(f"数据布局: {target_datalayout}")
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}")
if args.clean:
if output_dir and os.path.exists(output_dir):
for f in os.listdir(output_dir):
fpath: str = os.path.join(output_dir, f)
try:
if os.path.isfile(fpath) or os.path.islink(fpath):
os.remove(fpath)
elif os.path.isdir(fpath):
shutil.rmtree(fpath, ignore_errors=True)
except:
pass
_vlog().info(f"output: 已清空 {output_dir}")
if temp_dir and os.path.exists(temp_dir):
for f in os.listdir(temp_dir):
fpath: str = os.path.join(temp_dir, f)
try:
if os.path.isfile(fpath) or os.path.islink(fpath):
os.remove(fpath)
elif os.path.isdir(fpath):
shutil.rmtree(fpath, ignore_errors=True)
except:
pass
_vlog().info(f"temp: 已清空 {temp_dir}")
_vlog().info("清理完成")
if (not phase or phase == 'all') and not args.project:
return
if args.clear_cache:
# 清除 .transpyc_cache 全局缓存
cache_dir: str = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), '.transpyc_cache')
if os.path.isdir(cache_dir):
shutil.rmtree(cache_dir, ignore_errors=True)
os.makedirs(cache_dir, exist_ok=True)
_vlog().info(f"已清除全局缓存: {cache_dir}")
else:
_vlog().info(f"全局缓存目录不存在: {cache_dir}")
if phase in ('1', 'all'):
if isinstance(src, list):
_vlog().error("阶段一需要指定源目录,不支持文件列表")
sys.exit(1)
all_include_dirs: list[str] = list(project_include_dirs) if project_include_dirs else []
gen: Phase1Generator = Phase1Generator(src, temp_dir, include_dirs=all_include_dirs, target_triple=target_triple, target_datalayout=target_datalayout)
gen.run()
save_sha1_map(temp_dir, gen.sha1_map)
if phase in ('2', 'all'):
if not output_dir:
_vlog().error("阶段二需要指定 --output 目录")
sys.exit(1)
if not os.path.exists(temp_dir):
_vlog().error(f"声明目录不存在,请先运行阶段一: {temp_dir}")
sys.exit(1)
if isinstance(src, list):
_vlog().error("阶段二需要指定源目录,不支持文件列表")
sys.exit(1)
linker_cmd: str | None = resolved.get('linker', {}).get('cmd')
linker_flags: list[str] = resolved.get('linker', {}).get('flags', [])
linker_output: str | None = resolved.get('linker', {}).get('output')
# 显式静态链接文件列表(.a/.obj 等),相对 project.vpj 目录解析为绝对路径
static_libs: list[str] = []
for lib in resolved.get('linker', {}).get('static_libs', []) or []:
static_libs.append(lib if os.path.isabs(lib) else os.path.normpath(os.path.join(project_dir, lib)))
# 显式动态链接文件列表(.dll/.so 等),相对 project.vpj 目录解析为绝对路径;
# 链接时作为输入文件(GNU ld 从 DLL/SO 提取导入符号),运行时需 DLL/SO 在搜索路径
dynamic_libs: list[str] = []
for lib in resolved.get('linker', {}).get('dynamic_libs', []) or []:
dynamic_libs.append(lib if os.path.isabs(lib) else os.path.normpath(os.path.join(project_dir, lib)))
target_triple = resolved.get('target', {}).get('triple')
target_datalayout = resolved.get('target', {}).get('datalayout')
force_recompile_includes: bool = bool(resolved.get('options', {}).get('force_recompile_includes', False))
if args.rebuild_includes:
force_recompile_includes = True
all_inc_dirs: list[str] = list(project_include_dirs)
seen_dirs: set[str] = set()
for inc_dir in all_inc_dirs:
abs_inc: str = os.path.abspath(inc_dir)
if abs_inc in seen_dirs:
continue
seen_dirs.add(abs_inc)
if not os.path.isdir(abs_inc):
continue
inc_parent: str = os.path.dirname(abs_inc)
inc_base: str = os.path.basename(abs_inc)
precompiled_dir: str = os.path.join(inc_parent, inc_base + '.binary')
if os.path.isdir(precompiled_dir):
cleared: int = 0
for fname in os.listdir(precompiled_dir):
fpath: str = os.path.join(precompiled_dir, fname)
try:
if os.path.isfile(fpath) or os.path.islink(fpath):
os.remove(fpath)
cleared += 1
except:
pass
_vlog().info(f"includes.binary: 已清空 {precompiled_dir} ({cleared} 个文件)")
trans: Phase2Translator = Phase2Translator(src, temp_dir, output_dir, compile_cmd=cc_cmd, compile_flags=cc_flags, linker_cmd=linker_cmd, linker_flags=linker_flags, linker_output=linker_output, static_libs=static_libs, dynamic_libs=dynamic_libs, include_dirs=project_include_dirs, target_triple=target_triple, target_datalayout=target_datalayout, force_recompile_includes=force_recompile_includes, asan_mode=args.asan)
trans.sha1_map = Load_sha1_map(temp_dir)
trans.slice_level = slice_level
# 调用点热点统计与每函数计时(--hotspot-sites):None=未启用(零影响)
trans.hotspot_sites_file = hotspot_sites_file
trans.hotspot_sites_all = bool(args.hotspot_sites_all)
trans.hotspot_callees = hotspot_callees
trans.run()
if args.run and linker_output and not linker_output.endswith('.bin'):
exe_path: str = os.path.join(output_dir, linker_output)
if os.path.isfile(exe_path):
_vlog().info(f"\n[运行] 执行: {exe_path}")
try:
result: subprocess.CompletedProcess = subprocess.run([exe_path], cwd=output_dir)
_vlog().info(f"[运行] 退出码: {result.returncode}")
except Exception as e:
_vlog().error(f"[运行] 执行失败: {e}")
sys.exit(1)
else:
_vlog().error(f"[运行] 找不到可执行文件: {exe_path}")
sys.exit(1)