main

square/leakcanary

Last updated at: 29/12/2023 09:46

ProcessInfo.kt

TLDR

The ProcessInfo file defines an interface and a concrete implementation of the interface for retrieving information about the current process and device resources.

Methods

availableDiskSpaceBytes

This method takes a File object representing a path and returns the available disk space in bytes.

availableRam

This method takes a Context object representing the application context and returns the available RAM on the device. The return value can be one of the following:

  • LowRamDevice: Indicating that the device is a low RAM device.
  • BelowThreshold: Indicating that the available RAM is below a certain threshold.
  • Memory: Indicating the available RAM in bytes.

Classes

ProcessInfo.Real

This class is the concrete implementation of the ProcessInfo interface. It provides implementations for all the methods defined in the interface. It uses various Android system APIs and utilities to retrieve information about the current process and device resources.

package leakcanary

import android.annotation.SuppressLint
import android.app.ActivityManager
import android.app.ActivityManager.MemoryInfo
import android.app.ActivityManager.RunningAppProcessInfo
import android.content.Context
import android.os.Build.VERSION.SDK_INT
import android.os.Process
import android.os.SystemClock
import android.system.Os
import android.system.OsConstants
import java.io.File
import java.io.FileReader
import leakcanary.ProcessInfo.AvailableRam.BelowThreshold
import leakcanary.ProcessInfo.AvailableRam.LowRamDevice
import leakcanary.ProcessInfo.AvailableRam.Memory

interface ProcessInfo {

  val isImportanceBackground: Boolean

  val elapsedMillisSinceStart: Long

  fun availableDiskSpaceBytes(path: File): Long

  sealed class AvailableRam {
    object LowRamDevice : AvailableRam()
    object BelowThreshold : AvailableRam()
    class Memory(val bytes: Long) : AvailableRam()
  }

  fun availableRam(context: Context): AvailableRam

  @SuppressLint("NewApi")
  object Real : ProcessInfo {
    private val memoryOutState = RunningAppProcessInfo()
    private val memoryInfo = MemoryInfo()

    private val processStartUptimeMillis by lazy {
      Process.getStartUptimeMillis()
    }

    private val processForkRealtimeMillis by lazy {
      readProcessForkRealtimeMillis()
    }

    override val isImportanceBackground: Boolean
      get() {
        ActivityManager.getMyMemoryState(memoryOutState)
        return memoryOutState.importance >= RunningAppProcessInfo.IMPORTANCE_BACKGROUND
      }

    override val elapsedMillisSinceStart: Long
      get() = if (SDK_INT >= 24) {
        SystemClock.uptimeMillis() - processStartUptimeMillis
      } else {
        SystemClock.elapsedRealtime() - processForkRealtimeMillis
      }

    @SuppressLint("UsableSpace")
    override fun availableDiskSpaceBytes(path: File) = path.usableSpace

    override fun availableRam(context: Context): AvailableRam {
      val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager

      if (SDK_INT >= 19 && activityManager.isLowRamDevice) {
        return LowRamDevice
      } else {
        activityManager.getMemoryInfo(memoryInfo)

        return if (memoryInfo.lowMemory || memoryInfo.availMem <= memoryInfo.threshold) {
          BelowThreshold
        } else {
          val systemAvailableMemory = memoryInfo.availMem - memoryInfo.threshold

          val runtime = Runtime.getRuntime()
          val appUsedMemory = runtime.totalMemory() - runtime.freeMemory()
          val appAvailableMemory = runtime.maxMemory() - appUsedMemory

          val availableMemory = systemAvailableMemory.coerceAtMost(appAvailableMemory)
          Memory(availableMemory)
        }
      }
    }

    /**
     * See https://dev.to/pyricau/android-vitals-when-did-my-app-start-24p4#process-fork-time
     */
    private fun readProcessForkRealtimeMillis(): Long {
      val myPid = Process.myPid()
      val ticksAtProcessStart = readProcessStartTicks(myPid)

      val ticksPerSecond = if (SDK_INT >= 21) {
        Os.sysconf(OsConstants._SC_CLK_TCK)
      } else {
        val tckConstant = try {
          Class.forName("android.system.OsConstants").getField("_SC_CLK_TCK").getInt(null)
        } catch (e: ClassNotFoundException) {
          Class.forName("libcore.io.OsConstants").getField("_SC_CLK_TCK").getInt(null)
        }
        val os = Class.forName("libcore.io.Libcore").getField("os").get(null)!!
        os::class.java.getMethod("sysconf", Integer.TYPE).invoke(os, tckConstant) as Long
      }
      return ticksAtProcessStart * 1000 / ticksPerSecond
    }

    // Benchmarked (with Jetpack Benchmark) on Pixel 3 running
    // Android 10. Median time: 0.13ms
    private fun readProcessStartTicks(pid: Int): Long {
      val path = "/proc/$pid/stat"
      val stat = FileReader(path).buffered().use { reader ->
        reader.readLine()
      }
      val fields = stat.substringAfter(") ")
        .split(' ')
      return fields[19].toLong()
    }
  }
}