as

Settings
Sign out
Notifications
Alexa
亚马逊应用商店
AWS
文档
Support
Contact Us
My Cases
开发
测试
应用发布
盈利
用户参与
设备规格
资源

Fire平板电脑的应用性能脚本

Fire平板电脑的应用性能脚本

“性能测试”是亚马逊Fire OS设备上的应用测试过程,测试领域包括兼容性、可靠性、速度、响应时间、稳定性和资源使用等。您可以使用此测试来识别和解决应用的性能瓶颈。性能测试涉及收集和评估关键性能指标 (KPI)。要收集KPI指标,您需要在亚马逊设备上运行一组特定的步骤,然后使用日志等设备资源查找或计算指标。

在将您的应用提交到亚马逊应用商店之前,请务必运行性能测试。本页提供测试不同类别KPI的步骤,并包含可在自动化中使用的示例代码。本指南涵盖以下KPI:

设置

要开始使用,请在开发计算机上安装以下软件程序包:

除了安装这些软件程序包之外,还需要完成以下操作:

  • 为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冷启动是指应用进程停止或设备重启后,应用启动并显示其第一帧所需的时间。测试冷启动时,您需要在应用被强制停止后启动应用,这模拟了首次使用或全新启动的场景。冷启动通常比热启动所需的时间更长,因为应用必须重新加载服务。

测量从冷启动开始到第一帧的时间

  1. 运行logcat清除命令。
    String = "adb shell logcat -v threadtime -b all -c"
    
  2. 下载、启动和登录应用。
     adb shell am start <应用程序包>/<应用主活动>
    
  3. 运行logcat命令。
    String = "adb shell logcat -v threadtime -b all -d"
    
  4. 通过查找包含以下字符串的日志行来捕获计时器值。
     cool_app_launch_time
    
  5. 应用完全启动后,强制停止该应用。
    adb shell 我强制停止 <app_pkg>
    
  6. 重复这些步骤,进行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是指当应用进程已在后台运行时,应用启动并显示其第一帧所用的时间。作为该启动活动的一部分,系统将应用从后台带到前台。测试热启动时,应在应用进入后台后启动该应用。热启动通常比冷启动更快,因为应用已经缓存了服务。

测量热启动的第一帧时间

  1. 请确保应用在后台运行。
  2. 运行logcat清除命令。
    String = "adb shell logcat -v threadtime -b all -c"
    
  3. 启动并登录应用。
     adb shell am start <应用程序包>/<应用主活动>
    
  4. 运行logcat命令。
    String = "adb shell logcat -v threadtime -b all -d" 
    
  5. 通过查找包含以下字符串的日志行来捕获计时器值。
     warm_app_warm_transition_launch_time
    
  6. 应用完全启动后,运行以下命令。
    adb shell 输入 keyevent KEYCODE_HOME
    
  7. 重复这些步骤,进行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冷启动是指应用进程停止或设备重启后,应用启动、完全绘制以及为用户交互做好准备所用的时间。测试冷启动时,您需要在应用被强制停止后启动应用,这模拟了首次使用或全新启动的场景。冷启动通常比热启动所需的时间更长,因为应用必须重新加载服务。

测试步骤

  1. 确保您已登录应用。
  2. 运行logcat清除命令。
    String = "adb shell logcat -v threadtime -b all -c"
    
  3. 启动应用。
     adb shell am start <应用程序包>/<应用主活动>
    
  4. 运行logcat命令并捕获完全绘制的计时器值。
    String = "adb shell logcat -v threadtime -b all -d"
    
  5. 在应用完全启动并捕获计时器值后,运行以下命令强制停止应用。
     adb shell am force-stop <应用程序包>
    
  6. 重复10次迭代的步骤。

场景: RTU热启动 - 完全显示时间

RTU热启动是指当应用进程已在后台运行时,应用启动、完全绘制以及为用户交互做好准备所用的时间。作为该启动活动的一部分,系统将应用从后台带到前台。测试热启动时,应在应用进入后台后启动该应用。热启动通常比冷启动更快,因为应用已经缓存了服务。

测试步骤

  1. 确保您已登录应用后,将该应用置于后台。
  2. 运行logcat清除命令。
    String = "adb shell logcat -v threadtime -b all -c"
    
  3. 启动应用。
     adb shell am start <应用程序包>/<应用主活动>
    
  4. 运行logcat命令并捕获完全绘制的计时器值。
    String = "adb shell logcat -v threadtime -b all -d"
    
  5. 应用完全启动后,运行命令将应用移至后台。
    adb shell 输入 keyevent KEYCODE_HOME
    
  6. 重复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分钟,然后计算应用的内存消耗。

测试步骤

  1. 下载、安装和登录应用(如果适用)。
  2. 运行logcat清除命令。
    String = "adb shell logcat -v threadtime -b all -c"
    
  3. 启动应用。
     adb shell am start <应用程序包>/<应用主活动>
    
  4. 播放应用的核心内容(例如游戏或视频)15分钟。
  5. 运行dumpsys命令并捕获total_pss内存值。
     adb shell  dumpsys meminfo -a <应用程序包>
    
  6. 捕获内存值后,运行命令以强制停止应用。
     adb  shell am force-stop <应用程序包>
    
  7. 重复这些步骤,进行5次迭代。

场景: 后台内存

后台内存KPI捕获应用在后台时的内存消耗。要测量这一点,您需要打开应用并播放视频或玩游戏10分钟,将应用置于后台,然后计算应用的内存消耗。虽然没有定义后台内存消耗的阈值,但当系统内存 (RAM) 不足时,应用在后台使用的内存量是停止后台应用的决定性因素。当系统需要更多内存来完成前台任务和其他优先任务时,将首先停止后台内存消耗量最高的应用。

测试步骤

  1. 下载、安装和登录应用(如果适用)。
  2. 运行logcat清除命令。
    String = "adb shell logcat -v threadtime -b all -c"
    
  3. 启动应用。
     adb shell am start <应用程序包>/<应用主活动>
    
  4. 播放应用的核心内容(例如游戏或视频)10分钟。
  5. 运行命令将应用移至后台。
    adb shell 输入 keyevent KEYCODE_HOME
    
  6. 等待10到15秒钟,让应用在后台稳定运行。
  7. 运行dumpsys命令并捕获total_pss内存值。
    adb shell dumpsys meminfo-a  <app_pkg>
    
  8. 重复这些步骤,进行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日志文件。

已复制到剪贴板。

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()
    }
}

Last updated: 2026年1月30日