Developer Console

How to measure and improve app startup times in Fire OS

Mayur Ahir Jun 06, 2023
Share:
How to App performance Best practices
Blog_Header_Post_Img

App launch performance is an important factor for building an engaging user experience. When an app is slow to startup, users grow frustrated and often abandon the product. In contrast, quick startup times are made possible through a few optimizations to your code while developing. In this article, we are sharing the best practices to measure and improve the performance of your app’s first launch experience.

Measuring app startup times

loading steps

By measuring startup and loading times, you can better understand launch performance from start to finish experience. To implement this instrumentation, first start by calling the available APIs in your Activity. Afterwards, check the logs for calculating the elapsed time between app launch and the completed display for app resources and view hierarchies. Simulating different app startup scenarios locally on your own device allows you to quickly test and measure the impact of your updates in realtime.

There are two datapoints to measure app startup times on Fire OS - “Time to Display First Frame” and “Time for Ready to Use” (RTU) time. We recommend you analyze and compare these times while optimizing the startup experience when users interact with the app. Here are the details for each of these measurements:

app start types

Time to Display First Frame is time taken by your app to launch until the first frame is displayed to the user. During this period, your app may draw its background, initiate navigation, load local assets, and render placeholders for the content loaded either locally or fetched from remote endpoint.

In Fire OS 5 (API level 22) and higher, logcat provides a value called Displayed representing the time elapsed between launching the process and the completion of drawing the first frame of the corresponding activity on the screen. This value represents your app’s Time to Display First Frame.

Time for Ready to Use (RTU) describes the time taken by your app to launch until it is fully drawn on the screen and is ready for the user to start interacting. At the RTU point your app has completed all rendering on the screen and content fetched from the remote endpoints is displayed. It's good practice to show a progress indicator to keep users' attention if there are noticeable delays when loading remote resources over the network.

For Fire TV and Android apps, “Lazy Loading” is when an app enables the initial drawing of the window, loads resources asynchronously in the background, and updates the view hierarchy. Consider the completed loading of all resources and display of views as a separate metric, and manually trigger reportFullyDrawn() to let the system know that your activity is finished with lazy loading. This value represents your app’s Ready to Use time. Note: In order to reliably report this metric, we recommended you review your app’s logs to identify which activities which should be factored in before triggering the reportFullyDrawn().

How to measure app startup times

app loading time

When you submit your Amazon Fire TV app or game to the Amazon Appstore, the app must pass a series of tests to qualify for publication. As you develop your app, use the time to start tests criteria from our docs as a guide for improving key performance indicators before submitting the app.

Note: For each of these measurements listed in this section, make sure to first ensure your app and it’s processes are not running in the background. You should also measure these values and force close the app several times in order to get an average value.

Cool start: Time to display first frame

Launch the app and measure the time taken to draw the First frame by observing the logs.

Copied to clipboard
ActivityManager: Displayed {package}/{activity}: +1s534ms
Cool start: Time for Ready to Use

Launch the app and measure the logs for how long until users can fully interact with the app.

Copied to clipboard
ActivityManager: Fully drawn {package}/{activity}: +4s54ms
Warm start: Time to display first frame

Launch the app, and press Home button from the remote control to put your app into background. From the Fire TV’s Recent apps row, select your app and re-launch to bring it in the foreground. Measure the time taken to draw the First frame by observing the logs.

Copied to clipboard
ActivityManager: Displayed {package}/{activity}: +1s534ms
Warm start: Time for Ready to Use

Launch the app, and press Home button from the remote control to put your app into background. From the Fire TV’s Recent apps row, select your app and re-launch to bring it in the foreground. Measure the time taken for the app to be fully loaded by observing the logs – this is the app screen where customers can interact with the app.

Copied to clipboard
ActivityManager: Fully drawn {package}/{activity}: +4s54ms
Best practices for implementing reportFullyDrawn()

