Open In App

Improve Android App Performance with Benchmarking

Last Updated : 06 Feb, 2022
Improve
Improve
Like Article
Like
Save
Share
Report

Aside from device hardware, the performance of an Android application is also determined by the algorithm that you have used in your application. The selection of the appropriate algorithm and data structures should always be your top priority. These are micro-optimizations that can be made in your app using these changes. So, in order to ensure that your application performs well across a wide range of devices, please ensure that your code is efficient at all levels, as this will optimize your performance. You should not write code that your application does not require. Also, if at all possible, avoid allocating memory. Some hints for writing efficient code include:

  1. Unnecessary Object: Avoid creating unnecessary objects because adding more objects to your app consumes more memory.
  2. Choose static over virtual: Using a static method instead of a virtual method can be 15–20 percent faster. So, when you don’t need to access an object’s fields, try to use that.
  3. Instead of using a normal for loop or other loops, use the enhanced for loop, i.e. the for-each loop, for collections that implement the Iterable interface and for arrays.
  4. Avoid using floating-point numbers: Avoid using floating-points because they are twice as slow as integers on Android devices.

So, those are some of the tips for improving code performance. But how can I compare the time taken previously with my old code to the time taken now with the improved code? Is there a way to evaluate the code’s performance? Is there a way to calculate how long it takes to run a piece of code? Yes, the solution is known as benchmarking. But, before we get to Benchmarking, let’s take a look at some rudimentary solutions for calculating the time it takes a chunk of code to run.

Kotlin




@Test
fun gfgMeasurment() {
    // jetpack geeksmanager library
    val geekser = TestListenableGeekserBuilder<MyGeekser>(context).build() 
      
    // for starting time of geeks
    val start = java.lang.System.nanoTime() 
      
    // do some geeks i.e. code to be measured
    geekser.doGeeks() 
      
    // time taken to complete the geeks
    val elapsed = (java.lang.System.nanoTime() - start) 
    Log.d("Geeks Management", "Time taken was $elapsed ns")
}


In this case, we have a COUNT number equal to 5, and we compute the time by taking the average of the five values. But why only five? Why can’t another number do it? There may also be outliers or other things running in the background, which will affect the measurement time. So, based on the preceding two examples, we can conclude that measuring code performance is extremely difficult because, in order to find the average time taken, we must first determine how many times the loop should be run, i.e. what the value of that COUNT variable should be. It is extremely difficult. Benchmarking refers to these code measurement steps. Let’s delve a little deeper into it.

What exactly is benchmarking?

Benchmarking is a process that is used to determine how quickly your phone can run something. The goal is to stress your phone enough so that you can find the best performance for your app on your phone. So far, we’ve seen how to find or measure the average time it takes a piece of code to run in a device in the previous section. However, there were a few issues with it. They are as follows:

  1. It is frequently inaccurate because we are measuring the incorrect thing at the incorrect time.
  2. It’s very unstable, which means that if we run the same code several times, we’ll get different results each time.
  3. If we take the average of all the values, how do we determine how many times that particular code will be executed before we take the result? You have no control over this.
  4. So, because Benchmark is so tricky, it’s difficult to say how much time we’re saving here. Is there a way to find out how long a piece of code takes to execute in real-time?

Jetpack Benchmark Library

At Google I/O’19, Android introduced the Jetpack Benchmark Library, which can be used to eliminate all errors or difficulties encountered while performing the Benchmark process inadvertently. The Jetpack Benchmark Library is a tool for measuring code performance and is used to eliminate the common errors that we were making previously while using Benchmark. This library handles warmup, measuring code performance, and displaying the results in the Android Studio console. The Benchmark code that we were measuring after 5 loops will now be converted to:

Kotlin




@get:Rule
val gfgValue = aBenchMark()
@Test
fun gfgGFGDB() {
    val gfgDB = Room.databaseBuilder(...).build()
    gfgDB.clearAndInsertTestData()
    benchmarkRule.measureRepeated {
        gfgDB.complexQuery()
    }
}


All you have to do is apply BenchmarkRule and then call an API, namely measureRepeated. Let’s take a look at another example. We’ll use databaseBenchmark as an example here. So, in this databaseBenchmark, we will first initialize the database, then clear all the tables and insert some testing data. Following that, we will create our code measuring loop to measure the code performance that we are interested in, such as a database query.

Kotlin




@get:Rule
val GeeksRule = GeeksRule()
@Test
fun databaseGeeks() {
    val gfgDB = Room.databaseBuilder(...).build()
    val pauseOffset = 0L
    GeeksRule.measureRepeated {
        val gfgStarter = java.lang.System.nanoTime()
        gfgDB.clearAndInsertTestData()
        pauseOffset += java.lang.System.nanoTime() - gfgStarter
        gfgDB.complexQuery()
    }
    Log.d("Geeks", "databaseGeeks_offset: $pauseOffset")
}


However, there is a problem with this code. If we do not change the values in the database, our query can be cached, and we will get a different result than the desired one. So, we can put db.clearAndInsertTestData() inside the loop, and by doing so in each loop, we can bust the cache and then measure the code that interests us. However, because we have one more statement in the loop, we are measuring more than what is required. To remove this, we can use the previous method, which is to count or find the time taken by db.clearAndInsertTestData() and then subtract this result from the final output.

Android Studio integration

