移动游戏性能测试工具链搭建指南

公开说明:本文为方法论演示稿。项目、场景、设备、平台、路径、端口及性能数据均已重构或合成,不对应任何真实项目、客户或生产环境;代码仅用于说明思路。

脱敏说明:文中的产品、项目、设备、路径、域名与性能数据均已泛化处理,仅用于说明测试方法和分析思路。

概述

手游性能测试不是"开个工具跑一下"那么简单——它需要一套完整的工具链来覆盖采集、监控、分析、报告、回归检测等全链路环节。在示例 UE 项目中的长期测试实践中,测试流程逐步搭建了一套覆盖CPU、GPU、内存、帧率、功耗、温度六大维度的性能测试工具链,实现了从手动测试到自动化流水线的演进。

本文将这套工具链的搭建方法完整分享出来,包括每类工具的选型理由、配置方法、使用技巧和集成方案。目标读者是有一定基础但尚未体系化的性能测试工程师。


一、工具链全景图

┌─────────────────────────────────────────────────────┐
│                   性能测试工具链                       │
├──────────┬──────────┬──────────┬──────────┬──────────┤
│  帧率/CPU │   GPU    │   内存   │  功耗/温度 │  系统级   │
├──────────┼──────────┼──────────┼──────────┼──────────┤
│ UE4 Stat │ RenderDoc│memreport │ Battery  │ systrace │
│ 示例性能采集工具  │ AGI      │ LLM      │ Historian│ perfetto │
│ 示例性能监测工具│ 移动 GPU HWC │ MAT      │ PowerMon │ simpleperf│
│ dumpsys  │ Snapdragon│ dumpsys │ 温度枪   │ strace   │
│ gfxinfo  │  Profiler│ meminfo  │          │ ftrace   │
└──────────┴──────────┴──────────┴──────────┴──────────┘
         ↓              ↓           ↓           ↓
┌─────────────────────────────────────────────────────┐
│              数据汇聚层 (CSV / JSON / SQLite)          │
├─────────────────────────────────────────────────────┐
│              分析层 (Python / Jupyter)                 │
├─────────────────────────────────────────────────────┤
│              报告层 (Markdown / HTML / Grafana)       │
└─────────────────────────────────────────────────────┘

二、基础环境准备

2.1 开发机配置

# 推荐配置
# OS: Ubuntu 20.04/22.04 或 Windows + WSL2
# CPU: 8核心以上(数据分析需要)
# RAM: 16GB以上
# 存储: 500GB SSD(性能数据量大)

# 基础软件安装
sudo apt update
sudo apt install -y python3 python3-pip python3-venv adb \
    git curl wget unzip

# Python虚拟环境
python3 -m venv ~/perf_env
source ~/perf_env/bin/activate
pip install pandas numpy matplotlib seaborn plotly \
    scipy jupyter notebook openpyxl

# ADB配置
sudo apt install android-tools-adb
# 或从Android SDK获取最新版

2.2 测试设备准备

# 查看已连接设备
adb devices

# 设备基础信息采集
adb shell getprop ro.product.model        # 设备型号
adb shell getprop ro.build.version.sdk    # Android版本
adb shell cat /proc/cpuinfo               # CPU信息
adb shell cat /proc/meminfo | head -5     # 内存信息
adb shell getprop ro.hardware.chipname    # 芯片型号

# 开发者选项配置
adb shell settings put global development_settings_enabled 1
adb shell settings put system screen_brightness_mode 0  # 关闭自动亮度
adb shell settings put system screen_brightness 128      # 固定亮度
adb shell svc wifi disable               # 关闭WiFi(减少干扰)
adb shell svc data disable               # 关闭数据(如不需要网络)

2.3 设备信息采集脚本

#!/bin/bash
# collect_device_info.sh - 采集设备基础信息

SERIAL=$(adb get-serialnumber)
OUTPUT="device_info_${SERIAL}.txt"

echo "=== 设备信息采集 ===" > "$OUTPUT"
echo "采集时间: $(date)" >> "$OUTPUT"
echo "" >> "$OUTPUT"