You need to call Activity.reportFullyDrawn() since the system_server will only pick the timestamp where the first frame was drawn which is not the state a customer can already start using your app. When calling reportFullyDrawn(), system_server will register the timestamp at time of reception. This is closer to the state you want to log.

Keep in mind it can be difficult to identify the correct time to call reportFullyDrawn() depending on the architecture used. In general, we recommended to focus on the customer experience and not necessarily on the technical view. More specifically, you should call reportFullyDrawn() once the app is able to provide a meaningful experience for customers to start interacting e.g., placeholders elements are rendered while you still load assets in the background, possibly for parts of the screen that are currently not visible.

Implementing reportFullyDrawn() effectively can improve the user experience of an application. In addition, having a complete view on data handling, network activities, and multithreading are important factors that must be prioritized to support the overall user experience.

 

Approaching both “Cold” and “Warm” starts

During a cold start, the Activity#onCreate method is called. Override this method to initialise any Fire TV feature integration-specific libraries at the end of application creation step (e.g., Alexa VSK, ADM, etc). A detailed example for initializing the Alexa Client Library from your app's onCreate can be found on the Appstore Developer Docs.

A warm start is measured from the point where Activity#onCreate is called just before app’s view hierarchy is inflated to the point when Activity#onResume is called. It is a good practice to override your Activity#onResume method, and add reportFullyDrawn() call to track warm start time for ready to use as onCreate will not be called in some instances (e.g., when a user presses home button and returns to the application from Fire TV’s Recent apps row the activity is simply brought to the foreground and only onResume is called).

When using WebView, reportFullyDrawn() should be called after the UI of your remote web-app has fully loaded. Consider overriding WebViewClient#onPageFinished method in your application to ensure that Time for Ready to Use is reported only on page load and not for other user events like touch, button click, etc.

Note: It is recommend that you do an empirical analysis of application logs and identify actions performed during different activity lifecycle states to reliably trigger the reportFullyDrawn() at an appropriate stage.

WebView implementation example:

Copied to clipboard
// MainActivity.java
public class MainActivity extends Activity {

    ...
    private static final Uri mBaseUri = Uri.parse("https://mysampleapp.com");

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_main);

        if (savedInstanceState == null) {
            getSupportFragmentManager().beginTransaction()
                .replace(R.id.main_browse_fragment, new MainFragment())
                .commitNow();
        }

        WebView mWebView = findViewById(R.id.web_layout);
        mWebView.getSettings().setJavaScriptEnabled(true);
        mWebView.loadUrl(mBaseUri.toString());
        mWebView.setWebViewClient(new WebViewClient() {
            @Override
            public void onPageFinished(WebView view, String url) {
                super.onPageFinished(view, url);

                reportFullyDrawn();
            }
        });
    }
    
    ...
}

Improving app startup times

To ensure for a fast start and a fluid user experience without blocking scenarios, consider how your app is loading resources and what are the upfront impediments to quick launch experience. In addition, we have a related dev article on developing splash screens for Fire OS. Here are several tips to improve your app’s startup times:

Measure on device instead of emulator

It's important to test your app on a variety of devices and Fire OS versions to make sure that it starts up quickly and works as expected. This will help you identify any performance issues that may occur on specific devices. While an emulator is a good tool for checking quick functionality during the development, it should not be used for measuring performance, as capabilities of a physical device, such as the CPU, memory, and storage would play an important role in properly optimizing performance of your app.

Do not compare published app startup times with sideloaded apps

Measuring your app’s performance on a physical device when the app is not installed via Appstore will show differing results. For example, publishing on Amazon Appstore includes code injections to adapt to Appstore services which don’t apply to sideloaded apps.

To run accurate tests as a published app, use Amazon’s Live App Testing (LAT) service. By uploading your app to LAT, your app will experience the full suite of Amazon services in Amazon appstore’s production environment. Utilizing LAT will help ensure that your app works exactly as your customers will see the app published on Amazon Appstore.

