274 lines
9.9 KiB
Python
274 lines
9.9 KiB
Python
|
|
"""
|
|||
|
|
Trace 文件加载节点
|
|||
|
|
用于从 Unreal Insights 导出和加载 Trace 数据
|
|||
|
|
"""
|
|||
|
|
import polars as pl
|
|||
|
|
from server.app.core.user_manager import CLOUD_ROOT
|
|||
|
|
import server.app.utils.io as io
|
|||
|
|
import subprocess
|
|||
|
|
from pathlib import Path
|
|||
|
|
from typing import Any, Dict, Optional
|
|||
|
|
from server.app.core.node_base import (
|
|||
|
|
TraceNode,
|
|||
|
|
input_port,
|
|||
|
|
output_port,
|
|||
|
|
param,
|
|||
|
|
context_var,
|
|||
|
|
NodeType,
|
|||
|
|
CachePolicy
|
|||
|
|
)
|
|||
|
|
from server.app.core.node_registry import register_node
|
|||
|
|
import yaml
|
|||
|
|
|
|||
|
|
|
|||
|
|
def load_system_config():
|
|||
|
|
"""加载系统配置"""
|
|||
|
|
config_path = Path(__file__).parent.parent.parent / "system_config.yaml"
|
|||
|
|
if config_path.exists():
|
|||
|
|
with open(config_path, 'r', encoding='utf-8') as f:
|
|||
|
|
return yaml.safe_load(f)
|
|||
|
|
return {}
|
|||
|
|
|
|||
|
|
|
|||
|
|
@register_node
|
|||
|
|
class TraceLoader_Metadata(TraceNode):
|
|||
|
|
"""
|
|||
|
|
Trace 元数据加载器
|
|||
|
|
|
|||
|
|
功能:
|
|||
|
|
1. 调用 UnrealInsights.exe 打开 .utrace 文件
|
|||
|
|
2. 执行 ExportMetadata 命令导出 CSV
|
|||
|
|
3. 读取 CSV 文件并加载到内存
|
|||
|
|
4. 返回 DataFrame 格式的数据表
|
|||
|
|
|
|||
|
|
使用场景:
|
|||
|
|
- 分析 Trace 文件的元数据信息
|
|||
|
|
- 作为数据处理工作流的起点
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
CATEGORY = "Loader"
|
|||
|
|
DISPLAY_NAME = "Trace 元数据加载器"
|
|||
|
|
DESCRIPTION = "从 .utrace 文件导出并加载元数据"
|
|||
|
|
ICON = "📥"
|
|||
|
|
NODE_TYPE = NodeType.INPUT # 输入节点(无输入端口)
|
|||
|
|
CACHE_POLICY = CachePolicy.DISK # 使用磁盘缓存(元数据较大)
|
|||
|
|
|
|||
|
|
@output_port("metadata", "DataTable", description="导出的元数据表")
|
|||
|
|
@param("utrace_file", "String", default="", description="Utrace 文件路径", widget="file", required=True)
|
|||
|
|
@param("tokens", "String", description="Token事件列表,以 | 进行分割")
|
|||
|
|
def process(self, inputs: Dict[str, Any], context: Optional[Dict] = None) -> Dict[str, Any]:
|
|||
|
|
# 获取参数
|
|||
|
|
tokens = self.get_param("tokens", "")
|
|||
|
|
utrace_file = self.get_param("utrace_file", "")
|
|||
|
|
output_csv = self.get_param("output_csv", "temp/metadata.csv")
|
|||
|
|
|
|||
|
|
print("TraceLoader_Metadata: process called with utrace_file =", utrace_file, ", tokens =", tokens)
|
|||
|
|
if not utrace_file:
|
|||
|
|
raise ValueError("必须指定 utrace_file 参数")
|
|||
|
|
|
|||
|
|
# 加载系统配置
|
|||
|
|
config = load_system_config()
|
|||
|
|
insights_path = config.get("unreal", {}).get("insights_path", "")
|
|||
|
|
|
|||
|
|
# 构建完整路径
|
|||
|
|
# utrace 文件路径(用户目录下)
|
|||
|
|
if not insights_path or not Path(insights_path).exists():
|
|||
|
|
raise FileNotFoundError(
|
|||
|
|
f"UnrealInsights.exe 未找到:{insights_path}\n"
|
|||
|
|
f"请在 system_config.yaml 中配置正确的 insights_path"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
utrace_full_path = CLOUD_ROOT / utrace_file
|
|||
|
|
|
|||
|
|
if not utrace_full_path.exists():
|
|||
|
|
raise FileNotFoundError(f"Utrace 文件不存在:{utrace_full_path}")
|
|||
|
|
|
|||
|
|
# 构建输出 CSV 完整路径
|
|||
|
|
csv_full_path = CLOUD_ROOT / output_csv
|
|||
|
|
|
|||
|
|
# 确保输出目录存在
|
|||
|
|
csv_full_path.parent.mkdir(parents=True, exist_ok=True)
|
|||
|
|
if csv_full_path.exists():
|
|||
|
|
df = pl.read_csv(csv_full_path)
|
|||
|
|
return {
|
|||
|
|
"metadata": df,
|
|||
|
|
"file_path": str(csv_full_path)
|
|||
|
|
}
|
|||
|
|
io.safe_delete(csv_full_path)
|
|||
|
|
# 构建命令
|
|||
|
|
# UnrealInsights.exe -OpenTraceFile="path/to/file.utrace" -ExecOnAnalysisCompleteCmd="TimingInsights.ExportMetadata path/to/output.csv"
|
|||
|
|
cmd = [
|
|||
|
|
str(insights_path),
|
|||
|
|
f'-OpenTraceFile="{utrace_full_path}"',
|
|||
|
|
f'-ExecOnAnalysisCompleteCmd="TimingInsights.ExportMetadata {csv_full_path} -tokens={tokens}"'
|
|||
|
|
]
|
|||
|
|
print("TraceLoader_Metadata: 2.Execute::", " ".join(cmd))
|
|||
|
|
# 执行命令
|
|||
|
|
result = subprocess.run(
|
|||
|
|
' '.join(cmd),
|
|||
|
|
shell=True,
|
|||
|
|
capture_output=True,
|
|||
|
|
text=True,
|
|||
|
|
encoding='utf-8',
|
|||
|
|
errors='replace'
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# 检查执行结果
|
|||
|
|
if result.returncode != 0:
|
|||
|
|
error_msg = f"UnrealInsights 执行失败 (退出码: {result.returncode})\n"
|
|||
|
|
if result.stderr:
|
|||
|
|
error_msg += f"错误输出:\n{result.stderr}"
|
|||
|
|
raise RuntimeError(error_msg)
|
|||
|
|
|
|||
|
|
if not csv_full_path.exists():
|
|||
|
|
raise FileNotFoundError(
|
|||
|
|
f"CSV 文件未生成:{csv_full_path}\n"
|
|||
|
|
f"命令输出: {result.stdout}"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# 读取 CSV 文件
|
|||
|
|
df = pl.read_csv(csv_full_path)
|
|||
|
|
|
|||
|
|
# 准备输出
|
|||
|
|
outputs = {
|
|||
|
|
"metadata": df,
|
|||
|
|
"file_path": str(csv_full_path)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#io.safe_delete(csv_full_path)
|
|||
|
|
return outputs
|
|||
|
|
|
|||
|
|
|
|||
|
|
@register_node
|
|||
|
|
class TraceLoader_Events(TraceNode):
|
|||
|
|
"""
|
|||
|
|
Trace 事件加载器
|
|||
|
|
|
|||
|
|
功能:
|
|||
|
|
导出 Trace 文件中的事件数据(Timing Events)
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
CATEGORY = "Loader"
|
|||
|
|
DISPLAY_NAME = "Trace 事件加载器"
|
|||
|
|
DESCRIPTION = "从 .utrace 文件导出并加载事件数据"
|
|||
|
|
ICON = "⏱️"
|
|||
|
|
NODE_TYPE = NodeType.INPUT
|
|||
|
|
CACHE_POLICY = CachePolicy.DISK
|
|||
|
|
|
|||
|
|
@output_port("events", "DataTable", description="导出的事件数据表")
|
|||
|
|
@output_port("file_path", "String", description="导出的 CSV 文件路径")
|
|||
|
|
@param("utrace_file", "String", default="", description="Utrace 文件路径", widget="file", required=True)
|
|||
|
|
@param("output_csv", "String", default="temp/events.csv", description="输出 CSV 路径", required=True)
|
|||
|
|
@param("timeout", "Number", default=120, description="命令执行超时时间(秒)", min=10, max=600, step=10)
|
|||
|
|
@param("auto_cleanup", "Boolean", default=False, description="执行后自动删除 CSV 文件")
|
|||
|
|
@context_var("row_count", "Integer", description="事件数量")
|
|||
|
|
@context_var("column_count", "Integer", description="列数")
|
|||
|
|
@context_var("execution_time", "Number", description="执行时间(秒)")
|
|||
|
|
def process(self, inputs: Dict[str, Any], context: Optional[Dict] = None) -> Dict[str, Any]:
|
|||
|
|
import time
|
|||
|
|
start_time = time.time()
|
|||
|
|
|
|||
|
|
# 获取参数
|
|||
|
|
utrace_file = self.get_param("utrace_file", "")
|
|||
|
|
output_csv = self.get_param("output_csv", "temp/events.csv")
|
|||
|
|
timeout = self.get_param("timeout", 120)
|
|||
|
|
auto_cleanup = self.get_param("auto_cleanup", False)
|
|||
|
|
|
|||
|
|
if not utrace_file:
|
|||
|
|
raise ValueError("必须指定 utrace_file 参数")
|
|||
|
|
|
|||
|
|
# 加载系统配置
|
|||
|
|
config = load_system_config()
|
|||
|
|
insights_path = config.get("unreal", {}).get("insights_path", "")
|
|||
|
|
cloud_root = Path(config.get("storage", {}).get("cloud_root", "./cloud"))
|
|||
|
|
|
|||
|
|
user_id = (context or {}).get("user_id", "guest")
|
|||
|
|
|
|||
|
|
# 构建路径
|
|||
|
|
if not insights_path or not Path(insights_path).exists():
|
|||
|
|
raise FileNotFoundError(f"UnrealInsights.exe 未找到:{insights_path}")
|
|||
|
|
|
|||
|
|
if Path(utrace_file).is_absolute():
|
|||
|
|
utrace_full_path = Path(utrace_file)
|
|||
|
|
else:
|
|||
|
|
utrace_full_path = cloud_root / "users" / user_id / utrace_file
|
|||
|
|
|
|||
|
|
if not utrace_full_path.exists():
|
|||
|
|
raise FileNotFoundError(f"Utrace 文件不存在:{utrace_full_path}")
|
|||
|
|
|
|||
|
|
if Path(output_csv).is_absolute():
|
|||
|
|
csv_full_path = Path(output_csv)
|
|||
|
|
else:
|
|||
|
|
csv_full_path = cloud_root / output_csv
|
|||
|
|
|
|||
|
|
csv_full_path.parent.mkdir(parents=True, exist_ok=True)
|
|||
|
|
|
|||
|
|
# 构建命令(导出事件数据)
|
|||
|
|
cmd = [
|
|||
|
|
str(insights_path),
|
|||
|
|
f'-OpenTraceFile="{utrace_full_path}"',
|
|||
|
|
f'-ExecOnAnalysisCompleteCmd="TimingInsights.ExportTimingEvents {csv_full_path}"'
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
result = subprocess.run(
|
|||
|
|
' '.join(cmd),
|
|||
|
|
shell=True,
|
|||
|
|
capture_output=True,
|
|||
|
|
text=True,
|
|||
|
|
timeout=timeout,
|
|||
|
|
encoding='utf-8',
|
|||
|
|
errors='replace'
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
if result.returncode != 0:
|
|||
|
|
error_msg = f"UnrealInsights 执行失败 (退出码: {result.returncode})\n"
|
|||
|
|
if result.stderr:
|
|||
|
|
error_msg += f"错误输出:\n{result.stderr}"
|
|||
|
|
raise RuntimeError(error_msg)
|
|||
|
|
|
|||
|
|
# 等待文件生成
|
|||
|
|
wait_time = 0
|
|||
|
|
while not csv_full_path.exists() and wait_time < 5:
|
|||
|
|
time.sleep(0.5)
|
|||
|
|
wait_time += 0.5
|
|||
|
|
|
|||
|
|
if not csv_full_path.exists():
|
|||
|
|
raise FileNotFoundError(f"CSV 文件未生成:{csv_full_path}")
|
|||
|
|
|
|||
|
|
# 读取 CSV(使用 polars,保持后端一致)
|
|||
|
|
df = pl.read_csv(csv_full_path)
|
|||
|
|
execution_time = time.time() - start_time
|
|||
|
|
|
|||
|
|
outputs = {
|
|||
|
|
"events": df,
|
|||
|
|
"file_path": str(csv_full_path)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
result_context = {
|
|||
|
|
"row_count": len(df),
|
|||
|
|
"column_count": len(df.columns),
|
|||
|
|
"execution_time": round(execution_time, 2)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if auto_cleanup:
|
|||
|
|
try:
|
|||
|
|
csv_full_path.unlink()
|
|||
|
|
result_context["cleaned_up"] = True
|
|||
|
|
except Exception as e:
|
|||
|
|
result_context["cleanup_error"] = str(e)
|
|||
|
|
|
|||
|
|
out = outputs
|
|||
|
|
# 可选地将 context 信息并入 outputs.meta 或单独管理;这里仅返回 outputs
|
|||
|
|
return out
|
|||
|
|
|
|||
|
|
except subprocess.TimeoutExpired:
|
|||
|
|
raise TimeoutError(f"命令执行超时(超过 {timeout} 秒)")
|
|||
|
|
except Exception as e:
|
|||
|
|
if csv_full_path.exists():
|
|||
|
|
try:
|
|||
|
|
csv_full_path.unlink()
|
|||
|
|
except:
|
|||
|
|
pass
|
|||
|
|
raise
|