Writing your own profiler to analyze the performance of an application on Android

As the application develops, it is worth auditing it to detect implicit performance degradations. I recently audited the iFunny comment section and wrote my own profiler. It will not replace the existing Android Profile tools on the market from Android Studio, Battery Historian and Systrace, but it has a number of advantages:









  1. .





  2. .





, , .





, , .





1. Android Profiler. Android Studio, CPU, Memory, Network Energy . , .





:





  • .





  • .





  • , .





:





  • - , , .





  • .





  • . , heap , , Android Profiler .





2. Systrace. , .





3. Battery Historian. , , , , .





Android Profiler, . . , , Excel Numbers.





Espresso.





CPU

, Android Linux, , .





, /proc/<PID>/stat , PID android.os.Process.myPid(). Android O /proc, top -n 1, -n - . , CPU .





class CpuUsageExporter(context: Context) : AppMetricExporter {
  
  private companion object {
    const val CPU_USAGE_FILENAME = "cpu_usage.txt"
    const val PACKAGE_NAME = "com.example.app"
  }
  
  private val cpuPw = PrintWriter(FileOutputStream(File(context.filesDir, CPU_USAGE_FILENAME), true), true)
  
  override fun export() {
    try {
      recordCpu()
    } catch (th: Throwable) {
      Assert.fail(th)
    }
  }
	
  override fun close() {
    cpuPw.close()
  }
	
  private fun recordCpu() {
    val processLine = readSystemFile("top", "-n", "1").filter { it.contains(PACKAGE_NAME) }
      .flatMap { it.split(" ") }
      .map(String::trim)
      .filter(String::isNotEmpty)
    if (processLine.isNotEmpty()) {
      val index = processLine.indexOfFirst { it == "S" || it == "R" || it == "D" }
      check(index > -1) {
        "Not found process state of $PACKAGE_NAME"
      }
      
		cpuPw.println(processLine[index + 1].toFloat().toInt().toString())
    }
  }
	
  @Throws(java.lang.Exception::class)
  private fun readSystemFile(vararg pSystemFile: String): List<String> {
    return Runtime.getRuntime()
      .exec(pSystemFile).inputStream.bufferedReader()
      .useLines {
        it.toList()
      }
  }
}
      
      



recordCpu() try-catch, . cpu_usage.txt, .





Memory

, Debug Runtime, . heap 90%, . , . Android Profiler Android Studio (Profiler → + → Load from file). , .





class MemoryUsageExporter(context: Context) : AppMetricExporter {
	
  private companion object {
    const val MEM_USAGE_FILENAME = "mem_usage.txt"
    const val CRITICAL_MEMORY_LOADING = 0.9
  }
	
  private val absolutePath = context.filesDir.absolutePath
  private val memPw = PrintWriter(FileOutputStream(File(context.filesDir, MEM_USAGE_FILENAME), true), true)
	
  override fun export() {
    val runtime = Runtime.getRuntime()
    
    val maxHeapSizeInMB = InformationUnit.BYTE.toMB(runtime.maxMemory())
    val availHeapSizeInMB = InformationUnit.BYTE.toMB(runtime.freeMemory())
    val usedHeapSizeInMB = InformationUnit.BYTE.toMB((runtime.totalMemory() - runtime.freeMemory()))
		
    val totalNativeMemorySize = InformationUnit.BYTE.toMB(Debug.getNativeHeapSize())
    val availNativeMemoryFreeSize = InformationUnit.BYTE.toMB(Debug.getNativeHeapFreeSize())
    val usedNativeMemoryInMb = InformationUnit.BYTE.toMB(totalNativeMemorySize - availNativeMemoryFreeSize)
		
    if (usedHeapSizeInMB > maxHeapSizeInMB * CRITICAL_MEMORY_LOADING) {
      Debug.dumpHprofData("$absolutePath/dump_heap_memory_${System.currentTimeMillis()}.hprof")
    }
		
    val str =
    	"$usedHeapSizeInMB $availHeapSizeInMB $maxHeapSizeInMB $usedNativeMemoryInMb $availNativeMemoryFreeSize $totalNativeMemorySize"
    memPw.println(str)
  }
  
  override fun close() {
    memPw.close()
  }
}
      
      



, mem_usage.txt , - .





Network

android.net.TrafficStats, . . network_usage.txt.





class NetworkUsageExporter(context: Context) : AppMetricExporter {
  
  private companion object {
    const val NETWORK_USAGE_FILENAME = "network_usage.txt"
  }
  
  private val networkPw = PrintWriter(FileOutputStream(File(context.filesDir, NETWORK_USAGE_FILENAME), true), true)
  
  private var transmittedBytes = 0L
  private var receivedBytes = 0L
  
  override fun export() {
    val tBytes = TrafficStats.getTotalTxBytes()
    val rBytes = TrafficStats.getTotalRxBytes()
    
    if (tBytes.toInt() == TrafficStats.UNSUPPORTED || rBytes.toInt() == TrafficStats.UNSUPPORTED) {
      throw RuntimeException("Device not support network monitoring")
    } else if (transmittedBytes > 0 && receivedBytes > 0) {
      networkPw.println("${tBytes - transmittedBytes} ${rBytes - receivedBytes}")
    }
    
    transmittedBytes = tBytes
    receivedBytes = rBytes
  }
  
  override fun close() {
    networkPw.close()
  }
}
      
      



Battery

. BatteryManager sticky Intent, , registerReceiver IntentFilter c action Intent.ACTION_BATTERY_CHANGED (, , , ).





.





class BatteryUsageExporter(private val context: Context) : AppMetricExporter {
  
  private companion object {
    const val BATTERY_USAGE_FILENAME = "battery_usage.txt"
    const val INVALIDATE_VALUE = -1
  }
  
  private val batteryPw = PrintWriter(FileOutputStream(File(context.filesDir, BATTERY_USAGE_FILENAME), true), true)
  private val intentFilter = IntentFilter(Intent.ACTION_BATTERY_CHANGED)
  
  override fun export() {
    val batteryStatus: Intent? = intentFilter.let { ifilter -> context.registerReceiver(null, ifilter) }
    val status: Int = batteryStatus?.getIntExtra(BatteryManager.EXTRA_LEVEL, INVALIDATE_VALUE) ?: INVALIDATE_VALUE
    if (status != INVALIDATE_VALUE) {
      batteryPw.println("$status")
    }
  }
  
  override fun close() {
    batteryPw.close()
  }
}
      
      



AppMetricUsageManager. , . :





class AppMetricUsageManager(context: Context) {
  
  private companion object {
    const val INTERVAL_TIME_IN_SEC = 1L
    const val INITIAL_DELAY = 0L
  }
  
  private val exporters = listOf(CpuUsageExporter(context),
                                 MemoryUsageExporter(context),
                                 BatteryUsageExporter(context),
                                 NetworkUsageExporter(context))
  
  private var disposable: Disposable? = null
  
  fun startCollect() {
    disposable = Observable.interval(INITIAL_DELAY, INTERVAL_TIME_IN_SEC, TimeUnit.SECONDS)
      .subscribe({ exporters.forEach { it.export() } }, { th -> Timber.e(th) })
  }
  
  fun stopCollect() {
    exporters.forEach { it.close() }
    disposable.safeDispose()
    disposable = null
  }
}
      
      



safeDispose() — , dispose, Observable null .









. , ?





, Android Profiler — , , . Android Profiler, Systrace Battery Historian, .








All Articles