Lazy loading, delaying intensive tasks, and profiling for your app’s startup

One of the biggest factors that affects app startup time is the size of your app's resources. The more resources your app uses, the longer it will take to start up. Try to use as few resources as possible, especially when your app is first starting up. This means using smaller images, less data, and fewer threads.

“Lazy loading” is a technique where your app only loads resources when they are required. If you don't need all of your libraries to be initialized when your app starts up, you can lazy load them or disable auto initialization. Use the Android Studio Profiler to identify which resources are taking the longest to load. This will help improve app startup times by reducing the amount of data loaded upfront.

It is advised to start with a simple, more static UI, which can be drawn fast e.g., using placeholders for views that you can use in place of real views. Only after you have called reportFullyDrawn(), you should activate and utilize demanding animations, intensive drawing operations, and UI-manipulations. Consider also displaying static data initially, to immerse the user into the application without delaying the application start any further. Background threads can help to improve app startup time by allowing you to perform tasks in the background while your app is loading. This can help reduce the time users have to wait for your app to become usable.

Consider a user-friendly data management

Data management is key to a fast application launch and needs a holistic approach on the application lifecycle: Think about which data you want to present to users on application launch to then store that data permanently after you have retrieved it.

Consider the speed of different storage approaches. For example, any form of database loaded within the application will likely reduce startup speeds compared to storing data using the built-in file-operations of the platform.

To address this concern, serialize the relevant data directly to the file system and on application launch. Define your data representation to implement the java.io.Serializable interface, and utilize Android’s ObjectOutputStream and ObjectInputStream classes as displayed in this example:

Copied to clipboard
public void serialize(Context context, String filename, Serializable object) throws Exception {
    FileOutputStream fOut = context.openFileOutput(filename, Context.MODE_PRIVATE);
    ObjectOutputStream oOut = new ObjectOutputStream(fOut);
    oOut.writeObject(object);
    oOut.close();
}

public Object deserialize(Context context, String filename) throws Exception {
    FileInputStream fIn = context.openFileInput(filename);
    ObjectInputStream oIn = new ObjectInputStream(fIn);
    Object result = oIn.readObject();
    oIn.close();

    return result;
}

This approach allows for fast serialization and deserialization of the data you want to initially present to the user, and could speed up the application launch significantly when comparing it to the use of additional frameworks.

Calculate average after multiple measurements

Optimizing the reportFullyDrawn() call requires careful analysis of what is going on at various application lifecycle stages. For this, Android provides performance tools and samples to capture macro-benchmark metrics. You should calculate the average after multiple measures to help understand if your app is trying to do too much on launch or trying to initialize too many resources.

To help with this improvement, we have included a sample script to automate benchmarking of your application. Please replace com.example.myapplication and .MainActivity with your actual package name and activity names respectively:

Copied to clipboard
// app_start_times.kts
import java.util.Scanner

private val CMD_LOGCAT = "adb logcat"
private val CMD_HOME = "adb shell input keyevent 3"
private val COOLDOWN_MILIS: Long = 1 * 1000

//APP PARAMETERS
private val PACKAGE_NAME = "com.example.myapplication"
private val ACTIVITY_NAME = "MainActivity"

//SCRIPT PARAMETERS
private var MEASURE_TIMES = 10
private var MEASURE_COLD_START = true
private var MEASURE_WARM_START = false

val logcatScanner = startAdbLogcat()

prepareApp(MEASURE_COLD_START, logcatScanner)
runMeasureLoop(MEASURE_COLD_START, MEASURE_TIMES, logcatScanner)

prepareApp(MEASURE_WARM_START, logcatScanner)
runMeasureLoop(MEASURE_WARM_START, MEASURE_TIMES, logcatScanner)

