自动化性能跑测机器人 (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)

生产环境落地收益

  1. 排除人为波动:角色以恒定移速走过预设的 50 个复杂地貌与怪物区域,单帧耗时曲线的标准差下降了 78%,使 1ms 级别的代码改动劣化能够被精准捕获。
  2. 场景热点聚类:结合记录的 PlayerPosX/Z 与 DeltaTimeMs,跑测系统可自动在地图上渲染“性能热力图”,直观暴露出哪栋建筑面数过高、哪个特效存在严重的 Overdraw,指导美术与关卡进行定向优化。
Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