echo "--- 基本信息 ---" >> "$OUTPUT"
echo "型号: $(adb shell getprop ro.product.model)" >> "$OUTPUT"
echo "品牌: $(adb shell getprop ro.product.brand)" >> "$OUTPUT"
echo "Android版本: $(adb shell getprop ro.build.version.release)" >> "$OUTPUT"
echo "SDK版本: $(adb shell getprop ro.build.version.sdk)" >> "$OUTPUT"
echo "CPU ABI: $(adb shell getprop ro.product.cpu.abi)" >> "$OUTPUT"
echo "" >> "$OUTPUT"

echo "--- CPU信息 ---" >> "$OUTPUT"
adb shell cat /proc/cpuinfo >> "$OUTPUT"
echo "" >> "$OUTPUT"

echo "--- 内存信息 ---" >> "$OUTPUT"
adb shell cat /proc/meminfo >> "$OUTPUT"
echo "" >> "$OUTPUT"

echo "--- GPU信息 ---" >> "$OUTPUT"
adb shell dumpsys SurfaceFlinger | grep "GLES:" >> "$OUTPUT"
echo "" >> "$OUTPUT"

echo "--- 屏幕信息 ---" >> "$OUTPUT"
adb shell wm size >> "$OUTPUT"
adb shell wm density >> "$OUTPUT"

echo "设备信息已保存到: $OUTPUT"

三、帧率采集工具

3.1 UE4 Stat命令集

# 在UE4控制台中执行(通过Remote Console或游戏内控制台)

# 基础帧率
stat fps

# 详细帧时间
stat unit          # 显示Game线程、Render线程、GPU时间

# 帧率图表
stat fps_chart     # 帧率时序图
stat unit_graph    # 帧时间时序图

# 启用CSV输出(最推荐)
csvprofile start   # 开始录制到CSV
csvprofile stop    # 停止录制

# 输出位置: Saved/Profiling/CSV/

3.2 adb gfxinfo 采集

#!/bin/bash
# gfxinfo_collector.sh - 通过gfxinfo采集帧时间

PACKAGE="$1"
DURATION="${2:-60}"  # 默认采集60秒
OUTPUT="gfxinfo_$(date +%Y%m%d_%H%M%S).csv"

echo "frame_number,vsync_timestamp,frame_completed,total_time_ms" > "$OUTPUT"

# 重置统计
adb shell dumpsys gfxinfo "$PACKAGE" framestats reset > /dev/null

echo "采集 ${DURATION} 秒的帧数据..."

END_TIME=$(($(date +%s) + DURATION))

while [ $(date +%s) -lt $END_TIME ]; do
    # 获取帧统计
    STATS=$(adb shell dumpsys gfxinfo "$PACKAGE" framestats 2>/dev/null)

    # 解析帧数据行(以数字开头的行)
    echo "$STATS" | grep -E "^[0-9]" | while IFS=',' read -ra FIELDS; do
        if [ ${#FIELDS[@]} -ge 14 ]; then
            START=${FIELDS[1]}  # INTENDED_VSYNC
            END=${FIELDS[13]}   # FRAME_COMPLETED

            if [ "$START" -gt 0 ] && [ "$END" -gt "$START" ]; then
                TOTAL_NS=$((END - START))
                TOTAL_MS=$(echo "scale=2; $TOTAL_NS / 1000000" | bc)
                echo "0,$START,$END,$TOTAL_MS" >> "$OUTPUT"
            fi
        fi
    done

    # 重置以避免重复计数
    adb shell dumpsys gfxinfo "$PACKAGE" framestats reset > /dev/null
    sleep 1
done

echo "数据已保存到: $OUTPUT"
echo "总帧数: $(wc -l < $OUTPUT)"

3.3 示例性能采集工具配置

示例性能采集工具代表一类公开第三方采集工具,可按实际环境选择支持免 root 采集的产品。

# 示例性能采集工具 CLI模式使用
# 需要先在PC端登录示例性能采集工具

# 基础采集
示例性能采集工具 -s <serial> -p <package> -o output.csv \
    --fps --cpu --memory --gpu --battery --temperature

# 指定采集时长
示例性能采集工具 -s <serial> -p <package> -o output.csv \
    --fps --cpu --memory --duration 300

# 导出为标准CSV格式(方便后续Python分析)
示例性能采集工具 -s <serial> -p <package> -o output.csv \
    --fps --cpu --memory --format csv

四、GPU性能工具

4.1 RenderDoc(推荐)

# RenderDoc 截帧分析(详见专题文章)
# PC端安装后通过GUI连接Android设备

# 命令行截帧
renderdoccmd capture \
    --target-android \
    --device <serial> \
    --package com.example.demoapp \
    --capture-frame 60 \
    --output frame_capture.rdcd

# 也可以通过Python脚本自动化
# renderdoc_auto.py - 自动化截帧分析
import renderdoc as rd

def capture_frame(device_serial, package_name, frame_number):
    """通过RenderDoc Python API截帧"""
    # 初始化RenderDoc
    cap = rd.OpenCaptureFile()

    # 配置Android设备
    options = rd.ReplayOptions()
    options.optimisation = rd.ReplayOptimisationLevel.Fastest

    # ... 截帧逻辑
    return result

4.2 Android GPU Inspector (AGI)

AGI是Google官方的GPU分析工具,支持Vulkan和GPU Counter。

# 安装AGI后,通过命令行启动
agi -device <serial> -package <package> -api vulkan

# 获取GPU计数器
# 支持的计数器因GPU厂商不同而异
# 移动GPU档位A: GPU Busy, Shader ALU, Texture Fetch, etc.
# 移动GPU档位A: Job Cycles, Fragment Cycles, External Memory Reads, etc.

4.3 UE4 GPU Profiler

# UE4内置GPU分析(需要Development或Debug包)

# 开始GPU分析
profilegpu

# 查看GPU统计
stat gpu

# 查看特定Pass的耗时
stat scenerendering
stat initviews
stat shadows
stat lighting
stat postprocessing
stat translucent

五、内存分析工具

5.1 memreport(详见专题文章)

# UE4 memreport
memreport -full

# 输出位置: Saved/Profiling/MemReports/
# 解析脚本见 memreport 解读文章

5.2 Android系统级内存监控

#!/bin/bash
# mem_monitor.sh - 持续监控内存变化

PACKAGE="$1"
DURATION="${2:-120}"
OUTPUT="meminfo_$(date +%Y%m%d_%H%M%S).csv"

echo "timestamp,pss_total_kb,native_heap_kb,java_heap_kb,graphics_kb,code_kb" > "$OUTPUT"

END_TIME=$(($(date +%s) + DURATION))

while [ $(date +%s) -lt $END_TIME ]; do
    TIMESTAMP=$(date +%H:%M:%S)

    MEMINFO=$(adb shell dumpsys meminfo "$PACKAGE" 2>/dev/null)

    PSS=$(echo "$MEMINFO" | grep "TOTAL PSS:" | awk '{print $3}')
    NATIVE=$(echo "$MEMINFO" | grep "Native Heap:" | head -1 | awk '{print $3}')
    JAVA=$(echo "$MEMINFO" | grep "Java Heap:" | head -1 | awk '{print $3}')
    GRAPHICS=$(echo "$MEMINFO" | grep "Graphics:" | awk '{print $2}')
    CODE=$(echo "$MEMINFO" | grep "Code:" | head -1 | awk '{print $2}')

    echo "$TIMESTAMP,$PSS,$NATIVE,$JAVA,$GRAPHICS,$CODE" >> "$OUTPUT"

    sleep 5
done

echo "内存监控完成: $OUTPUT"

5.3 LLM(Low Level Memory)标签

# UE4 LLM启用
# 在启动参数中添加
-LLM -LLMCSV

# 运行时查看
stat LLM
stat LLMFULL

# 输出CSV格式的内存标签数据
# Saved/Profiling/LLM/

六、功耗与温度监控

6.1 Battery Historian

# 1. 重置电池统计
adb shell dumpsys batterystats --reset

# 2. 执行测试场景(如10分钟游戏)

# 3. 导出电池数据
adb shell dumpsys batterystats > batterystats.txt
adb bugreport > bugreport.zip

# 4. 使用Battery Historian分析
# 在Docker中运行Battery Historian
docker run -p 9999:9999 gcr.io/android-battery-historian/stable:latest

# 5. 访问 http://localhost:9999 上传bugreport.zip

6.2 温度监控

#!/bin/bash
# temp_monitor.sh - 持续监控设备温度

DURATION="${1:-120}"
OUTPUT="temperature_$(date +%Y%m%d_%H%M%S).csv"

echo "timestamp,battery_temp,cpu_temp,gpu_temp" > "$OUTPUT"

END_TIME=$(($(date +%s) + DURATION))

while [ $(date +%s) -lt $END_TIME ]; do
    TIMESTAMP=$(date +%H:%M:%S)

    # 电池温度
    BATTERY=$(adb shell dumpsys battery | grep temperature | awk '{print $2}')
    BATTERY_C=$(echo "scale=1; $BATTERY / 10" | bc)

    # CPU温度(不同设备路径不同)
    CPU_TEMP=""
    for path in /sys/class/thermal/thermal_zone0/temp \
                /sys/class/thermal/thermal_zone1/temp \
                /sys/devices/virtual/thermal/thermal_zone0/temp; do
        TEMP=$(adb shell cat $path 2>/dev/null)
        if [ -n "$TEMP" ] && [ "$TEMP" -gt 1000 ] 2>/dev/null; then
            CPU_C=$(echo "scale=1; $TEMP / 1000" | bc)
            CPU_TEMP="$CPU_C"
            break
        fi
    done

    echo "$TIMESTAMP,$BATTERY_C,$CPU_TEMP," >> "$OUTPUT"
    sleep 10
done

echo "温度监控完成: $OUTPUT"

6.3 功耗估算

#!/usr/bin/env python3
"""基于电池数据估算功耗"""

import subprocess
import time

def get_battery_info():
    """获取电池信息"""
    result = subprocess.run(['adb', 'shell', 'dumpsys', 'battery'],
                          capture_output=True, text=True)
    info = {}
    for line in result.stdout.split('\n'):
        if 'level:' in line:
            info['level'] = int(line.split(':')[1].strip())
        elif 'temperature:' in line:
            info['temperature'] = int(line.split(':')[1].strip()) / 10.0
        elif 'voltage:' in line:
            info['voltage'] = int(line.split(':')[1].strip()) / 1000.0  # mV -> V
    return info

def estimate_power(duration_sec=300, interval_sec=30):
    """估算平均功耗"""
    # 获取设备电池容量
    cap_result = subprocess.run(['adb', 'shell', 'cat', '/sys/class/power_supply/battery/charge_full'],
                               capture_output=True, text=True)
    battery_capacity_mah = int(cap_result.stdout.strip()) / 1000  # μAh -> mAh

    start_battery = get_battery_info()
    print(f"开始测试 - 电量: {start_battery['level']}%, 电压: {start_battery.get('voltage', 'N/A')}V")

    time.sleep(duration_sec)

    end_battery = get_battery_info()
    print(f"结束测试 - 电量: {end_battery['level']}%, 电压: {end_battery.get('voltage', 'N/A')}V")

    # 计算消耗
    level_drop = start_battery['level'] - end_battery['level']
    energy_consumed_mah = battery_capacity_mah * level_drop / 100
    avg_current_ma = energy_consumed_mah / (duration_sec / 3600)

    avg_voltage = (start_battery.get('voltage', 3.8) + end_battery.get('voltage', 3.8)) / 2
    avg_power_mw = avg_current_ma * avg_voltage

    print(f"\n=== 功耗估算 ===")
    print(f"测试时长: {duration_sec}秒")
    print(f"电量消耗: {level_drop}% ({energy_consumed_mah:.0f} mAh)")
    print(f"平均电流: {avg_current_ma:.0f} mA")
    print(f"平均电压: {avg_voltage:.2f} V")
    print(f"平均功耗: {avg_power_mw:.0f} mW ({avg_power_mw/1000:.2f} W)")

    return {
        'avg_current_ma': avg_current_ma,
        'avg_power_mw': avg_power_mw,
        'level_drop': level_drop
    }

if __name__ == '__main__':
    estimate_power(duration_sec=300)

七、系统级Tracing工具

7.1 Perfetto(推荐替代Systrace)

# Perfetto是Google的新一代系统追踪工具

# 1. 基础trace采集
adb shell perfetto -o /data/misc/perfetto-traces/trace.pb -t 10s \
    sched freq idle am wm gfx view binder_driver hal dalvik camera input res memory

# 2. 拉取trace文件
adb pull /data/misc/perfetto-traces/trace.pb ./trace.pb

# 3. 在 https://ui.perfetto.dev 打开分析

# 4. 使用配置文件进行高级采集
cat > /tmp/perfetto_config.pbtx << 'EOF'
buffers {
  size_kb: 65536
  fill_policy: RING_BUFFER
}

data_sources {
  config {
    name: "linux.ftrace"
    ftrace_config {
      ftrace_events: "sched/sched_switch"
      ftrace_events: "power/cpu_frequency"
      ftrace_events: "power/suspend_resume"
      ftrace_events: "gpu_mem/gpu_mem_total"
    }
  }
}

data_sources {
  config {
    name: "linux.process_stats"
    process_stats_config {
      scan_all_processes_on_start: true
      proc_stats_poll_ms: 1000
    }
  }
}

duration_ms: 30000
EOF

adb push /tmp/perfetto_config.pbtx /data/local/tmp/config.pbtx
adb shell perfetto -c /data/local/tmp/config.pbtx -o /data/misc/perfetto-traces/trace.pb

7.2 simpleperf(CPU 热点分析)

# simpleperf是Android NDK自带的性能分析工具

# 1. 采集CPU热点(需要root或可调试应用)
adb shell simpleperf record -p $(adb shell pidof com.example.demoapp) \
    --duration 10 -o /data/local/tmp/perf.data

# 2. 拉取数据
adb pull /data/local/tmp/perf.data ./perf.data

# 3. 生成报告
simpleperf report -i perf.data --sort comm,dso,symbol

# 4. 生成火焰图
simpleperf report -i perf.data -g --symfs ./symbols > report.txt
# 使用 FlameGraph 工具生成可视化

八、工具链集成与自动化

8.1 统一采集框架

#!/usr/bin/env python3
"""
perf_collector.py - 统一性能采集框架
同时采集帧率、CPU、GPU、内存、温度等数据
"""

import subprocess
import threading
import time
import csv
import os
from datetime import datetime

class PerfCollector:
    def __init__(self, device_serial, package_name, output_dir='./perf_data'):
        self.serial = device_serial
        self.package = package_name
        self.output_dir = output_dir
        self.running = False
        os.makedirs(output_dir, exist_ok=True)

        timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
        self.csv_file = os.path.join(output_dir, f'perf_{timestamp}.csv')

    def _get_memory_info(self):
        result = subprocess.run(
            ['adb', '-s', self.serial, 'shell', 'dumpsys', 'meminfo', self.package],
            capture_output=True, text=True, timeout=5
        )
        pss = native = graphics = 0
        for line in result.stdout.split('\n'):
            if 'TOTAL PSS:' in line:
                parts = line.split()
                pss = int(parts[2]) if len(parts) > 2 else 0
            elif 'Native Heap:' in line and native == 0:
                parts = line.split()
                native = int(parts[2]) if len(parts) > 2 else 0
            elif 'Graphics:' in line:
                parts = line.split()
                graphics = int(parts[1]) if len(parts) > 1 else 0
        return pss, native, graphics

    def _get_temperature(self):
        result = subprocess.run(
            ['adb', '-s', self.serial, 'shell', 'dumpsys', 'battery'],
            capture_output=True, text=True, timeout=5
        )
        for line in result.stdout.split('\n'):
            if 'temperature:' in line:
                temp = int(line.split(':')[1].strip())
                return temp / 10.0
        return 0.0

    def _get_battery_level(self):
        result = subprocess.run(
            ['adb', '-s', self.serial, 'shell', 'dumpsys', 'battery'],
            capture_output=True, text=True, timeout=5
        )
        for line in result.stdout.split('\n'):
            if 'level:' in line:
                return int(line.split(':')[1].strip())
        return 0

    def _get_cpu_usage(self):
        result = subprocess.run(
            ['adb', '-s', self.serial, 'shell', 'top', '-n', '1', '-b'],
            capture_output=True, text=True, timeout=5
        )
        for line in result.stdout.split('\n'):
            if self.package in line:
                parts = line.split()
                for p in parts:
                    if '%' in p:
                        return float(p.replace('%', ''))
        return 0.0

    def _collect_loop(self, interval_sec=5):
        with open(self.csv_file, 'w', newline='') as f:
            writer = csv.writer(f)
            writer.writerow([
                'timestamp', 'elapsed_sec', 'pss_kb', 'native_heap_kb',
                'graphics_kb', 'cpu_percent', 'temperature_c', 'battery_level'
            ])

            start_time = time.time()
            while self.running:
                ts = datetime.now().strftime('%H:%M:%S.%3')[:-1]
                elapsed = time.time() - start_time

                pss, native, graphics = self._get_memory_info()
                cpu = self._get_cpu_usage()
                temp = self._get_temperature()
                battery = self._get_battery_level()

                writer.writerow([ts, f'{elapsed:.1f}', pss, native, graphics,
                                f'{cpu:.1f}', f'{temp:.1f}', battery])
                f.flush()

                print(f"[{ts}] PSS:{pss}KB CPU:{cpu:.0f}% Temp:{temp:.0f}°C Batt:{battery}%")

                time.sleep(interval_sec)

    def start(self, interval_sec=5):
        """开始采集"""
        self.running = True
        self.thread = threading.Thread(target=self._collect_loop, args=(interval_sec,))
        self.thread.daemon = True
        self.thread.start()
        print(f"性能采集已启动,数据保存到: {self.csv_file}")

    def stop(self):
        """停止采集"""
        self.running = False
        self.thread.join(timeout=10)
        print(f"性能采集已停止,数据文件: {self.csv_file}")

if __name__ == '__main__':
    import sys

    serial = sys.argv[1] if len(sys.argv) > 1 else None
    package = sys.argv[2] if len(sys.argv) > 2 else 'com.example.demoapp'

    if not serial:
        result = subprocess.run(['adb', 'devices'], capture_output=True, text=True)
        lines = result.stdout.strip().split('\n')[1:]
        devices = [l.split('\t')[0] for l in lines if '\tdevice' in l]
        if not devices:
            print("未检测到设备")
            sys.exit(1)
        serial = devices[0]

    collector = PerfCollector(serial, package)
    collector.start(interval_sec=5)

    try:
        while True:
            time.sleep(1)
    except KeyboardInterrupt:
        collector.stop()

8.2 自动化测试脚本

#!/bin/bash
# auto_perf_test.sh - 自动化性能测试脚本

PACKAGE="com.example.demoapp"
SERIAL=$(adb devices | grep -v "List" | head -1 | awk '{print $1}')
TEST_DURATION=300  # 5分钟

echo "=== 自动化性能测试 ==="
echo "设备: $SERIAL"
echo "包名: $PACKAGE"
echo "时长: $TEST_DURATION 秒"

# 1. 采集设备信息
bash collect_device_info.sh

# 2. 启动统一采集器
python3 perf_collector.py "$SERIAL" "$PACKAGE" &
COLLECTOR_PID=$!
sleep 2

# 3. 冷启动游戏
echo "启动游戏..."
adb -s "$SERIAL" shell am force-stop "$PACKAGE"
sleep 2
adb -s "$SERIAL" shell am start -n "$PACKAGE/com.epicgames.ue4.SplashActivity"
sleep 30  # 等待加载完成

# 4. 采集UE4 memreport
echo "采集memreport..."
adb -s "$SERIAL" shell "echo 'memreport -full' > /dev/null"  # 需要实际的控制台通道
sleep 5

# 5. 等待测试完成
echo "测试进行中... (${TEST_DURATION}秒)"
sleep $TEST_DURATION

# 6. 采集结束时的memreport
echo "采集结束memreport..."

# 7. 停止采集器
kill $COLLECTOR_PID
wait $COLLECTOR_PID 2>/dev/null

# 8. 拉取数据
mkdir -p test_results_$(date +%Y%m%d)
adb -s "$SERIAL" pull /sdcard/Android/data/$PACKAGE/files/Saved/Profiling/ \
    ./test_results_$(date +%Y%m%d)/profiling/

# 9. 生成报告
python3 generate_report.py ./perf_data/

echo "=== 测试完成 ==="

8.3 Jenkins/GitLab CI集成

# .gitlab-ci.yml 示例
performance_test:
  stage: test
  tags:
    - android-device  # 标签指定有Android设备的runner
  script:
    - python3 perf_collector.py "$DEVICE_SERIAL" "$PACKAGE" &
    - sleep 5
    - adb shell am start -n "$PACKAGE/com.epicgames.ue4.SplashActivity"
    - sleep 300
    - kill %1
    - python3 generate_report.py ./perf_data/
    - python3 check_regression.py ./perf_data/ ./baseline/
  artifacts:
    paths:
      - perf_data/
      - reports/
    expire_in: 30 days
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'  # 定时任务触发

九、设备档位A专项配置

针对设备档位A(入门级 SoC, 移动GPU档位A, 2 GB RAM)的特殊配置:

# 1. 确保足够存储空间(2GB RAM设备存储通常紧张)
adb shell df -h /data

# 2. 清理后台进程
adb shell am kill-all

# 3. 关闭不必要的系统服务
adb shell settings put global auto_time 0
adb shell settings put global auto_time_zone 0

# 4. 移动GPU档位A Counter采集
# 示例GPU-D 支持的Performance Counter
adb shell "echo 1 > /sys/module/mali/parameters/mali_gpu_perf"
# 通过 sysfs 读取GPU利用率
adb shell cat /sys/module/mali/parameters/mali_gpu_utilization

# 5. 设置性能模式(部分某品牌设备支持)
adb shell "echo performance > /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor"
# 注意:这会影响功耗测试的真实性,仅用于峰值性能测试

十、数据分析流水线

10.1 Jupyter Notebook模板

# perf_analysis_template.ipynb 的核心cell结构

# Cell 1: 导入与数据加载
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns

# 加载数据
df = pd.read_csv('perf_data/perf_20260101_143000.csv')
print(f"数据量: {len(df)} 条记录,时长: {df['elapsed_sec'].max():.0f} 秒")

# Cell 2: 数据清洗
df = df[df['elapsed_sec'] > 10]  # 去掉前10秒
df['fps'] = 1000 / df['frame_time_ms'] if 'frame_time_ms' in df.columns else None

# Cell 3: 基础统计
display(df.describe())

# Cell 4: 多维度时序图
fig, axes = plt.subplots(4, 1, figsize=(16, 16), sharex=True)
axes[0].plot(df['elapsed_sec'], df['fps_smooth'], 'b-')
axes[0].set_ylabel('FPS')
axes[1].plot(df['elapsed_sec'], df['cpu_percent'], 'r-')
axes[1].set_ylabel('CPU %')
axes[2].plot(df['elapsed_sec'], df['pss_kb']/1024, 'g-')
axes[2].set_ylabel('PSS (MB)')
axes[3].plot(df['elapsed_sec'], df['temperature_c'], 'orange')
axes[3].set_ylabel('温度 (°C)')
axes[3].set_xlabel('时间 (秒)')
plt.tight_layout()
plt.savefig('reports/multi_dimension_timeline.png', dpi=150)

# Cell 5: 相关性分析
corr_cols = ['fps', 'cpu_percent', 'pss_kb', 'temperature_c']
if all(c in df.columns for c in corr_cols):
    corr = df[corr_cols].corr()
    sns.heatmap(corr, annot=True, cmap='coolwarm')
    plt.title('指标相关性矩阵')
    plt.savefig('reports/correlation_heatmap.png')

10.2 自动化回归检测

#!/usr/bin/env python3
"""性能回归检测:对比当前版本与基线"""

import json
import sys

def load_metrics(perf_dir):
    """从采集数据中提取关键指标"""
    # 实际实现中会解析CSV并计算统计指标
    return {
        'avg_fps': 28.4,
        'p50_fps': 30.1,
        'p99_frame_time_ms': 67.3,
        'jank_rate': 8.72,
        'avg_pss_mb': 623,
        'peak_pss_mb': 756,
        'avg_cpu_percent': 45.2,
        'peak_temperature': 42.1
    }

def check_regression(baseline_file, current_metrics, thresholds_file='thresholds.json'):
    """检查是否有回归"""
    with open(baseline_file) as f:
        baseline = json.load(f)
    with open(thresholds_file) as f:
        thresholds = json.load(f)

    issues = []
    for metric, threshold in thresholds.items():
        if metric not in current_metrics or metric not in baseline:
            continue

        base_val = baseline[metric]
        curr_val = current_metrics[metric]

        # 判断方向(FPS类指标越大越好,其他越小越好)
        if 'fps' in metric.lower():
            regression = (base_val - curr_val) / base_val > threshold
        else:
            regression = (curr_val - base_val) / base_val > threshold

        if regression:
            change = (curr_val - base_val) / base_val * 100
            issues.append(f"[REGRESSION] {metric}: {base_val:.1f} → {curr_val:.1f} ({change:+.1f}%)")

    return issues

if __name__ == '__main__':
    thresholds = {
        'avg_fps': 0.10,
        'p99_frame_time_ms': 0.15,
        'jank_rate': 0.20,
        'avg_pss_mb': 0.10,
        'peak_pss_mb': 0.10,
    }

    with open('thresholds.json', 'w') as f:
        json.dump(thresholds, f, indent=2)

    current = load_metrics('./current_perf/')
    issues = check_regression('./baseline.json', current)

    if issues:
        print("❌ 性能回归检测到:")
        for issue in issues:
            print(f"  {issue}")
        sys.exit(1)
    else:
        print("✅ 性能回归检测通过")
        sys.exit(0)

总结

手游性能测试工具链的搭建是一个持续演进的过程。核心建议:

  1. 分层覆盖:帧率→CPU/GPU→内存→功耗/温度→系统级,逐层深入
  2. 自动化优先:能脚本化的不要手动,能CI集成的不要人工触发
  3. 数据归档:每次测试的原始数据都要保留,建立版本基线
  4. 设备矩阵:覆盖高、中、低三个档次的代表设备(如设备档位A代表低端)
  5. 工具组合:没有万能工具,不同问题用不同工具组合分析
  6. Python是粘合剂:用Python串联采集、分析、报告的全流程

工具选型推荐清单:

维度推荐工具备选工具
帧率UE4 CSV Profiler + Python示例性能采集工具
GPURenderDocAGI
内存UE4 memreport + dumpsys meminfoMAT
CPUUE4 Stat + Perfettosimpleperf
功耗Battery Historian示例性能采集工具
温度adb dumpsys battery温度枪
系统Perfettosystrace

标签: #手游性能测试 #工具链 #Android #UE4 #示例性能采集工具 #RenderDoc #Perfetto #自动化测试 #CI/CD #性能优化

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