As you build your app for Amazon Fire devices, getting your features just right is definitely important. Building a great app also means creating a smooth and reliable experience that your users will enjoy. Crashes, lags, and janky UI interactions will lead to bad reviews and even uninstalls. No developer wants that.
So addressing performance issues and keeping your app stable are incredibly important parts of your app developer mission. They’re key to keeping users happy and ensuring your app succeeds. Thankfully, with tools from the Amazon Developer Console like the App Health Insights dashboard, it’s easier to spot problems and make the right fixes.
This guide will walk you through how to understand the dashboard and what to look for. You’ll find many tips to improve your app’s performance and stability. Are you ready to make sure your app shines on every Fire device? Then let’s get started!
The App Health Insights dashboard is a powerful tool to help you understand how your app is performing. It provides key metrics and actionable insights so you can focus on fixing what matters most.
To access it, you’ll need a live app. To get started:
The dashboard is divided into two main sections: the performance dashboard and the stability dashboard. Each section focuses on a specific aspect of your app’s health.
With this data, you don’t have to guess what’s going wrong—you can see it clearly and address it right away.
For developers, time is precious. The dashboard saves you from digging through endless logs or waiting for user complaints to roll in. Instead, it highlights the most critical problems in your app, letting you prioritize fixes that will have the biggest impact on your users’ experience. By tackling these issues early, you can reduce negative reviews and improve retention.
The performance dashboard helps you measure your app’s efficiency. By focusing on a few key metrics, you can identify and address bottlenecks that impact the user experience.
App latency metrics track two key aspects of how your app launches: app launch time and ready-to-use time.
Both metrics are critical because they influence the user’s perception of speed and responsiveness. Exceeding these benchmarks can make the app feel slow and lead to user frustration.
To reduce app launch time and ready-to-use time, here are some things you can do:
Improve data management efficiency: Use file system operations instead of databases for initial data loading, which can help speed up application launch significantly. Consider serializing relevant data directly to the file system for faster retrieval. The following example implements fast serialization and deserialization of the data to be presented to the user initially:
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;
}
private val executor = Executors.newSingleThreadExecutor()
fun <T : Serializable> serialize(context: Context, filename: String, data: T) {
executor.execute {
try {
context.openFileOutput(filename, Context.MODE_PRIVATE).use { fileOut ->
ObjectOutputStream(fileOut).use { objOut ->
objOut.writeObject(data)
}
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}
fun <T : Serializable> deserialize(context: Context, filename: String, callback: (T?) -> Unit) {
executor.execute {
val result: T? = try {
context.openFileInput(filename).use { fileIn ->
ObjectInputStream(fileIn).use { objIn ->
@Suppress("UNCHECKED_CAST")
objIn.readObject() as? T
}
}
} catch (e: Exception) {
e.printStackTrace()
null
}
callback(result)
}
}
As an alternative to the traditional serialization method shown above, consider the use of Flatbuffers for a flexible and memory efficient approach.
For more details and code examples on these recommendations, check out How to measure and improve app startup times in Fire OS.
Foreground Low Memory Killer Events (LME) for memory usage
The Foreground Low Memory Killer Events (LME) metric tracks the average daily instances of foreground low memory events that kill the app on the device. A foreground LME occurs when the system is critically short on memory, even after terminating all non-persistent background apps or services. This metric helps you understand how frequently your app is experiencing severe memory constraints that result in forcibly closing the app.
Minimizing these events depends on healthy memory usage for your app. Of course, this depends on your app’s complexity. However, as a basic benchmark, strive to limit foreground memory consumption to less than 1000 MB for gaming apps and less than 600 MB for non-gaming apps. Excessive memory usage that triggers frequent LMEs can lead to app instability or crashes, particularly on devices with limited resources.
For a comprehensive guide on managing your app's memory, see the Android Developers page on reducing app memory usage. The following strategies capture key takeaways from that resource, helping you reduce memory usage and improve your app's performance:
public class MainActivity extends AppCompatActivity
implements ComponentCallbacks2 {
// Other activity code.
/**
* Release memory when the UI becomes hidden or when system
* resources become low.
* @param level the memory-related event that is raised.
*/
public void onTrimMemory(int level) {
if (level >= ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN) {
// Release memory related to UI elements, such as bitmap caches.
}
if (level >= ComponentCallbacks2.TRIM_MEMORY_BACKGROUND) {
// Release memory related to background processing, such as by
// closing a database connection.
}
}
}
import android.app.Activity
import android.content.ComponentCallbacks2
import android.content.res.Configuration
import android.os.Bundle
import android.util.Log
class MainActivity : Activity(), ComponentCallbacks2 {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
registerComponentCallbacks(this)
}
override fun onTrimMemory(level: Int) {
when {
level >= ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN -> {
// Release memory related to UI elements, such as bitmap caches
Log.d("MemoryTrim", "UI is hidden. Freeing up UI-related resources.")
}
level >= ComponentCallbacks2.TRIM_MEMORY_BACKGROUND -> {
// Release memory related to background processing, such as closing a database connection
Log.d("MemoryTrim", "App in background. Releasing background resources.")
}
}
}
override fun onLowMemory() {
// Handle extreme low-memory situations
Log.w("MemoryTrim", "System is running low on memory!")
}
override fun onConfigurationChanged(newConfig: Configuration) {
// Required override but not used for memory management
}
override fun onDestroy() {
super.onDestroy()
unregisterComponentCallbacks(this)
}
}
public void doSomethingMemoryIntensive() {
// Before doing something that requires a lot of memory,
// check whether the device is in a low memory state.
ActivityManager.MemoryInfo memoryInfo = getAvailableMemory();
if (!memoryInfo.lowMemory) {
// Do memory intensive work.
}
}
// Get a MemoryInfo object for the device's current memory status.
private ActivityManager.MemoryInfo getAvailableMemory() {
ActivityManager activityManager =
(ActivityManager) this.getSystemService(ACTIVITY_SERVICE);
ActivityManager.MemoryInfo memoryInfo = new ActivityManager.MemoryInfo();
activityManager.getMemoryInfo(memoryInfo);
return memoryInfo;
}
import android.content.Context
fun Context.isMemoryLow(): Boolean {
val activityManager = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
val memoryInfo = ActivityManager.MemoryInfo()
activityManager.getMemoryInfo(memoryInfo)
return memoryInfo.lowMemory
}
fun doSomethingMemoryIntensive(context: Context) {
if (!context.isMemoryLow()) {
// Perform memory-intensive operation
}
}
Fluidity measures the smoothness of animations and transitions by tracking frame drop rates. This metric highlights how often frames are skipped during rendering, which can make your app feel choppy and less responsive.
The Test Criteria for Amazon Appstore Apps states:
For a smooth experience, apps should aim for a frame drop rate of less than 5%. Rates exceeding 10% can significantly degrade the user experience, making the app appear laggy and unpolished.
To improve fluidity, simplify complex animations and reduce the number of objects rendering simultaneously. Optimize rendering tasks by ensuring efficient GPU usage and minimizing unnecessary computations during transitions. Tools like Jank Detection in the Android Studio Performance Profiler and Profile GPU Rendering tool can help pinpoint and resolve rendering issues effectively.
The stability dashboard helps you identify and resolve issues that could cause your app to crash, hang, or otherwise frustrate users. By monitoring a few key metrics, you can proactively improve your app’s reliability and user experience.
Crash rate measures how often your app crashes relative to the number of user sessions. This metric is critical because frequent crashes can quickly lead to poor reviews and uninstalls.
For a healthy app, crash rates should remain below 1%. Apps with crash rates exceeding this benchmark risk alienating users and eroding trust. To reduce crash rates, developers should implement the following strategies recommended to Android developers:
By systematically addressing these areas, developers can significantly reduce crash rates and improve overall app stability on Fire tablets.
ANRs occur when your app’s main thread is unresponsive for too long, causing the system to notify users that the app isn’t working. This often leads to users force-quitting your app.
Healthy apps should aim to have an ANR rate of less than 0.47%. Higher rates indicate your app may have performance bottlenecks or blocking operations on the main thread.
To address ANRs, implement these recommended fixes:
// Bad example: Performing time-consuming task on the main thread
public void onClick(View v) {
// This will block the UI and potentially cause an ANR
Bitmap bitmap = processBitMap("image.png");
imageView.setImageBitmap(bitmap);
}
// Good example: Moving time-consuming task to a background thread
public void onClick(View v) {
new Thread(new Runnable() {
public void run() {
// A potentially time-consuming task performed off the main thread
final Bitmap bitmap = processBitMap("image.png");
// Use post() to update UI safely from the main thread
imageView.post(new Runnable() {
public void run() {
imageView.setImageBitmap(bitmap);
}
});
}
}).start();
}
import android.graphics.Bitmap
import android.os.Handler
import android.os.Looper
import android.view.View
import android.widget.ImageView
import java.util.concurrent.Executors
class MyActivity {
private val executor = Executors.newSingleThreadExecutor()
private val mainHandler = Handler(Looper.getMainLooper())
fun onClick(view: View, imageView: ImageView) {
executor.execute {
// Perform time-consuming task in background thread
val bitmap = processBitmap("image.png")
// Update UI safely on the main thread
mainHandler.post {
imageView.setImageBitmap(bitmap)
}
}
}
private fun processBitmap(filename: String): Bitmap {
// Simulate bitmap processing (e.g., decoding an image)
Thread.sleep(2000) // Simulate delay
return Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888)
}
}
Beyond the performance and stability dashboards, there are additional factors to keep in mind to ensure your app delivers the best experience possible:
Creating a high-quality app for Fire devices means paying attention to performance and stability. With the App Health Insights dashboard, developers can focus on key metrics to ensure they’re delivering an app with a smooth and reliable experience that keeps users coming back.
Interested in diving deeper? The following resources will help you on your journey toward continuous improvement of your app quality: