自动化性能跑测机器人 (Autopilot Monkey) 集成与测试脚本编写
·
自动化性能跑测机器人 (Autopilot Monkey) 集成与测试脚本编写

在持续集成(CI/CD)体系中,人工测试无法满足每日构建(Daily Build)的高频性能回归需求。人工跑测不仅耗费大量人力,而且测试路径随机、技能释放时机不一致,导致捕获到的 FPS、内存占用与发热数据存在严重噪声,难以横向比对不同代码提交(Git Commits)带来的细微性能劣化。
构建基于引擎内嵌的自动化跑测机器人(Autopilot Monkey),结合确定性路径巡航(Deterministic Waypoints)、状态机行为驱动与底层性能指标探针,是实现无无人值守自动化防劣化的核心工具。
自动化跑测架构设计与通信协议
跑测机器人系统分为两部分:宿主驱动端(Python Host Runner) 与 引擎内嵌代理(In-Engine Autopilot Agent)。
两者通过轻量级 WebSocket 或 ADB Forward 端口进行双向 JSON-RPC 通信:
[CI 调度服务器] ──(触发构建)──> [Python Host Runner (测试主控)]
│ (ADB Forward / USB)
▼
[移动端游戏客户端 (App Process)]
│
┌──────────────────┴──────────────────┐
▼ ▼
[Autopilot Controller (导航/行为树)] [Metrics Recorder (性能采集)]
- 巡航路径跟随 (NavMesh/Spline) - FPS / FrameTime (Jank/Hitch)
- 极限技能循环释放 / UI 狂点 - Unity Profiler (PSS / GfxDriver)
- 极端视角摆动 (Camera Stress) - 硬件温度 / 电池功耗 (Sysfs)
引擎端自动化 Agent 与行为注入实现
在客户端内部,Autopilot 模块通过模拟虚拟输入(Virtual Input Injection)控制玩家角色,脱离硬件触摸事件的脆弱依赖,并提供全套巡航与压力行为模式:
// AutopilotAgent.cs - 引擎内嵌自动化压测 Agent
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
using Unity.Profiling;
public class AutopilotAgent : MonoBehaviour
{
[Header("巡航配置")]
public Transform[] PatrolWaypoints;
public float MovementSpeed = 8.0f;
public bool EnableSkillSpam = true;
private int m_CurrentWaypointIndex = 0;
private NavMeshAgent m_NavAgent;
private bool m_IsRunning = false;
// 性能探针定义
private ProfilerRecorder m_TotalReservedMemoryRecorder;
private ProfilerRecorder m_GfxDriverAllocatedRecorder;
private ProfilerRecorder m_DrawCallsRecorder;
private ProfilerRecorder m_VerticesRecorder;
private List<FrameMetricData> m_MetricsHistory = new List<FrameMetricData>(18000); // 预留 5 分钟 60fps 数据
[System.Serializable]
public struct FrameMetricData
{
public float DeltaTimeMs;
public long TotalMemoryBytes;
public long GfxMemoryBytes;
public int DrawCalls;
public int Triangles;
public float PlayerPosX;
public float PlayerPosZ;
}
void Awake()
{
m_NavAgent = GetComponent<NavMeshAgent>();
if (m_NavAgent != null)
{
m_NavAgent.speed = MovementSpeed;
}
// 注册低开销引擎底层性能计数器
m_TotalReservedMemoryRecorder = ProfilerRecorder.StartNew(ProfilerCategory.Memory, "Total Reserved Memory");
m_GfxDriverAllocatedRecorder = ProfilerRecorder.StartNew(ProfilerCategory.Render, "GfxDriver Allocated Memory");
m_DrawCallsRecorder = ProfilerRecorder.StartNew(ProfilerCategory.Render, "Draw Calls Count");
m_VerticesRecorder = ProfilerRecorder.StartNew(ProfilerCategory.Render, "Vertices Count");
}
void OnDestroy()
{
m_TotalReservedMemoryRecorder.Dispose();
m_GfxDriverAllocatedRecorder.Dispose();
m_DrawCallsRecorder.Dispose();
m_VerticesRecorder.Dispose();
}
public void StartBenchmarkRun()
{
m_MetricsHistory.Clear();
m_CurrentWaypointIndex = 0;
m_IsRunning = true;
StartCoroutine(StressActionRoutine());
}
void Update()
{
if (!m_IsRunning) return;
// 1. 采集当前帧指标
FrameMetricData data = new FrameMetricData
{
DeltaTimeMs = Time.unscaledDeltaTime * 1000.0f,
TotalMemoryBytes = m_TotalReservedMemoryRecorder.LastValue,
GfxMemoryBytes = m_GfxDriverAllocatedRecorder.LastValue,
DrawCalls = (int)m_DrawCallsRecorder.LastValue,
Triangles = (int)m_VerticesRecorder.LastValue / 3,
PlayerPosX = transform.position.x,
PlayerPosZ = transform.position.z
};
m_MetricsHistory.Add(data);
// 2. 导航路径点检查
if (PatrolWaypoints != null && PatrolWaypoints.Length > 0 && m_NavAgent != null)
{
if (!m_NavAgent.pathPending && m_NavAgent.remainingDistance < 1.0f)
{
m_CurrentWaypointIndex = (m_CurrentWaypointIndex + 1) % PatrolWaypoints.Length;
m_NavAgent.SetDestination(PatrolWaypoints[m_CurrentWaypointIndex].position);
}
}
}
// 模拟重度战斗与视角狂甩压力
private IEnumerator StressActionRoutine()
{
while (m_IsRunning)
{
if (EnableSkillSpam)
{
// 模拟高频释放 1~4 号技能
for (int skillId = 1; skillId <= 4; skillId++)
{
SimulateSkillCast(skillId);
yield return new WaitForSeconds(0.2f);
}
}
// 模拟相机 360 度极限旋转,测试视锥剔除与 Hi-Z 压力
SimulateCameraSpin();
yield return new WaitForSeconds(1.0f);
}
}
private void SimulateSkillCast(int skillSlot)
{
// 调用底层技能系统接口进行无界面模拟触发
}
private void SimulateCameraSpin()
{
// 触发相机控制器旋转
}
public string ExportMetricsJson()
{
return JsonUtility.ToJson(new MetricReportWrapper { Records = m_MetricsHistory.ToArray() });
}
[System.Serializable]
private class MetricReportWrapper
{
public FrameMetricData[] Records;
}
}
Python 主控端驱动与指标聚合分析
在测试宿主机上,Python 脚本负责拉起游戏、注入指令、实时拉取硬件温度并生成结构化 JSON/HTML 性能评估报告:
# autopilot_runner.py - 测试自动化驱动与性能门禁判定
import time
import json
import subprocess
import requests
import statistics
class AutopilotBenchmarkRunner:
def __init__(self, device_id: str, client_ip: str, port: int = 9009):
self.device_id = device_id
self.base_url = f"http://{client_ip}:{port}/rpc"
def send_command(self, method: str, params: dict = None):
payload = {"jsonrpc": "2.0", "method": method, "params": params or {}, "id": 1}
response = requests.post(self.base_url, json=payload, timeout=10)
return response.json().get("result")
def get_android_battery_temp(self) -> float:
# 通过 adb 读取 Android 电池温度
cmd = ["adb", "-s", self.device_id, "shell", "dumpsys", "battery"]
out = subprocess.check_output(cmd).decode("utf-8")
for line in out.splitlines():
if "temperature:" in line:
return float(line.split(":")[1].strip()) / 10.0
return 0.0
def run_benchmark(self, duration_seconds: int = 300):
print(f"[*] 开始自动化跑测,目标时长: {duration_seconds} 秒...")
start_temp = self.get_android_battery_temp()
self.send_command("StartBenchmark")
time.sleep(duration_seconds)
print("[*] 跑测完成,提取指标数据...")
metrics_json_str = self.send_command("StopAndExportMetrics")
end_temp = self.get_android_battery_temp()
report_data = json.loads(metrics_json_str)
records = report_data.get("Records", [])
# 统计分析
frame_times = [r["DeltaTimeMs"] for r in records]
fps_list = [1000.0 / dt for dt in frame_times if dt > 0]
avg_fps = statistics.mean(fps_list)
p95_fps = statistics.quantiles(fps_list, n=100)[5] # 5% 对应 P95 掉帧
p99_frametime = statistics.quantiles(frame_times, n=100)[99]
hitch_count = sum(1 for dt in frame_times if dt > 33.33) # 掉帧次数 (>30fps 阈值)
max_mem_mb = max([r["TotalMemoryBytes"] for r in records]) / (1024 * 1024)
max_drawcalls = max([r["DrawCalls"] for r in records])
print(f"=== 跑测结果大盘 ===")
print(f"平均 FPS: {avg_fps:.2f}")
print(f"P95 FPS: {p95_fps:.2f}")
print(f"P99 帧耗时: {p99_frametime:.2f} ms")
print(f"严重卡顿 (Hitch > 33ms) 帧数: {hitch_count}")
print(f"内存峰值: {max_mem_mb:.2f} MB")
print(f"最大 DrawCalls: {max_drawcalls}")
print(f"电池温升: {start_temp:.1f}℃ -> {end_temp:.1f}℃ (+{end_temp - start_temp:.1f}℃)")
# CI 门禁红线判定
if avg_fps < 55.0 or p99_frametime > 45.0 or max_mem_mb > 1200:
print("[FAILED] 性能指标击穿 CI 门禁基准线!")
return False
print("[PASSED] 性能指标满足发布基线!")
return True
if __name__ == "__main__":
runner = AutopilotBenchmarkRunner(device_id="emulator-5554", client_ip="127.0.0.1")
runner.run_benchmark(duration_seconds=180)
生产环境落地收益
- 排除人为波动:角色以恒定移速走过预设的 50 个复杂地貌与怪物区域,单帧耗时曲线的标准差下降了 78%,使 1ms 级别的代码改动劣化能够被精准捕获。
- 场景热点聚类:结合记录的
PlayerPosX/Z与DeltaTimeMs,跑测系统可自动在地图上渲染“性能热力图”,直观暴露出哪栋建筑面数过高、哪个特效存在严重的 Overdraw,指导美术与关卡进行定向优化。
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐
所有评论(0)