fun startAdbLogcat(): Scanner{
    val logcatProcess = Runtime.getRuntime().exec(CMD_LOGCAT)
    println("ADB Logcat Started")
    Thread.sleep(COOLDOWN_MILIS)
    val stream = logcatProcess.inputStream
    val scanner = Scanner(stream)
    scanner.useDelimiter("\n")
    return scanner
}

fun prepareApp(isWarmStart: Boolean, scanner: Scanner) {
    println("Step 1 - PREPARATION")
    val appStart = generateAppStartAdbCommand()
    val appStop = generateAppStopAdbCommand()
    val fullyDrawm = generateFullyDrawnLog()

    if(isWarmStart) {
        println("  - Opening app once and putting to background (for WARM START measure)")

        // To have proper WARM START benchmarks, open the app once,
        // wait until it is fully drawn and put it in the background
        Runtime.getRuntime().exec(appStart)
        waitForLog(fullyDrawm, scanner)
        Thread.sleep(COOLDOWN_MILIS)
        Runtime.getRuntime().exec(CMD_HOME)
    } else {
        println("  - Making sure that the app is closed (for COLD START measure)")
        Runtime.getRuntime().exec(appStop)
    }
}

fun runMeasureLoop(isWarmStart: Boolean, benchmarkTimes: Int, scanner: Scanner) {
    var totalMillis: Long = 0

    println("Step 2 - BENCHMARKING $benchmarkTimes TIMES")
    for(i in 1..benchmarkTimes) {
        println("  - Run $i")
        val benchmark = benchmarkApp(isWarmStart, scanner)
        totalMillis = totalMillis + benchmark
        Thread.sleep(COOLDOWN_MILIS)
    }

    println("Step 3 - RESULTS")
    println("  - Start type: ${if(isWarmStart) "WARM START" else "COLD START"}")
    println("  - Times benchmarked: $benchmarkTimes")
    println("  - Average start time: ${totalMillis/benchmarkTimes} milliseconds")
}

fun waitForLog(logToSearch: String, scanner: Scanner): String {
    while (scanner.hasNext()) {
        val str = scanner.next().trim()
        if(str.contains(logToSearch)) {
            return str
        }
    }

    return ""
}

fun benchmarkApp(isWarmStart: Boolean, scanner: Scanner): Long {
    val appStart = generateAppStartAdbCommand()
    val appStop = generateAppStopAdbCommand()
    val logToSearch = if(isWarmStart) generateFullyDrawnLog() else generateTimeToDisplayLog()

    Runtime.getRuntime().exec(appStart)
    val log = waitForLog(logToSearch, scanner)
    val millis = extractMiliseconds(log)

    Thread.sleep(COOLDOWN_MILIS)
    if(isWarmStart) {
        println("    * WARM START at $millis milliseconds")
        Runtime.getRuntime().exec(CMD_HOME)
    } else {
        println("    * COLD START at $millis milliseconds")
        Runtime.getRuntime().exec(appStop)
    }

    return millis
}

fun extractMiliseconds(logcat: String): Long {
    //1s234ms -> 1234 millis
    //345ms -> 345 millis
    var milis = logcat.split(" ").last().filter { it.isDigit() }
    return milis.toLong()
}

fun generateAppStartAdbCommand(): String {
    return "adb shell am start -n ${PACKAGE_NAME}/.${ACTIVITY_NAME}"
}

fun generateAppStopAdbCommand(): String {
    return "adb shell am force-stop ${PACKAGE_NAME}"
}

fun generateTimeToDisplayLog(): String {
    return "Displayed ${PACKAGE_NAME}/.${ACTIVITY_NAME}"
}

fun generateFullyDrawnLog(): String {
    return "Fully drawn ${PACKAGE_NAME}/.${ACTIVITY_NAME}"
}

Conclusion

Following these best practices will help you improve the startup time of your Fire TV app. By making your app more efficient and using the latest tools and techniques, you can make sure that your app is a great experience for users.

Related articles

Sign up for our newsletter

Stay up to date with the latest Amazon Developer news, industry trends and blog posts.