package {{PACKAGE_NAME}}
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.ActivityManager
import android.app.Application
import android.app.ApplicationExitInfo
import android.app.Service
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.os.Debug
import android.os.Handler
import android.os.IBinder
import android.os.Looper
import android.os.Process
import android.os.ResultReceiver
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import org.json.JSONArray
import org.json.JSONObject
import uniffi.{{UNIFFI_NAMESPACE}}.BenchException
import uniffi.{{UNIFFI_NAMESPACE}}.BenchReport
import uniffi.{{UNIFFI_NAMESPACE}}.BenchSpec
import uniffi.{{UNIFFI_NAMESPACE}}.runBenchmark
private const val DEFAULT_FUNCTION = "{{DEFAULT_FUNCTION}}"
private const val DEFAULT_ITERATIONS = 20u
private const val DEFAULT_WARMUP = 3u
private const val DEFAULT_ANDROID_BENCHMARK_TIMEOUT_SECS = 1800L
private const val DEFAULT_ANDROID_HEARTBEAT_INTERVAL_SECS = 10L
private const val FUNCTION_EXTRA = "bench_function"
private const val ITERATIONS_EXTRA = "bench_iterations"
private const val WARMUP_EXTRA = "bench_warmup"
private const val TIMEOUT_EXTRA = "bench_timeout_secs"
private const val HEARTBEAT_EXTRA = "bench_heartbeat_secs"
private const val SPEC_ASSET = "bench_spec.json"
private const val RUN_BENCHMARK_ACTION = "{{PACKAGE_NAME}}.RUN_BENCHMARK"
private const val RESULT_RECEIVER_EXTRA = "bench_result_receiver"
private const val RESULT_DISPLAY_EXTRA = "bench_display"
private const val RESULT_ERROR_EXTRA = "bench_error"
private const val RESULT_FAILURE_JSON_EXTRA = "bench_failure_json"
private const val RESULT_HEARTBEAT_JSON_EXTRA = "bench_heartbeat_json"
private const val RESULT_PID_EXTRA = "bench_pid"
private const val RESULT_PROCESS_NAME_EXTRA = "bench_process_name"
private const val RESULT_OK = 1
private const val RESULT_ERROR = 2
private const val RESULT_HEARTBEAT = 3
private const val FOREGROUND_NOTIFICATION_ID = 1001
private const val FOREGROUND_CHANNEL_ID = "mobench_benchmark"
private const val FOREGROUND_CHANNEL_NAME = "Mobench benchmark"
private const val WORKER_PROCESS_SUFFIX = ":mobench_worker"
private const val FAILURE_SCHEMA_VERSION = 1
class MainActivity : AppCompatActivity() {
@Volatile private var benchmarkComplete = false
@Volatile private var benchmarkFailed = false
@Volatile private var failureJson: String? = null
@Volatile private var workerPid: Int? = null
@Volatile private var workerProcessName: String? = null
@Volatile private var lastProgressAtMs: Long? = null
@Volatile private var startedAtMs: Long = 0L
private lateinit var params: BenchParams
data class BenchParams(
val function: String,
val iterations: UInt,
val warmup: UInt,
val timeoutSecs: Long,
val heartbeatIntervalSecs: Long,
)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val resultText = findViewById<TextView>(R.id.result_text)
resultText?.text = "Running benchmark..."
params = resolveBenchParams()
startedAtMs = android.os.SystemClock.elapsedRealtime()
lastProgressAtMs = startedAtMs
val resultReceiver = object : ResultReceiver(Handler(Looper.getMainLooper())) {
override fun onReceiveResult(resultCode: Int, resultData: Bundle?) {
when (resultCode) {
RESULT_HEARTBEAT -> {
workerPid = resultData?.getInt(RESULT_PID_EXTRA)?.takeIf { it > 0 }
workerProcessName = resultData?.getString(RESULT_PROCESS_NAME_EXTRA)
lastProgressAtMs = android.os.SystemClock.elapsedRealtime()
}
RESULT_OK -> {
val display = resultData?.getString(RESULT_DISPLAY_EXTRA)
?: "Benchmark worker completed"
resultText?.text = display
benchmarkComplete = true
android.util.Log.i("BenchRunner", "Benchmark worker completed")
}
else -> {
val payload = resultData?.getString(RESULT_FAILURE_JSON_EXTRA)
val display = resultData?.getString(RESULT_DISPLAY_EXTRA)
?: resultData?.getString(RESULT_ERROR_EXTRA)
?: "Benchmark worker returned no result"
resultText?.text = display
benchmarkFailed = true
benchmarkComplete = true
failureJson = payload
if (payload == null) {
emitFailure("unknown", display)
}
android.util.Log.e("BenchRunner", display)
}
}
}
}
try {
val intent = Intent(this, BenchmarkWorkerService::class.java).apply {
action = RUN_BENCHMARK_ACTION
putExtra(FUNCTION_EXTRA, params.function)
putExtra(ITERATIONS_EXTRA, params.iterations.toInt())
putExtra(WARMUP_EXTRA, params.warmup.toInt())
putExtra(TIMEOUT_EXTRA, params.timeoutSecs)
putExtra(HEARTBEAT_EXTRA, params.heartbeatIntervalSecs)
putExtra(RESULT_RECEIVER_EXTRA, resultReceiver)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
startForegroundService(intent)
} else {
startService(intent)
}
} catch (e: Exception) {
android.util.Log.e("BenchRunner", "Failed to run benchmark worker", e)
val message = "Failed to run benchmark worker: ${e.message}"
resultText?.text = message
emitFailure("exception", message)
}
}
fun isBenchmarkComplete(): Boolean {
return benchmarkComplete
}
fun isBenchmarkFailed(): Boolean {
return benchmarkFailed
}
fun getBenchmarkFailureJson(): String? {
return failureJson
}
fun benchmarkTimeoutSecs(): Long {
return params.timeoutSecs
}
fun heartbeatIntervalSecs(): Long {
return params.heartbeatIntervalSecs
}
fun checkWorkerExit(): String? {
if (benchmarkComplete || workerPid == null) {
return failureJson
}
val pid = workerPid ?: return failureJson
val processName = workerProcessName ?: workerProcessName()
val alive = runningAppProcesses().any {
it.pid == pid || it.processName == processName
}
val graceMs = params.heartbeatIntervalSecs.coerceAtLeast(1L) * 3_000L
val stale = android.os.SystemClock.elapsedRealtime() - (lastProgressAtMs ?: startedAtMs) > graceMs
if (!alive && stale) {
emitFailure("worker_exit", "Benchmark worker process exited before BENCH_JSON was emitted")
}
return failureJson
}
fun emitTimeoutFailureFromTest(): String {
checkWorkerExit()
if (failureJson == null) {
emitFailure("timeout", "Timed out waiting ${params.timeoutSecs}s for benchmark completion")
}
return failureJson ?: "{}"
}
private fun emitFailure(kind: String, message: String) {
if (failureJson != null) {
benchmarkFailed = true
benchmarkComplete = true
return
}
val payload = buildFailureJson(
this,
params.function,
kind,
message,
startedAtMs,
lastProgressAtMs,
workerPid,
workerProcessName ?: workerProcessName(),
)
val encoded = payload.toString()
failureJson = encoded
benchmarkFailed = true
benchmarkComplete = true
android.util.Log.e("BenchRunner", "BENCH_FAILURE_JSON $encoded")
}
private fun resolveBenchParams(): BenchParams {
val assetParams = loadBenchParamsFromAssets()
val defaults = assetParams ?: BenchParams(
DEFAULT_FUNCTION,
DEFAULT_ITERATIONS,
DEFAULT_WARMUP,
DEFAULT_ANDROID_BENCHMARK_TIMEOUT_SECS,
DEFAULT_ANDROID_HEARTBEAT_INTERVAL_SECS,
)
// Check for intent extras used by local automation, smoke tests, and provider-driven runs.
val intentFunction = intent?.getStringExtra(FUNCTION_EXTRA)?.takeUnless { it.isBlank() }
val intentIterations = intent?.let {
val value = it.getIntExtra(ITERATIONS_EXTRA, -1)
if (value >= 0) value.toUInt() else null
}
val intentWarmup = intent?.let {
val value = it.getIntExtra(WARMUP_EXTRA, -1)
if (value >= 0) value.toUInt() else null
}
// Resolve final values with logging
val fn = intentFunction ?: defaults.function
val iterations = intentIterations ?: defaults.iterations
val warmup = intentWarmup ?: defaults.warmup
val timeoutSecs = intent?.getLongExtra(TIMEOUT_EXTRA, defaults.timeoutSecs) ?: defaults.timeoutSecs
val heartbeatSecs = intent?.getLongExtra(HEARTBEAT_EXTRA, defaults.heartbeatIntervalSecs)
?: defaults.heartbeatIntervalSecs
// Log the resolution source for debugging
if (assetParams == null && intentFunction == null && intentIterations == null && intentWarmup == null) {
android.util.Log.i("BenchRunner", "Using hardcoded defaults: function=$fn, iterations=$iterations, warmup=$warmup")
} else {
val sources = mutableListOf<String>()
if (intentFunction != null) sources.add("function from intent")
if (intentIterations != null) sources.add("iterations from intent")
if (intentWarmup != null) sources.add("warmup from intent")
if (assetParams != null) {
if (intentFunction == null) sources.add("function from bench_spec.json")
if (intentIterations == null) sources.add("iterations from bench_spec.json")
if (intentWarmup == null) sources.add("warmup from bench_spec.json")
}
android.util.Log.i("BenchRunner", "Resolved params: function=$fn, iterations=$iterations, warmup=$warmup (sources: ${sources.joinToString(", ")})")
}
return BenchParams(fn, iterations, warmup, timeoutSecs, heartbeatSecs)
}
private fun loadBenchParamsFromAssets(): BenchParams? {
return try {
val raw = assets.open(SPEC_ASSET).bufferedReader().use { it.readText() }
if (raw.isBlank()) {
android.util.Log.w("BenchRunner", "bench_spec.json exists but is empty, using defaults")
null
} else {
val json = JSONObject(raw)
// Log warnings for missing or invalid config values
val function = if (json.has("function")) {
json.getString("function")
} else {
android.util.Log.w("BenchRunner", "Config missing 'function' key, using default: $DEFAULT_FUNCTION")
DEFAULT_FUNCTION
}
val iterations = if (json.has("iterations")) {
try {
json.getInt("iterations").toUInt()
} catch (e: Exception) {
android.util.Log.w("BenchRunner", "Config 'iterations' is not a valid integer: ${json.opt("iterations")}, using default: $DEFAULT_ITERATIONS")
DEFAULT_ITERATIONS
}
} else {
android.util.Log.w("BenchRunner", "Config missing 'iterations' key, using default: $DEFAULT_ITERATIONS")
DEFAULT_ITERATIONS
}
val warmup = if (json.has("warmup")) {
try {
json.getInt("warmup").toUInt()
} catch (e: Exception) {
android.util.Log.w("BenchRunner", "Config 'warmup' is not a valid integer: ${json.opt("warmup")}, using default: $DEFAULT_WARMUP")
DEFAULT_WARMUP
}
} else {
android.util.Log.w("BenchRunner", "Config missing 'warmup' key, using default: $DEFAULT_WARMUP")
DEFAULT_WARMUP
}
val timeoutSecs = json.optLong("android_benchmark_timeout_secs", DEFAULT_ANDROID_BENCHMARK_TIMEOUT_SECS)
.takeIf { it > 0L } ?: DEFAULT_ANDROID_BENCHMARK_TIMEOUT_SECS
val heartbeatSecs = json.optLong("android_heartbeat_interval_secs", DEFAULT_ANDROID_HEARTBEAT_INTERVAL_SECS)
.takeIf { it > 0L } ?: DEFAULT_ANDROID_HEARTBEAT_INTERVAL_SECS
android.util.Log.i("BenchRunner", "Loaded config from bench_spec.json: function=$function, iterations=$iterations, warmup=$warmup, timeout=${timeoutSecs}s, heartbeat=${heartbeatSecs}s")
BenchParams(function, iterations, warmup, timeoutSecs, heartbeatSecs)
}
} catch (e: java.io.FileNotFoundException) {
android.util.Log.d("BenchRunner", "No bench_spec.json in assets, will use intent extras or defaults")
null
} catch (e: Exception) {
android.util.Log.e("BenchRunner", "Failed to parse bench_spec.json from assets", e)
null
}
}
}
class BenchmarkWorkerService : Service() {
data class BenchParams(
val function: String,
val iterations: UInt,
val warmup: UInt,
val timeoutSecs: Long,
val heartbeatIntervalSecs: Long,
)
override fun onBind(intent: Intent?): IBinder? = null
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
if (intent?.action != RUN_BENCHMARK_ACTION) {
stopSelf(startId)
return START_NOT_STICKY
}
val resultReceiver = intent.resultReceiverExtra()
val params = BenchParams(
function = intent.getStringExtra(FUNCTION_EXTRA)?.takeUnless { it.isBlank() } ?: DEFAULT_FUNCTION,
iterations = intent.getIntExtra(ITERATIONS_EXTRA, DEFAULT_ITERATIONS.toInt()).toUInt(),
warmup = intent.getIntExtra(WARMUP_EXTRA, DEFAULT_WARMUP.toInt()).toUInt(),
timeoutSecs = intent.getLongExtra(TIMEOUT_EXTRA, DEFAULT_ANDROID_BENCHMARK_TIMEOUT_SECS),
heartbeatIntervalSecs = intent.getLongExtra(HEARTBEAT_EXTRA, DEFAULT_ANDROID_HEARTBEAT_INTERVAL_SECS),
)
startBenchmarkForeground()
Thread {
val startMs = android.os.SystemClock.elapsedRealtime()
val heartbeat = WorkerHeartbeat(resultReceiver, params, startMs)
heartbeat.start()
try {
val result = runBenchmarkInWorker(params)
val bundle = Bundle().apply {
putString(RESULT_DISPLAY_EXTRA, result.displayText)
result.errorMessage?.let { putString(RESULT_ERROR_EXTRA, it) }
result.failureJson?.let { putString(RESULT_FAILURE_JSON_EXTRA, it) }
}
resultReceiver?.send(if (result.errorMessage == null) RESULT_OK else RESULT_ERROR, bundle)
} finally {
heartbeat.stop()
stopBenchmarkForeground()
stopSelf(startId)
}
}.apply {
name = "mobench-benchmark-worker"
start()
}
return START_NOT_STICKY
}
private fun startBenchmarkForeground() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val manager = getSystemService(NotificationManager::class.java)
manager?.createNotificationChannel(
NotificationChannel(
FOREGROUND_CHANNEL_ID,
FOREGROUND_CHANNEL_NAME,
NotificationManager.IMPORTANCE_LOW
)
)
}
startForeground(FOREGROUND_NOTIFICATION_ID, buildBenchmarkNotification())
}
@Suppress("DEPRECATION")
private fun stopBenchmarkForeground() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
stopForeground(Service.STOP_FOREGROUND_REMOVE)
} else {
stopForeground(true)
}
}
@Suppress("DEPRECATION")
private fun buildBenchmarkNotification(): Notification {
val builder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
Notification.Builder(this, FOREGROUND_CHANNEL_ID)
} else {
Notification.Builder(this)
.setPriority(Notification.PRIORITY_LOW)
}
return builder
.setSmallIcon(android.R.drawable.ic_dialog_info)
.setContentTitle("Running benchmark")
.setContentText("Mobench isolated worker is measuring this run")
.setOngoing(true)
.setOnlyAlertOnce(true)
.setShowWhen(false)
.setCategory(Notification.CATEGORY_SERVICE)
.build()
}
private fun runBenchmarkInWorker(params: BenchParams): WorkerBenchmarkResult {
BenchNativeLibrary.ensureLoaded()
val startMs = android.os.SystemClock.elapsedRealtime()
val display = try {
val spec = BenchSpec(
name = params.function,
iterations = params.iterations,
warmup = params.warmup
)
val processMemorySampler = ProcessMemorySampler()
var runProcessPeakMemoryKb: Long?
processMemorySampler.start()
val report = try {
runBenchmark(spec)
} finally {
runProcessPeakMemoryKb = processMemorySampler.stop()
}
// Debug: Log first sample's raw nanoseconds
if (report.samples.isNotEmpty()) {
android.util.Log.d("BenchmarkWorker", "First sample duration_ns: ${report.samples[0].durationNs}")
}
val json = buildBenchReportJson(report, runProcessPeakMemoryKb)
android.util.Log.i("BenchRunner", "BENCH_JSON ${json}")
formatBenchReport(report)
} catch (e: BenchException) {
android.util.Log.e("BenchRunner", "Benchmark error: ${e.message}", e)
val payload = buildFailureJson(
this,
params.function,
"exception",
"Benchmark error: ${e.message}",
startMs,
android.os.SystemClock.elapsedRealtime(),
Process.myPid(),
currentProcessName()
).toString()
android.util.Log.e("BenchRunner", "BENCH_FAILURE_JSON $payload")
return WorkerBenchmarkResult(
displayText = "Benchmark error: ${e.message}",
errorMessage = "Benchmark error: ${e.message}",
failureJson = payload
)
} catch (e: Exception) {
android.util.Log.e("BenchRunner", "Unexpected error during benchmark execution", e)
val payload = buildFailureJson(
this,
params.function,
"exception",
"Unexpected error: ${e.message}",
startMs,
android.os.SystemClock.elapsedRealtime(),
Process.myPid(),
currentProcessName()
).toString()
android.util.Log.e("BenchRunner", "BENCH_FAILURE_JSON $payload")
return WorkerBenchmarkResult(
displayText = "Unexpected error: ${e.message}",
errorMessage = "Unexpected error: ${e.message}",
failureJson = payload
)
}
return WorkerBenchmarkResult(displayText = display, errorMessage = null, failureJson = null)
}
}
private data class WorkerBenchmarkResult(
val displayText: String,
val errorMessage: String?,
val failureJson: String?,
)
private class WorkerHeartbeat(
private val receiver: ResultReceiver?,
private val params: BenchmarkWorkerService.BenchParams,
private val startMs: Long,
) {
@Volatile private var running = false
private var thread: Thread? = null
fun start() {
if (running) {
return
}
running = true
thread = Thread {
while (running) {
val now = android.os.SystemClock.elapsedRealtime()
val json = JSONObject()
.put("schema_version", FAILURE_SCHEMA_VERSION)
.put("platform", "android")
.put("function_name", params.function)
.put("elapsed_ms", now - startMs)
.put("pid", Process.myPid())
.put("process_name", currentProcessName())
.put("memory", currentMemoryJson())
android.util.Log.i("BenchRunner", "BENCH_HEARTBEAT_JSON $json")
receiver?.send(RESULT_HEARTBEAT, Bundle().apply {
putString(RESULT_HEARTBEAT_JSON_EXTRA, json.toString())
putInt(RESULT_PID_EXTRA, Process.myPid())
putString(RESULT_PROCESS_NAME_EXTRA, currentProcessName())
})
try {
Thread.sleep(params.heartbeatIntervalSecs.coerceAtLeast(1L) * 1000L)
} catch (e: InterruptedException) {
Thread.currentThread().interrupt()
break
}
}
}.apply {
name = "mobench-worker-heartbeat"
isDaemon = true
start()
}
}
fun stop() {
running = false
thread?.interrupt()
}
}
private object BenchNativeLibrary {
init {
System.loadLibrary("{{LIBRARY_NAME}}")
}
fun ensureLoaded() = Unit
}
private fun buildFailureJson(
context: Context,
functionName: String,
kind: String,
message: String,
startedAtMs: Long,
lastProgressAtMs: Long?,
pid: Int?,
processName: String?,
): JSONObject {
val elapsedNow = android.os.SystemClock.elapsedRealtime()
val json = JSONObject()
json.put("schema_version", FAILURE_SCHEMA_VERSION)
json.put("platform", "android")
json.put("device", "${Build.MANUFACTURER} ${Build.MODEL}".trim())
json.put("function_name", functionName)
json.put("kind", kind)
json.put("message", message)
json.put("elapsed_ms", elapsedNow - startedAtMs)
if (pid != null) {
json.put("pid", pid)
} else {
json.put("pid", JSONObject.NULL)
}
json.put("process_name", processName ?: JSONObject.NULL)
json.put("last_progress_at_ms", lastProgressAtMs ?: JSONObject.NULL)
json.put("memory", currentMemoryJson())
json.put("android_exit_info", androidExitInfoJson(context, pid, processName) ?: JSONObject.NULL)
return json
}
private fun androidExitInfoJson(context: Context, pid: Int?, processName: String?): JSONObject? {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
return null
}
return try {
val manager = context.getSystemService(ActivityManager::class.java) ?: return null
val reasons = manager.getHistoricalProcessExitReasons(context.packageName, pid ?: 0, 10)
val match = reasons.firstOrNull {
(pid != null && it.pid == pid) || (processName != null && it.processName == processName)
} ?: reasons.firstOrNull()
match?.let { info ->
JSONObject()
.put("reason", exitReasonName(info.reason))
.put("raw_reason", info.reason)
.put("description", info.description ?: JSONObject.NULL)
.put("importance", info.importance)
.put("timestamp", info.timestamp)
.put("pid", info.pid)
.put("process_name", info.processName ?: JSONObject.NULL)
}
} catch (e: Exception) {
android.util.Log.d("BenchRunner", "Unable to read historical process exit reasons", e)
null
}
}
private fun exitReasonName(reason: Int): String {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
return "unknown"
}
return when (reason) {
ApplicationExitInfo.REASON_ANR -> "anr"
ApplicationExitInfo.REASON_CRASH -> "crash"
ApplicationExitInfo.REASON_CRASH_NATIVE -> "crash_native"
ApplicationExitInfo.REASON_DEPENDENCY_DIED -> "dependency_died"
ApplicationExitInfo.REASON_EXCESSIVE_RESOURCE_USAGE -> "excessive_resource_usage"
ApplicationExitInfo.REASON_EXIT_SELF -> "exit_self"
ApplicationExitInfo.REASON_INITIALIZATION_FAILURE -> "initialization_failure"
ApplicationExitInfo.REASON_LOW_MEMORY -> "low_memory"
ApplicationExitInfo.REASON_OTHER -> "other"
ApplicationExitInfo.REASON_PERMISSION_CHANGE -> "permission_change"
ApplicationExitInfo.REASON_SIGNALED -> "signaled"
ApplicationExitInfo.REASON_USER_REQUESTED -> "user_requested"
else -> "unknown"
}
}
private fun currentMemoryJson(): JSONObject {
val memInfo = Debug.MemoryInfo()
return try {
Debug.getMemoryInfo(memInfo)
JSONObject()
.put("total_pss_kb", memInfo.totalPss)
.put("private_dirty_kb", memInfo.totalPrivateDirty)
.put("native_heap_kb", Debug.getNativeHeapAllocatedSize() / 1024)
.put("java_heap_kb", (Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory()) / 1024)
.put("process_pss_kb", currentProcessPssKb() ?: JSONObject.NULL)
} catch (e: Exception) {
JSONObject()
}
}
private fun Context.runningAppProcesses(): List<ActivityManager.RunningAppProcessInfo> {
return try {
val manager = getSystemService(ActivityManager::class.java)
manager?.runningAppProcesses ?: emptyList()
} catch (e: Exception) {
emptyList()
}
}
private fun Context.workerProcessName(): String = "$packageName$WORKER_PROCESS_SUFFIX"
private fun currentProcessName(): String {
if (Build.VERSION.SDK_INT >= 28) {
return Application.getProcessName()
}
return try {
java.io.File("/proc/self/cmdline").readText().trim('\u0000', ' ', '\n')
} catch (e: Exception) {
"unknown"
}
}
@Suppress("DEPRECATION")
private fun Intent.resultReceiverExtra(): ResultReceiver? {
return if (Build.VERSION.SDK_INT >= 33) {
getParcelableExtra(RESULT_RECEIVER_EXTRA, ResultReceiver::class.java)
} else {
getParcelableExtra(RESULT_RECEIVER_EXTRA) as? ResultReceiver
}
}
private class ProcessMemorySampler(private val sampleIntervalMs: Long = 1000L) {
@Volatile private var running = false
@Volatile private var peakKb = 0L
private var samplerThread: Thread? = null
fun start() {
if (running) {
return
}
running = true
recordCurrent()
samplerThread = Thread {
while (running) {
recordCurrent()
try {
Thread.sleep(sampleIntervalMs)
} catch (e: InterruptedException) {
Thread.currentThread().interrupt()
break
}
}
recordCurrent()
}.apply {
name = "mobench-process-memory-sampler"
isDaemon = true
start()
}
}
fun stop(): Long? {
running = false
try {
samplerThread?.join(sampleIntervalMs * 2)
} catch (e: InterruptedException) {
Thread.currentThread().interrupt()
}
recordCurrent()
return peakKb.takeIf { it > 0L }
}
@Synchronized
private fun recordCurrent() {
currentProcessPssKb()?.let { observedKb ->
if (observedKb > peakKb) {
peakKb = observedKb
}
}
}
}
private fun currentProcessPssKb(): Long? {
readProcMemoryKb("/proc/self/smaps_rollup", "Pss:")?.let { return it }
readProcMemoryKb("/proc/self/status", "VmHWM:")?.let { return it }
readProcMemoryKb("/proc/self/status", "VmRSS:")?.let { return it }
val memInfo = Debug.MemoryInfo()
return try {
Debug.getMemoryInfo(memInfo)
memInfo.totalPss.toLong().takeIf { it > 0L }
} catch (e: Exception) {
android.util.Log.d("BenchRunner", "Unable to read process memory from Debug.getMemoryInfo", e)
null
}
}
private fun readProcMemoryKb(path: String, label: String): Long? {
return try {
java.io.File(path).bufferedReader().use { reader ->
var line: String? = reader.readLine()
while (line != null) {
if (line.startsWith(label)) {
return@use parseMemoryKb(line)
}
line = reader.readLine()
}
null
}
} catch (e: Exception) {
null
}
}
private fun parseMemoryKb(line: String): Long? {
val value = line
.substringAfter(':', "")
.trim()
.split(' ')
.firstOrNull { it.isNotBlank() }
return value?.toLongOrNull()?.takeIf { it > 0L }
}
private fun median(values: List<Long>): Long {
val sorted = values.sorted()
if (sorted.isEmpty()) {
return 0L
}
val middle = sorted.size / 2
return if (sorted.size % 2 == 0) {
(sorted[middle - 1] + sorted[middle]) / 2
} else {
sorted[middle]
}
}
/**
* Formats a duration in nanoseconds to a human-readable string.
* Uses milliseconds (ms) by default, switches to seconds (s) if >= 1000ms.
*/
private fun formatDuration(ns: Long): String {
val ms = ns.toDouble() / 1_000_000.0
return if (ms >= 1000.0) {
val secs = ms / 1000.0
String.format("%.3fs", secs)
} else {
String.format("%.3fms", ms)
}
}
private fun formatBenchReport(report: BenchReport): String = buildString {
appendLine("=== Benchmark Results ===")
appendLine()
appendLine("Function: ${report.spec.name}")
appendLine("Iterations: ${report.spec.iterations}")
appendLine("Warmup: ${report.spec.warmup}")
appendLine()
appendLine("Samples (${report.samples.size}):")
report.samples.forEachIndexed { index, sample ->
appendLine(" ${index + 1}. ${formatDuration(sample.durationNs.toLong())}")
}
if (report.samples.isNotEmpty()) {
val durations = report.samples.map { it.durationNs.toLong() }
val min = durations.minOrNull() ?: 0L
val max = durations.maxOrNull() ?: 0L
val avg = durations.sum().toDouble() / durations.size.toDouble()
appendLine()
appendLine("Statistics:")
appendLine(" Min: ${formatDuration(min)}")
appendLine(" Max: ${formatDuration(max)}")
appendLine(" Avg: ${formatDuration(avg.toLong())}")
}
}
private fun buildBenchReportJson(report: BenchReport, runProcessPeakMemoryKb: Long?): JSONObject {
val json = JSONObject()
val spec = JSONObject()
spec.put("name", report.spec.name)
spec.put("iterations", report.spec.iterations.toInt())
spec.put("warmup", report.spec.warmup.toInt())
json.put("spec", spec)
val durationSamplesNs = report.samples.map { it.durationNs.toLong() }
val samplesNs = JSONArray()
durationSamplesNs.forEach { samplesNs.put(it) }
json.put("samples_ns", samplesNs)
val samples = JSONArray()
report.samples.forEach { sample ->
val sampleJson = JSONObject()
sampleJson.put("duration_ns", sample.durationNs.toLong())
optionalCpuTimeMs(sample)?.let { sampleJson.put("cpu_time_ms", it) }
optionalPeakMemoryKb(sample)?.let { sampleJson.put("peak_memory_kb", it) }
optionalProcessPeakMemoryKb(sample)?.let { sampleJson.put("process_peak_memory_kb", it) }
samples.put(sampleJson)
}
json.put("samples", samples)
val phases = JSONArray()
optionalPhases(report).forEach { phase ->
val phaseJson = JSONObject()
optionalStringProperty(phase, "name", "getName")?.let { phaseJson.put("name", it) }
optionalNumericProperty(phase, "durationNs", "getDurationNs")?.let {
phaseJson.put("duration_ns", it)
}
phases.put(phaseJson)
}
json.put("phases", phases)
if (durationSamplesNs.isNotEmpty()) {
val min = durationSamplesNs.minOrNull() ?: 0L
val max = durationSamplesNs.maxOrNull() ?: 0L
val avg = durationSamplesNs.sum().toDouble() / durationSamplesNs.size.toDouble()
val stats = JSONObject()
stats.put("min_ns", min)
stats.put("max_ns", max)
stats.put("avg_ns", avg.toDouble())
json.put("stats", stats)
}
val cpuSamplesMs = report.samples.mapNotNull { optionalCpuTimeMs(it) }
val peakSamplesKb = report.samples.mapNotNull { optionalPeakMemoryKb(it) }
val processPeakSamplesKb = report.samples.mapNotNull { optionalProcessPeakMemoryKb(it) }
val memInfo = Debug.MemoryInfo()
Debug.getMemoryInfo(memInfo)
val resources = JSONObject()
resources.put("platform", "android")
resources.put("timestamp_ms", System.currentTimeMillis())
resources.put("memory_process", "isolated_worker")
if (cpuSamplesMs.isNotEmpty()) {
val cpuTotalMs = cpuSamplesMs.sum()
resources.put("cpu_total_ms", cpuTotalMs)
resources.put("cpu_median_ms", median(cpuSamplesMs))
resources.put("elapsed_cpu_ms", cpuTotalMs)
}
if (peakSamplesKb.isNotEmpty()) {
val peakGrowthKb = peakSamplesKb.maxOrNull() ?: 0L
resources.put("peak_memory_kb", peakGrowthKb)
resources.put("peak_memory_growth_kb", peakGrowthKb)
}
val processPeakMemoryKb = processPeakSamplesKb.maxOrNull() ?: runProcessPeakMemoryKb
processPeakMemoryKb?.let {
resources.put("process_peak_memory_kb", it)
resources.put("isolated_process_peak_memory_kb", it)
}
resources.put("total_pss_kb", memInfo.totalPss)
resources.put("private_dirty_kb", memInfo.totalPrivateDirty)
resources.put("native_heap_kb", Debug.getNativeHeapAllocatedSize() / 1024)
val usedHeap = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory()
resources.put("java_heap_kb", usedHeap / 1024)
json.put("resources", resources)
return json
}
private fun optionalProcessPeakMemoryKb(sample: Any): Long? {
return optionalNumericProperty(sample, "processPeakMemoryKb", "getProcessPeakMemoryKb")
}
private fun optionalCpuTimeMs(sample: Any): Long? {
return optionalNumericProperty(sample, "cpuTimeMs", "getCpuTimeMs")
}
private fun optionalPeakMemoryKb(sample: Any): Long? {
return optionalNumericProperty(sample, "peakMemoryKb", "getPeakMemoryKb")
}
private fun optionalPhases(report: Any): List<Any> {
return optionalCollectionProperty(report, "phases", "getPhases")
}
private fun optionalCollectionProperty(instance: Any, propertyName: String, getterName: String): List<Any> {
try {
val getter = instance.javaClass.methods.firstOrNull {
it.name == getterName && it.parameterTypes.isEmpty()
}
coerceToList(getter?.invoke(instance))?.let { return it }
} catch (e: Exception) {
android.util.Log.d("BenchRunner", "Optional collection getter unavailable: $propertyName")
}
try {
val field = instance.javaClass.declaredFields.firstOrNull { it.name == propertyName }
if (field != null) {
field.isAccessible = true
coerceToList(field.get(instance))?.let { return it }
}
} catch (e: Exception) {
android.util.Log.d("BenchRunner", "Optional collection field unavailable: $propertyName")
}
return emptyList()
}
private fun coerceToList(value: Any?): List<Any>? {
return when (value) {
null -> null
is Iterable<*> -> value.filterNotNull()
is Array<*> -> value.filterNotNull()
else -> null
}
}
private fun optionalNumericProperty(instance: Any, propertyName: String, getterName: String): Long? {
try {
val getter = instance.javaClass.methods.firstOrNull {
it.name == getterName && it.parameterTypes.isEmpty()
}
coerceToLong(getter?.invoke(instance))?.let { return it }
} catch (e: Exception) {
android.util.Log.d("BenchRunner", "Optional property getter unavailable: $propertyName")
}
try {
val field = instance.javaClass.declaredFields.firstOrNull { it.name == propertyName }
if (field != null) {
field.isAccessible = true
coerceToLong(field.get(instance))?.let { return it }
}
} catch (e: Exception) {
android.util.Log.d("BenchRunner", "Optional property field unavailable: $propertyName")
}
return null
}
private fun optionalStringProperty(instance: Any, propertyName: String, getterName: String): String? {
try {
val getter = instance.javaClass.methods.firstOrNull {
it.name == getterName && it.parameterTypes.isEmpty()
}
getter?.invoke(instance)?.toString()?.let { return it }
} catch (e: Exception) {
android.util.Log.d("BenchRunner", "Optional string getter unavailable: $propertyName")
}
try {
val field = instance.javaClass.declaredFields.firstOrNull { it.name == propertyName }
if (field != null) {
field.isAccessible = true
field.get(instance)?.toString()?.let { return it }
}
} catch (e: Exception) {
android.util.Log.d("BenchRunner", "Optional string field unavailable: $propertyName")
}
return null
}
private fun coerceToLong(value: Any?): Long? {
return when (value) {
null -> null
is Long -> value
is Int -> value.toLong()
is Short -> value.toLong()
is Byte -> value.toLong()
is Number -> value.toLong()
else -> value.toString().toLongOrNull()
}
}