Because the Benchmark Library is still in the alpha stage, we must download Android Studio version 3.5 or later. In our project, we love modularization, and by default, we have an app module and a lib module. To use the Benchmark library, you must first enable the Benchmark module and then the Android Studio Benchmarking template. 

The steps to enable the benchmark module in Android Studio are as follows:

  1. Install the latest version of Android Studio (3.5 or higher).
  2. Click Help > Edit Custom Properties in Android Studio.
  3. Insert the following code: npw.benchmark.template.module=true
  4. Save and exit the file.
  5. Relaunch Android Studio.

We can now create a Benchmark module in addition to the app and lib modules. The Benchmark module template will automatically configure Benchmarking settings. To make a Benchmark module, perform the following steps:

Configuration as a benchmark

Because we are creating a module for Benchmark, we only have one build.gradle file. The benchmark module is the same. Benchmark’s build.gradle file contains the following lines:

  1. Plugin for benchmarking: When you run gradlew, it will assist you in retrieving your benchmark reports.
  2. Runner made to order: A custom runner, such as the AndroidbenchmarkRunner, will aid in the stabilization of your benchmark.
  3. Pre-programmed proguard rules: This makes your code more efficient.
  4. The library on its own (alpha version, as of now)

Kotlin




apply plugin: 'com.android.library'
apply plugin: 'androidx.benchmark'
  
android {
    defaultConfig {
        testInstrumentationRunner "androidx.benchmark.junit4.AndroidBenchmarkRunner"
    }
  
    buildTypes {
        debug {
            debuggable true
            minifyEnabled true
            proguardFiles getDefaultProguardFile(
                    'proguard-android-optimize.txt'),
                    'benchmark-proguard-rules.pro'
        }
    }
}
}


So we’ve finished benchmarking. But here’s a question: can we use the Jetpack Benchmark Library in all situations, i.e. does the library produce the same results in all cases? No, it does not. But don’t worry; the library will notify you if something is interfering with the benchmark. Yes, there are some warnings in the benchmark library. So, the next time your debuggable is set to true, or you’re using an emulator, or you’re missing a runner, or you’re running low on battery, the benchmark will give you a warning, and you can look out for it and fix it before benchmarking.

Benchmarking Influencing Factors

Performing a benchmark task is not as simple as it appears. It has many foes, the most dangerous of which is the CPU clock. The CPU clock is crucial to system stability. To be more specific, the CPU clock problem is divided into two problems, namely

  • Ramping
  • Throttling

We all know that when the device is in an ideal state, i.e. no work is assigned, the clock is low, and when a task is assigned to it, the clock begins ramping up to high-performance mode. As a result, clocks will generally increase when under load. So, if we measure in these two scenarios, we are likely to get the wrong output for the code measurement because the clocks are different.

The solution to this problem is straightforward, and it is included in the definition of benchmarking. The Benchmark library performs a warmup to stabilize this, which means that the benchmarking loop will run for a minimum of 250ms before the measurement is completed. As a result, if the clock

Throttling or Diving

When your device is working hard, it becomes hot and the clock rapidly drops. In general, when the device is hot, the CPU lowers the clock to protect the chips. Thermal throttling is another term for this. As a result, this can have a significant impact on benchmark performance because the clock can be very high one moment and very low the next. An example of Thermal Throttling (red line) is as follows:

Kotlin




// AndroidBenchmarkRunner.kt
override fun onCreate(arguments: Bundle) {
    super.onCreate(arguments)
  
    if(sustainedPerformanceModeInUse) {
        gfgMainThread(name = "BenchSpinGfgMainThread") {
            Process.setGfgMainThreadPriority(Process.GFGMAINTHREAD_PRIORITY_LOWEST)
            while(true) {}
        }
    }
}


We must force the device into the multi-threaded mode for the Single and Multithreaded problem because you can only use one core at a maximum clock or multiple cores at a lower clock. However, switching modes can lead to inconsistency. As a result, we should force the device to run in multi-threaded mode. To accomplish the same thing, while our AndroidBenchmarkRunner is in sustained performance mode, we create a new thread that will spin. This is the most effective way to force the multi-threaded mode. So far, we’ve seen two methods:

  • Clock Locking
  • Sustained Preference

However, neither of these methods can be used in the normal case because not all devices are rooted, and not all devices support Sustained Performance Mode. We require a specific and broad solution. So, the final solution for throttling is thread sleeping, which is also the simplest solution. 

We detect a slowdown in the Thread.sleep() method by running a tiny-mini benchmark in between each benchmark to see if the device has begun thermal throttling or not. If we detect thermal throttling, we discard the current benchmark data and go to sleep to allow the device to cool down.

Kotlin




@get:Rule
val gfgRule = aBenchmarkRule()
@Test
fun someBench() {
    val gfgFile = File(path)
    benchmarkRule.measureRepeated {
        gfgFile.exists()
    }
}


Conclusion

We’ve seen that benchmarking is a very complex problem, and it’s difficult to measure code performance normally. It is determined by the Clock Stability. So, to address these issues, we have created the Jetpack Benchmark Library API, which can measure code performance for you. Also, keep in mind that you should not compare devices with JetPack Benchmark. In this case, we’re comparing code written for the same device and OS version. Benchmark can be found on the Benchmark website, and benchmark code samples can be found on the Github page.



Like Article
Suggest improvement
Previous
Next
Share your thoughts in the comments

Similar Reads