Fire平板电脑的应用性能脚本
“性能测试”是亚马逊Fire OS设备上的应用测试过程,测试领域包括兼容性、可靠性、速度、响应时间、稳定性和资源使用等。您可以使用此测试来识别和解决应用的性能瓶颈。性能测试涉及收集和评估关键性能指标 (KPI)。要收集KPI指标,您需要在亚马逊设备上运行一组特定的步骤,然后使用日志等设备资源查找或计算指标。
在将您的应用提交到亚马逊应用商店之前,请务必运行性能测试。本页提供测试不同类别KPI的步骤,并包含可在自动化中使用的示例代码。本指南涵盖以下KPI:
- 延迟 - 第一帧时间 (TTFF)
- 准备好供使用 - 完全显示时间 (TTFD)
- 使用应用的核心功能(例如播放视频或玩游戏)后的内存
设置
要开始使用,请在开发计算机上安装以下软件程序包:
- Amazon Corretto
- Android Studio(确保在安装过程中安装平台工具)
- Appium
除了安装这些软件程序包之外,还需要完成以下操作:
- 为JAVA_HOME和ANDROID_HOME文件夹设置路径。
- 在设备上启用开发者模式并启用USB调试。有关说明,请参阅启用开发者选项。
- 捕获所连接设备的序列号。要列出物理连接设备的序列号,您可以使用Android调试桥 (ADB) 命令
adb devices -l。
测试策略
测试期间您可使用应用启动器意图或Monkey工具多次进行应用的启动和强制停止。在每次迭代之间,您必须执行某些操作,例如捕获ADB Logcat日志、执行导航操作、从Vitals和ADB日志中捕获计时器值,以及在强制停止应用之前捕获内存和RAM使用情况。根据您配置的迭代次数,此循环会继续。由于网络状况、系统负载和其他因素可能会影响测试结果,因此使用多次迭代来抵消外部因素的干扰。
要计算指标平均值,亚马逊建议对以下测试类别进行最少次数的迭代。
| 性能测试类别 | 建议的最小迭代次数 |
|---|---|
| 延迟 - 第一帧时间 (TTFF) | 50 |
| 准备好供使用 - 完全显示时间 (TTFD) | 10 |
| 内存 | 5 |
要测试的设备:
- Fire OS 8: Fire HD 10 (2023)
- Fire OS 8: Fire Max 11 (2023)
您可以使用在性能测试中捕获的日志、屏幕截图和其他工件进行调试或数据使用。清理工作包括强制停止Appium设备对象。
以下部分包含可以添加到测试自动化脚本中的代码示例。
获取设备类型
以下示例代码显示如何获取已连接设备的设备类型。
public String get_device_type() {
String deviceType = null;
try (BufferedReader read = new BufferedReader(new InputStreamReader
(Runtime.getRuntime().exec
("adb -s "+ DSN +" shell getprop ro.build.configuration")
.getInputStream())))
{
String outputLines = read.readLine();
switch (outputLines) {
case "tv":
deviceType = "FTV";
break;
case "tablet":
deviceType = "Tablet";
break;
}
}
catch (Exception e) {
System.out.println("获取设备类型信息时出现异常:" + e);
}
return deviceType;
}
import java.io.BufferedReader
import java.io.InputStreamReader
fun getDeviceType(): String? {
var deviceType: String? = null
try {
BufferedReader(InputStreamReader(Runtime.getRuntime().exec("adb -s $DSN shell getprop ro.build.configuration").inputStream)).use { read ->
val outputLines = read.readLine()
when (outputLines) {
"tv" -> deviceType = "FTV"
"tablet" -> deviceType = "Tablet"
}
}
} catch (e: Exception) {
println("获取设备类型信息时出现异常:$e")
}
return deviceType
}
检索主启动器活动的组件名称
以下示例代码显示如何检索主启动器活动的组件名称。此方法获取在测应用的主要活动,并通过组合应用程序包和主活动的名称来构造组件名称。
try (BufferedReader read = new BufferedReader(new InputStreamReader
(Runtime.getRuntime().exec("adb -s "+ DSN +" shell pm dump "+ appPackage +" | grep -A 1 MAIN").getInputStream()))) {
String outputLine = null;
String line;
while ((line = read.readLine()) != null) {
if (line.contains(appPackage + "/")) {
outputLine = line;
break;
}
}
outputLine = outputLine.split("/")[1];
String mainActivity = outputLine.split(" ")[0];
String componentName = appPackage + "/" + mainActivity;
return componentName;
}
catch (Exception e) {
System.out.println("检索应用主活动时出现异常" + e);
}
import java.io.BufferedReader
import java.io.InputStreamReader
try {
val process = Runtime.getRuntime().exec("adb -s $DSN shell pm dump $appPackage | grep -A 1 MAIN")
val inputStream = process.inputStream
val reader = BufferedReader(InputStreamReader(inputStream))
var line: String? = null
var outputLine: String? = null
while (reader.readLine().also { line = it } != null) {
if (line!!.contains("$appPackage/")) {
outputLine = line
break
}
}
outputLine = outputLine!!.split("/")[1]
val mainActivity = outputLine.split(" ")[0]
val componentName = "$appPackage/$mainActivity"
componentName
} catch (e: Exception) {
println("检索应用主活动时出现异常:$e")
}
使用主启动器活动的组件名称启动应用
借助以下示例代码使用主启动器活动的组件名称启动应用。该代码使用上一部分中定义的componentName变量,该变量通过组合应用程序包和主要活动来创建组件名称。
try (BufferedReader read = new BufferedReader(new InputStreamReader
(Runtime.getRuntime().exec("adb -s "+ DSN +" shell am start -n " + componentName).getInputStream()))) {
String deviceName = getDeviceName(DSN);
String line;
while ((line = read.readLine()) != null) {
if (line.startsWith("Starting: Intent")) {
System.out.println("应用启动成功,使用的是 - " + componentName);
break;
} else if (line.contains("Error")) {
System.out.println("应用启动错误");
}
}
} catch (Exception e) {
System.out.println("启动应用时出现异常:" + e);
}
import java.io.BufferedReader
import java.io.InputStreamReader
val process = Runtime.getRuntime().exec("adb -s $DSN shell am start -n $componentName")
val inputStream = process.inputStream
val reader = BufferedReader(InputStreamReader(inputStream))
try {
val deviceName = getDeviceName(DSN)
var line: String? = null
while (reader.readLine().also { line = it } != null) {
if (line!!.startsWith("Starting: Intent")) {
println("应用启动成功,使用的是 - $componentName")
break
} else if (line!!.contains("Error")) {
println("启动应用时出现错误")
}
}
} catch (e: Exception) {
println("启动应用时出现异常:$e")
}
使用Monkey工具启动应用
以下代码展示了如何使用Monkey工具启动应用。
try {
String monkeyCommand = null;
if (DEVICE_TYPE.equals(FTV)) {
monkeyCommand = " shell monkey --pct-syskeys 0 -p "
}
else {
monkeyCommand = " shell monkey -p "
}
BufferedReader launchRead = new BufferedReader(new InputStreamReader
(Runtime.getRuntime().exec("adb -s "+ DSN + monkeyCommand + appPackage +" -c android.intent.category.LAUNCHER 1").getInputStream()));
String line;
while ((line = launchRead.readLine()) != null) {
if (line.contains("Events injected")) {
System.out.println("使用Monkey工具成功启动应用 - " + appPackage);
launchRead.close();
return true;
}
else if (line.contains("Error") || line.contains("No activities found")) {
System.out.println("通过Monkey工具使用意图启动应用时出现错误”);
launchRead.close();
return false;
}
}
}
catch (Exception e) {
System.out.println("使用Monkey启动应用时出现异常" + e);
return false;
}
try {
val monkeyCommand: String?
if (DEVICE_TYPE == FTV) {
monkeyCommand = " shell monkey --pct-syskeys 0 -p "
}
else {
monkeyCommand = " shell monkey -p "
}
val launchRead = BufferedReader(InputStreamReader(
Runtime.getRuntime().exec("adb -s $DSN $monkeyCommand $appPackage -c android.intent.category.LAUNCHER 1").inputStream))
var line: String?
while (launchRead.readLine().also { line = it } != null) {
if (line!!.contains("Events injected")) {
println("使用Monkey工具成功启动应用 - $appPackage")
launchRead.close()
return true
}
else if (line!!.contains("Error") || line!!.contains("No activities found")) {
println("通过Monkey工具使用意图启动应用时出现错误")
launchRead.close()
return false
}
}
}
catch (e: Exception) {
println("使用Monkey启动应用时出现异常:$e")
return false
}
强制停止应用
以下示例代码示出如何强制停止应用。
try {
Runtime.getRuntime().exec("adb -s "+ DSN +" shell am force-stop " + appPackage);
System.out.println("已强制停止应用 - " + appPackage);
}
catch (Exception e) {
System.out.println("强制停止应用时出现异常" + e);
}
try {
Runtime.getRuntime().exec("adb -s ${DSN} shell am force-stop ${appPackage}")
println("已强制停止应用 - $appPackage")
} catch (e: Exception) {
println("强制停止应用时出现异常:$e")
}
一般命令
以下各部分提供了可在性能测试中使用的命令示例。
捕获ADB日志
以下示例代码示出如何捕获使用threadtime格式的ADB日志。
public String ADB_LOGCAT_DUMP = "adb shell logcat -v threadtime -b all -d";
public String ADB_LOGCAT_CLEAR = "adb shell logcat -v threadtime -b all -c";
//下面的方法用于将ADB命令附加到设备的DSN值
public Process adb(String DSN, String message) {
Process process = null;
try {
process = Runtime.getRuntime().exec("adb -s " + DSN + message);
}
catch (Exception e) {
System.out.println("执行ADB命令时出现错误" + e);
}
return process;
}
public const val ADB_LOGCAT_DUMP: String = "adb shell logcat -v threadtime -b all -d"
public const val ADB_LOGCAT_CLEAR: String = "adb shell logcat -v threadtime -b all -c"
//下面的函数用于将ADB命令附加到设备的DSN值
fun adb(DSN: String, message: String): Process? {
return try {
Runtime.getRuntime().exec("adb -s $DSN$message")
}
catch (e: Exception) {
println("Exception while executing adb commands$e")
null
}
}
从Android Vitals缓冲区日志捕获计时器值
以下示例代码示出如何从Vitals缓冲区中筛选出性能延迟计时器值。
public int get_vitals_timer(File logFile, String appPackage, String metricSearch) {
BufferedReader reader = null;
String line_Metric;
int timer = 0;
try {
reader = new BufferedReader(new FileReader(logFile))
while ((line_Metric = reader.readLine()) != null) {
if (line_Metric.contains("performance:" + metricSearch)
&& line_Metric.contains("key=" + appPackage)) {
timer = splitToTimer(line_Metric);
}
}
}
catch (Exception e) {
System.out.println(e);
}
finally {
reader.close();
}
return timer;
}
import java.io.File
fun getVitalsTimer(logFile: File, appPackage: String, metricSearch: String): Int {
var reader: BufferedReader? = null
var lineMetric: String
var timer = 0
try {
reader = logFile.bufferedReader()
while (reader.readLine().also { lineMetric = it } != null) {
if (lineMetric.contains("performance:$metricSearch") && lineMetric.contains("key=$appPackage")) {
timer = splitToTimer(lineMetric)
}
}
} catch (e: Exception) {
println(e)
} finally {
reader?.close()
}
return timer
}
从活动管理器缓冲区日志捕获计时器值
以下代码示出如何通过活动或窗口管理器缓冲区筛选出应用的计时器值。
public int get_displayed_timer(File adb_log_file, String appPackage) {
BufferedReader br = null;
int timer = 0;
String displayedMarker = null;
try {
br = new BufferedReader(new FileReader(adb_log_file));
while ((displayedMarker = br.readLine()) != null) {
if ((displayedMarker.contains(AM_ACTIVITY_LAUNCH_TIME) ||
displayedMarker.contains(WM_ACTIVITY_LAUNCH_TIME) ||
displayedMarker.contains(WARM_ACTIVITY_LAUNCH_TIME)) &&
displayedMarker.contains(appPackage))
break;
}
if (displayedMarker != null) {
displayedMarker = displayedMarker.split("]")[0].split("\\[")[1];
ffValue = Integer.parseInt(displayedMarker);
}
} catch (Exception e) {
System.out.println(e);
} finally {
br.close();
}
return timer;
}
public int get_displayed_timer(File adb_log_file, String appPackage) {
BufferedReader br = null;
int timer = 0;
String displayedMarker = null;
try {
br = new BufferedReader(new FileReader(adb_log_file));
while ((displayedMarker = br.readLine()) != null) {
if ((displayedMarker.contains(AM_ACTIVITY_LAUNCH_TIME) ||
displayedMarker.contains(WM_ACTIVITY_LAUNCH_TIME) ||
displayedMarker.contains(WARM_ACTIVITY_LAUNCH_TIME)) &&
displayedMarker.contains(appPackage))
break;
}
if (displayedMarker != null) {
displayedMarker = displayedMarker.split("]")[0].split("\\[")[1];
ffValue = Integer.parseInt(displayedMarker);
}
} catch (Exception e) {
System.out.println(e);
} finally {
br.close();
}
return timer;
}
日志中使用的常见缩写
在以编程方式启动应用之前,必须具有一些基本参数,例如应用的程序包名称和主要活动。为了帮助实现自动化,您还可以为日志中经常遇到的值添加常量。以下是为了进行性能测试而可以在自动化脚本中添加的常量。
public String COOL_APP = "cool_app_launch_time";
public String COOL_ACTIVITY = "cool_activity_launch_time";
public String WARM_APP_WARM = "warm_app_warm_transition_launch_time";
public String WARM_APP_COOL = "warm_app_cool_transition_launch_time";
public String AM_ACTIVITY_LAUNCH_TIME = "am_activity_launch_time:";
public String WM_ACTIVITY_LAUNCH_TIME = "wm_activity_launch_time:";
public String WARM_ACTIVITY_LAUNCH_TIME = "performance:warm_activity_launch_time:";
public String AM_FULLY_DRAWN = "am_activity_fully_drawn_time:";
public String WM_FULLY_DRAWN = "wm_activity_fully_drawn_time:";
const val COOL_APP: String = "cool_app_launch_time"
const val COOL_ACTIVITY: String = "cool_activity_launch_time"
const val WARM_APP_WARM: String = "warm_app_warm_transition_launch_time"
const val WARM_APP_COOL: String = "warm_app_cool_transition_launch_time"
const val AM_ACTIVITY_LAUNCH_TIME: String = "am_activity_launch_time:"
const val WM_ACTIVITY_LAUNCH_TIME: String = "wm_activity_launch_time:"
const val WARM_ACTIVITY_LAUNCH_TIME: String = "performance:warm_activity_launch_time:"
const val AM_FULLY_DRAWN: String = "am_activity_fully_drawn_time:"
const val WM_FULLY_DRAWN: String = "wm_activity_fully_drawn_time:"
延迟 - 第一帧时间
延迟KPI,即第一帧时间 (TTFF),用于衡量应用启动后显示其第一个可视帧所用的时间。通过在冷和热启动期间进行测量,该KPI旨在复现不同场景下的真实用户行为。
确定绘制完第一帧所需的时间
以下ADB命令示出如何确定绘制完第一帧所需的时间。
adb logcat | grep "Displayed APP_PACKAGE_NAME/LAUNCHER_ACTIVITY"
adb logcat | findstr "Displayed APP_PACKAGE_NAME\LAUNCHER_ACTIVITY"
输出示例
ActivityManager: Displayed APP_PACKAGE_NAME/LAUNCHER_ACTIVITY +930ms (total +849ms)
场景: 冷启动 - 第一帧时间
TTFF冷启动是指应用进程停止或设备重启后,应用启动并显示其第一帧所需的时间。测试冷启动时,您需要在应用被强制停止后启动应用,这模拟了首次使用或全新启动的场景。冷启动通常比热启动所需的时间更长,因为应用必须重新加载服务。
测量从冷启动开始到第一帧的时间
- 运行logcat清除命令。
String = "adb shell logcat -v threadtime -b all -c"
- 下载、启动和登录应用。
adb shell am start <应用程序包>/<应用主活动> - 运行logcat命令。
String = "adb shell logcat -v threadtime -b all -d"
- 通过查找包含以下字符串的日志行来捕获计时器值。
cool_app_launch_time - 应用完全启动后,强制停止该应用。
adb shell 我强制停止 <app_pkg> - 重复这些步骤,进行50次迭代。
冷启动迭代
以下示例代码运行延迟冷启动测试并输出配置的迭代次数的延迟值。
for (int i = 0; i < iterations; i++) {
if (!launchAppUsingMonkey(DSN, appPackage))
launchAppUsingIntent(DSN, appIntent);
Thread.sleep(10);
BufferedReader read = new BufferedReader
(new InputStreamReader(Runtime.getRuntime()
.exec("adb -s "+ DSN +" shell logcat -v threadtime -b all -d")
.getInputStream()));
String line_CoolApp;
String line_CoolActivity;
log_file_writer(read);
File adb_log_file = new File( kpi_log_file_path + "/KPI_Log.txt");
if (deviceType == "Tablet") {
timer = get_vitals_timer(adb_log_file, appPackage, COOL_APP);
} else if (deviceType == "FTV") {
timer = get_displayed_timer(adb_log_file, appPackage);
if (timer == 0) {
timer = get_vitals_timer(adb_log_file, appPackage, COOL_APP);
}
}
if (timer == 0) {
timer = get_vitals_timer(adb_log_file, appPackage, COOL_ACTIVITY);
}
forceStopApp(DSN, appPackage);
Thread.sleep(10);
Runtime.getRuntime().exec("adb -s "+ DSN +" shell logcat -b all -c");
}
for (int i = 0; i < iterations; i++) {
if (!launchAppUsingMonkey(DSN, appPackage)) {
launchAppUsingIntent(DSN, appIntent)
}
Thread.sleep(10)
val read = BufferedReader(InputStreamReader(Runtime.getRuntime().exec("adb -s ${DSN} shell logcat -v threadtime -b all -d").getInputStream()))
val line_CoolApp = read.readLine()
val line_CoolActivity = read.readLine()
log_file_writer(read)
val adb_log_file = File(kpi_log_file_path + "/KPI_Log.txt")
when (deviceType) {
"Tablet" -> timer = get_vitals_timer(adb_log_file, appPackage, COOL_APP)
"FTV" -> timer = get_displayed_timer(adb_log_file, appPackage)
.takeIf { it != 0 }
?: get_vitals_timer(adb_log_file, appPackage, COOL_APP)
}
if (timer == 0) {
timer = get_vitals_timer(adb_log_file, appPackage, COOL_ACTIVITY)
}
forceStopApp(DSN, appPackage)
Thread.sleep(10)
Runtime.getRuntime().exec("adb -s ${DSN} shell logcat -b all -c")
}
设备冷启动Vitals日志
以下是设备冷启动Vitals日志的示例。
03-03 12:42:32.589 892 992 I Vlog : PhoneWindowManager:ScreenTime:fgtracking=false;DV;1,Timer=1.0;TI;1,unit=count;DV;1,metadata=!{"d"#{"groupId"#"<应用程序包名称>"$"schemaId"#"123"$"startTimeMs"#"1.677827544773E12"$"packageName"#"<应用程序包名称>"$"endTimeMs"#"1.677827552587E12"$"durationMs"#"7813.0"}};DV;1:HI
03-03 12:42:33.657 892 1092 I Vlog : performance:cool_app_launch_time:fgtracking=false;DV;1,key=<应用程序包名称>;DV;1,**Timer****=****1333.0**;TI;1,unit=ms;DV;1,metadata=App_Metadata!{"d"#{"groupId"#"abc"$"schemaId"#"abc"$"app_version"#"123"$"isChild"#"false"}$"m"#{"prev"#"<应用程序包名称>"$"context"#"abc"}};DV;1:HI
03-03 12:42:33.661 892 1092 I Vlog : performance:user_app_launch_cool:fgtracking=false;DV;1,key=<应用程序包名称>;DV;1,Counter=1;CT;1,unit=count;DV;1,metadata=App_Metadata!{"d"#{"groupId"#"abc"$"schemaId"#"abc"$"isChild"#"false"}};DV;1:HI
03-03 12:49:20.880 892 1092 I Vlog : performance:cool_app_launch_time:fgtracking=false;DV;1,key=<应用程序包名称>;DV;1,**Timer****=****1225.0**;TI;1,unit=ms;DV;1,metadata=App_Metadata!{"d"#{"groupId"#"abc"$"schemaId"#"abc"$"app_version"#"123"$"isChild"#"false"}$"m"#{"prev"#"<应用程序包名称>"$"context"#"abc"}};DV;1:HI
03-03 12:49:20.918 892 1092 I Vlog : performance:user_app_launch_cool:fgtracking=false;DV;1,key=<应用程序包名称>;DV;1,Counter=1;CT;1,unit=count;DV;1,metadata=App_Metadata!{"d"#{"groupId"#"abc"$"schemaId"#"abc"$"isChild"#"false"}};DV;1:HI
要确定第一帧时间,请参阅确定绘制完第一帧所需的时间。
场景: 热启动 - 第一帧时间
热启动TTFF是指当应用进程已在后台运行时,应用启动并显示其第一帧所用的时间。作为该启动活动的一部分,系统将应用从后台带到前台。测试热启动时,应在应用进入后台后启动该应用。热启动通常比冷启动更快,因为应用已经缓存了服务。
测量热启动的第一帧时间
- 请确保应用在后台运行。
- 运行logcat清除命令。
String = "adb shell logcat -v threadtime -b all -c"
- 启动并登录应用。
adb shell am start <应用程序包>/<应用主活动> - 运行logcat命令。
String = "adb shell logcat -v threadtime -b all -d"
- 通过查找包含以下字符串的日志行来捕获计时器值。
warm_app_warm_transition_launch_time - 应用完全启动后,运行以下命令。
adb shell 输入 keyevent KEYCODE_HOME - 重复这些步骤,进行50次迭代。
热启动迭代
以下示例代码运行延迟热启动测试并输出配置的迭代次数的延迟值。
for (int i = 0; i < iterations; i++) {
if (!launchAppUsingMonkey(DSN, appPackage))
launchAppUsingIntent(DSN, appIntent);
Thread.sleep(10);
BufferedReader read = new BufferedReader
(new InputStreamReader(Runtime.getRuntime()
.exec("adb -s "+ DSN +" shell logcat -v threadtime -b all -d")
.getInputStream()));
String line_CoolApp;
String line_CoolActivity;
log_file_writer(read);
String adb_log_file = kpi_log_file_path + "/KPI_Log.txt";
if (deviceType == "Tablet") {
timer = get_vitals_timer(adb_log_file, appPackage, WARM_APP_WARM);
} else if (deviceType == "FTV") {
timer = get_displayed_timer(adb_log_file, appPackage);
if (timer == 0) {
timer = get_vitals_timer(adb_log_file, appPackage, WARM_APP_WARM);
}
}
if (timer == 0) {
timer = get_vitals_timer(adb_log_file, appPackage, WARM_APP_COOL);
}
Runtime.getRuntime().exec("adb -s "+ DSN +" shell input keyevent KEYCODE_HOME");
Thread.sleep(10);
Runtime.getRuntime().exec("adb -s "+ DSN +" shell logcat -b all -c");
}
for (int i = 0; i < iterations; i++) {
if (!launchAppUsingMonkey(DSN, appPackage))
launchAppUsingIntent(DSN, appIntent)
Thread.sleep(10)
val read = BufferedReader(InputStreamReader(Runtime.getRuntime().exec("adb -s ${DSN} shell logcat -v threadtime -b all -d").getInputStream()))
val line_CoolApp = read.readLine()
val line_CoolActivity = read.readLine()
log_file_writer(read)
val adb_log_file = kpi_log_file_path + "/KPI_Log.txt"
when (deviceType) {
"Tablet" -> timer = get_vitals_timer(adb_log_file, appPackage, WARM_APP_WARM)
"FTV" -> timer = get_displayed_timer(adb_log_file, appPackage)
.takeIf { it != 0 } ?: get_vitals_timer(adb_log_file, appPackage, WARM_APP_WARM)
}
if (timer == 0) {
timer = get_vitals_timer(adb_log_file, appPackage, WARM_APP_COOL)
}
Runtime.getRuntime().exec("adb -s ${DSN} shell input keyevent KEYCODE_HOME")
Thread.sleep(10)
Runtime.getRuntime().exec("adb -s ${DSN} shell logcat -b all -c")
}
设备热启动Vitals日志
以下是设备热启动Vitals日志的示例。
03-03 12:51:16.367 892 1066 I Vlog : Thermal:ScreenOn_SensorThrottling_P2P_Off:fgtracking=false;DV;1,key=0;DV;1,Timer=8.072222222222222E-4;TI;1,unit=hours;DV;1,metadata=<应用程序包名称>!{"d"#{"groupId"#"123"$"schemaId"#"123"$"zone"#"soc"}};DV;1:HI
03-03 12:51:16.367 892 1066 I Vlog : Thermal:ScreenOn_SensorThrottling_P2P_Off:fgtracking=false;DV;1,key=0;DV;1,Timer=8.072222222222222E-4;TI;1,unit=hours;DV;1,metadata=<应用程序包名称>!{"d"#{"groupId"#"123"$"schemaId"#"123"$"zone"#"bottom"}};DV;1:HI
03-03 12:51:16.367 892 1066 I Vlog : Thermal:ScreenOn_SensorThrottling_P2P_Off:fgtracking=false;DV;1,key=0;DV;1,Timer=8.072222222222222E-4;TI;1,unit=hours;DV;1,metadata=<应用程序包名称>!{"d"#{"groupId"#"123"$"schemaId"#"123"$"zone"#"side"}};DV;1:HI
03-03 12:51:16.368 892 1066 I Vlog : Thermal:Screen_On_Thermal_Throttling_P2P_Off:fgtracking=false;DV;1,key=0;DV;1,Timer=8.075E-4;TI;1,unit=hours;DV;1,metadata=<应用程序包名称>!{"d"#{"groupId"#"123"$"schemaId"#"abc"}};DV;1:HI
03-03 12:51:16.384 892 1092 I Vlog : performance:warm_app_warm_transition_launch_time:fgtracking=false;DV;1,key=<应用程序包名称>;DV;1,**Timer****=****191.0**;TI;1,unit=ms;DV;1,metadata=<应用程序包名称>!{"d"#{"groupId"#"123"$"schemaId"#"abc"$"app_version"#"123"$"isChild"#"false"}$"m"#{"prev"#"<应用程序包名称>"$"context"#"1234"}};DV;1:HI
03-03 12:51:16.395 892 1092 I Vlog : performance:user_app_launch_warm:fgtracking=false;DV;1,key=<应用程序包名称>;DV;1,Counter=1;CT;1,unit=count;DV;1,metadata=<应用程序包名称>!{"d"#{"groupId"#"123"$"schemaId"#"abc"$"isChild"#"false"}};DV;1:HI
要确定第一帧时间,请参阅确定绘制完第一帧所需的时间。
准备好供使用 - 完全显示时间
准备好供使用 (RTU) KPI,即完全显示时间 (TTFD),用于衡量应用从启动到准备好供使用状态所用的时间。例如,准备好供使用状态可以是应用的登录或主页可用的状态。RTU指标可以帮助识别应用中的启动性能问题。通过在冷和热启动期间进行测量,该KPI旨在复现不同场景下的真实用户行为。
对于需要用户登录的应用,请衡量登录后用例的RTU KPI。
检测完全绘制状态
如果您在应用中实现了reportFullyDrawn() 方法,则可以使用以下ADB命令来检测完全绘制状态。
adb logcat | grep "Fully drawn APP_PACKAGE_NAME/LAUNCHER_ACTIVITY"
adb logcat | findstr "完全绘制APP_PACKAGE_NAME\LAUNCHER_ACTIVITY"
示例输出
ActivityManager: 完全绘制APP_PACKAGE_NAME/LAUNCHER_ACTIVITY +930ms(总计+849ms)
场景: RTU冷启动 - 完全显示时间
RTU冷启动是指应用进程停止或设备重启后,应用启动、完全绘制以及为用户交互做好准备所用的时间。测试冷启动时,您需要在应用被强制停止后启动应用,这模拟了首次使用或全新启动的场景。冷启动通常比热启动所需的时间更长,因为应用必须重新加载服务。
测试步骤
- 确保您已登录应用。
- 运行logcat清除命令。
String = "adb shell logcat -v threadtime -b all -c"
- 启动应用。
adb shell am start <应用程序包>/<应用主活动> - 运行logcat命令并捕获完全绘制的计时器值。
String = "adb shell logcat -v threadtime -b all -d"
- 在应用完全启动并捕获计时器值后,运行以下命令强制停止应用。
adb shell am force-stop <应用程序包> - 重复10次迭代的步骤。
场景: RTU热启动 - 完全显示时间
RTU热启动是指当应用进程已在后台运行时,应用启动、完全绘制以及为用户交互做好准备所用的时间。作为该启动活动的一部分,系统将应用从后台带到前台。测试热启动时,应在应用进入后台后启动该应用。热启动通常比冷启动更快,因为应用已经缓存了服务。
测试步骤
- 确保您已登录应用后,将该应用置于后台。
- 运行logcat清除命令。
String = "adb shell logcat -v threadtime -b all -c"
- 启动应用。
adb shell am start <应用程序包>/<应用主活动> - 运行logcat命令并捕获完全绘制的计时器值。
String = "adb shell logcat -v threadtime -b all -d"
- 应用完全启动后,运行命令将应用移至后台。
adb shell 输入 keyevent KEYCODE_HOME - 重复10次迭代的步骤。
要确定完全显示的时间,请参见检测完全绘制状态。
RTU ADB日志
以下是亚马逊Fire平板电脑设备上某款游戏的完全绘制活动日志示例。
**`10`****`-`****`06`****` `****`10`****`:`****`28`****`:`****`03.932`****` `****`678`****` `****`703`****` I `****`ActivityTaskManager`****`:`****` `****`完全`****` 绘制 `****`<应用程序包名称`****`>`****`:`****` `****`+`****`5s36ms`**
内存
内存KPI详细概述了应用的内存消耗。除了内存值之外,此KPI还可以衡量前台和后台CPU使用情况、RAM使用情况、RAM空闲情况和其他详情。通过在应用处于前台和后台时进行测量,该KPI旨在复现不同场景下的真实用户行为。
计算内存使用情况
以下ADB命令显示如何计算前台和后台用例的应用内存使用情况。
adb shell dumpsys meminfo | grep "APP PACKAGE"
adb shell dumpsys meminfo | findstr "APP PACKAGE"
示例输出
81,773K: APP PACKAGE (pid 21917 / activities)
场景: 前台内存
前台内存KPI捕获应用在前台时的内存消耗。要测量这一点,您需要打开应用并播放视频或玩游戏15分钟,然后计算应用的内存消耗。
测试步骤
- 下载、安装和登录应用(如果适用)。
- 运行logcat清除命令。
String = "adb shell logcat -v threadtime -b all -c"
- 启动应用。
adb shell am start <应用程序包>/<应用主活动> - 播放应用的核心内容(例如游戏或视频)15分钟。
- 运行
dumpsys命令并捕获total_pss内存值。adb shell dumpsys meminfo -a <应用程序包> - 捕获内存值后,运行命令以强制停止应用。
adb shell am force-stop <应用程序包> - 重复这些步骤,进行5次迭代。
场景: 后台内存
后台内存KPI捕获应用在后台时的内存消耗。要测量这一点,您需要打开应用并播放视频或玩游戏10分钟,将应用置于后台,然后计算应用的内存消耗。虽然没有定义后台内存消耗的阈值,但当系统内存 (RAM) 不足时,应用在后台使用的内存量是停止后台应用的决定性因素。当系统需要更多内存来完成前台任务和其他优先任务时,将首先停止后台内存消耗量最高的应用。
测试步骤
- 下载、安装和登录应用(如果适用)。
- 运行logcat清除命令。
String = "adb shell logcat -v threadtime -b all -c"
- 启动应用。
adb shell am start <应用程序包>/<应用主活动> - 播放应用的核心内容(例如游戏或视频)10分钟。
- 运行命令将应用移至后台。
adb shell 输入 keyevent KEYCODE_HOME - 等待10到15秒钟,让应用在后台稳定运行。
- 运行
dumpsys命令并捕获total_pss内存值。adb shell dumpsys meminfo-a <app_pkg> - 重复这些步骤,进行5次迭代。
内存ADB转储日志
按比例分配共享库占用的内存 (PSS) 是指应用在设备上消耗的内存量。总计PSS用于计算应用位于前台或后台时的内存消耗。
Pss Pss Shared Private Shared Private SwapPss Heap Heap Heap
Total Clean Dirty Dirty Clean Clean Dirty Size Alloc Free
Native Heap 115268 0 384 115020 100 208 22 151552 119143 32408
Dalvik Heap 15846 0 264 15124 140 676 11 21026 14882 6144
Dalvik Other 8864 0 40 8864 0 0 0
Stack 136 0 4 136 0 0 0
Ashmem 132 0 264 0 12 0 0
Other dev 48 0 156 0 0 48 0
.so mmap 15819 9796 656 596 26112 9796 20
.apk mmap 2103 432 0 0 26868 432 0
.dex mmap 39396 37468 0 4 17052 37468 0
.oat mmap 1592 452 0 0 13724 452 0
.art mmap 2699 304 808 1956 12044 304 0
Other mmap 274 0 12 4 636 244 0
GL mtrack 42152 0 0 42152 0 0 0
Unknown 2695 0 92 2684 60 0 0
总计 247077 48452 2680 186540 96748 49628 53 172578 134025 38552
编写一个KPI日志文件
使用以下示例代码编写一次迭代的KPI日志文件。
File和FileWriter对象,并记住不要关闭FileWriter对象。
public void log_file_writer(BufferedReader bf_read) {
File adb_log_file = new File(kpi_log_file_path + "/KPI_Log.txt");
FileWriter fileWriter = new FileWriter(adb_log_file);
try {
String reader = null;
while ((reader = bf_read.readLine()) != null) {
fileWriter.write(reader.trim() + "\n");
}
}
catch (Exception e) {
System.out.println(e);
}
finally {
fileWriter.flush();
fileWriter.close();
bf_read.close();
}
}
import java.io.File
import java.io.FileWriter
import java.io.BufferedReader
fun logFileWriter(bfRead: BufferedReader) {
val adbLogFile = File("$kpi_log_file_path/KPI_Log.txt")
val fileWriter = FileWriter(adbLogFile)
try {
var reader: String?
while (bfRead.readLine().also { reader = it } != null) {
fileWriter.write(reader?.trim() + "\n")
}
} catch (e: Exception) {
println(e)
} finally {
fileWriter.flush()
fileWriter.close()
bfRead.close()
}
}
相关主题
- 要了解有关ADB命令的更多信息,请参阅Android开发者文档中的Android调试桥 (adb)。
Last updated: 2026年1月30日

