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 com.sun.jna.Library
import com.sun.jna.Native
import com.sun.jna.NativeLong
import com.sun.jna.Pointer
import com.sun.jna.Structure
import org.json.JSONArray
import org.json.JSONObject
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 = {{ANDROID_BENCHMARK_TIMEOUT_SECS}}L
private const val DEFAULT_ANDROID_HEARTBEAT_INTERVAL_SECS = {{ANDROID_HEARTBEAT_INTERVAL_SECS}}L
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 RUN_ID_EXTRA = "mobench_run_id"
private const val NONCE_EXTRA = "mobench_nonce"
private const val LOGICAL_SESSION_ID_EXTRA = "mobench_logical_session_id"
private const val FUNCTION_ID_EXTRA = "mobench_function_id"
private const val PRODUCER_EXTRA = "mobench_producer"
private const val SPEC_ASSET = "bench_spec.json"
private const val REPORT_SCHEMA_V2 = "mobench.run/v2"
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 BENCH_RESULT_OK = 1
private const val BENCH_RESULT_ERROR = 2
private const val BENCH_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
private var resultText: TextView? = null
private val watchdogHandler = Handler(Looper.getMainLooper())
private val watchdog = object : Runnable {
override fun run() {
if (benchmarkComplete) return
checkWorkerExit()
if (!benchmarkComplete) {
val elapsedMs = android.os.SystemClock.elapsedRealtime() - startedAtMs
if (elapsedMs >= params.timeoutSecs * 1_000L) {
emitFailure("timeout", "Timed out waiting ${params.timeoutSecs}s for benchmark completion")
}
}
if (!benchmarkComplete) watchdogHandler.postDelayed(this, watchdogPollIntervalMs())
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
resultText = findViewById(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) {
BENCH_RESULT_HEARTBEAT -> {
workerPid = resultData?.getInt(RESULT_PID_EXTRA)?.takeIf { it > 0 }
workerProcessName = resultData?.getString(RESULT_PROCESS_NAME_EXTRA)
lastProgressAtMs = android.os.SystemClock.elapsedRealtime()
}
BENCH_RESULT_OK -> {
val display = resultData?.getString(RESULT_DISPLAY_EXTRA)
?: "Benchmark worker completed without display output"
resultText?.text = display
benchmarkComplete = true
stopWatchdog()
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 valid result"
resultText?.text = display
failureJson = payload
if (payload == null) {
emitFailure("worker_error", display)
} else {
benchmarkFailed = true
benchmarkComplete = true
stopWatchdog()
}
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)
params.runId?.let { putExtra(RUN_ID_EXTRA, it) }
params.nonce?.let { putExtra(NONCE_EXTRA, it) }
params.logicalSessionId?.let { putExtra(LOGICAL_SESSION_ID_EXTRA, it) }
params.functionId?.let { putExtra(FUNCTION_ID_EXTRA, it) }
params.producer?.let { putExtra(PRODUCER_EXTRA, it) }
putExtra(RESULT_RECEIVER_EXTRA, resultReceiver)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
startForegroundService(intent)
} else {
startService(intent)
}
watchdogHandler.postDelayed(watchdog, watchdogPollIntervalMs())
} 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)
}
}
override fun onDestroy() {
stopWatchdog()
super.onDestroy()
}
fun isBenchmarkComplete(): Boolean = benchmarkComplete
fun isBenchmarkFailed(): Boolean = benchmarkFailed
fun getBenchmarkFailureJson(): String? = failureJson
fun benchmarkTimeoutSecs(): Long = params.timeoutSecs
fun heartbeatIntervalSecs(): Long = params.heartbeatIntervalSecs
fun checkWorkerExit(): String? {
if (benchmarkComplete) {
return failureJson
}
val expectedProcessName = workerProcessName ?: workerProcessName()
val pid = workerPid
val alive = runningAppProcesses().any {
(pid != null && it.pid == pid) || it.processName == expectedProcessName
}
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 a valid BENCH_JSON result 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): String {
failureJson?.let {
resultText?.text = message
benchmarkFailed = true
benchmarkComplete = true
stopWatchdog()
return it
}
val payload = buildFailureJson(
this,
params,
kind,
message,
startedAtMs,
lastProgressAtMs,
workerPid,
workerProcessName ?: workerProcessName(),
)
val encoded = payload.toString()
failureJson = encoded
benchmarkFailed = true
benchmarkComplete = true
resultText?.text = message
stopWatchdog()
android.util.Log.e("BenchRunner", "BENCH_FAILURE_JSON $encoded")
return encoded
}
private fun watchdogPollIntervalMs(): Long =
(params.heartbeatIntervalSecs.coerceAtLeast(1L) * 1_000L).coerceAtMost(5_000L)
private fun stopWatchdog() {
watchdogHandler.removeCallbacks(watchdog)
}
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,
)
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
}
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 heartbeatIntervalSecs =
intent?.getLongExtra(HEARTBEAT_EXTRA, defaults.heartbeatIntervalSecs)
?: defaults.heartbeatIntervalSecs
android.util.Log.i("BenchRunner", "Resolved params: function=$fn, iterations=$iterations, warmup=$warmup")
return BenchParams(
fn,
iterations,
warmup,
timeoutSecs,
heartbeatIntervalSecs,
defaults.runId,
defaults.nonce,
defaults.logicalSessionId,
defaults.functionId,
defaults.producer,
)
}
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)
val function = json.optString("function", DEFAULT_FUNCTION)
val iterations = json.optInt("iterations", DEFAULT_ITERATIONS.toInt()).toUInt()
val warmup = json.optInt("warmup", DEFAULT_WARMUP.toInt()).toUInt()
android.util.Log.i("BenchRunner", "Loaded config from bench_spec.json: function=$function, iterations=$iterations, warmup=$warmup")
BenchParams(
function,
iterations,
warmup,
json.optLong(
"android_benchmark_timeout_secs",
DEFAULT_ANDROID_BENCHMARK_TIMEOUT_SECS,
).takeIf { it > 0L } ?: DEFAULT_ANDROID_BENCHMARK_TIMEOUT_SECS,
json.optLong(
"android_heartbeat_interval_secs",
DEFAULT_ANDROID_HEARTBEAT_INTERVAL_SECS,
).takeIf { it > 0L } ?: DEFAULT_ANDROID_HEARTBEAT_INTERVAL_SECS,
json.optString("run_id").takeUnless { it.isBlank() },
json.optString("nonce").takeUnless { it.isBlank() },
json.optString("logical_session_id").takeUnless { it.isBlank() },
json.optString("function_id").takeUnless { it.isBlank() },
json.optString("producer").takeUnless { it.isBlank() },
)
}
} 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() {
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),
runId = intent.getStringExtra(RUN_ID_EXTRA),
nonce = intent.getStringExtra(NONCE_EXTRA),
logicalSessionId = intent.getStringExtra(LOGICAL_SESSION_ID_EXTRA),
functionId = intent.getStringExtra(FUNCTION_ID_EXTRA),
producer = intent.getStringExtra(PRODUCER_EXTRA),
)
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) BENCH_RESULT_OK else BENCH_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 {
val startMs = android.os.SystemClock.elapsedRealtime()
val display = try {
val processMemorySampler = ProcessMemorySampler()
var runProcessPeakMemoryKb: Long?
processMemorySampler.start()
val rawReport = try {
NativeBenchRunner.run(params)
} finally {
runProcessPeakMemoryKb = processMemorySampler.stop()
}
val json = buildNativeBenchReportJson(rawReport, params, runProcessPeakMemoryKb)
android.util.Log.i("BenchRunner", "BENCH_JSON ${json}")
formatNativeBenchReport(json)
} catch (e: Exception) {
android.util.Log.e("BenchRunner", "Benchmark error: ${e.message}", e)
val message = "Benchmark error: ${e.message}"
val payload = buildFailureJson(
this,
params,
"exception",
message,
startMs,
android.os.SystemClock.elapsedRealtime(),
Process.myPid(),
currentProcessName(),
).toString()
android.util.Log.e("BenchRunner", "BENCH_FAILURE_JSON $payload")
return WorkerBenchmarkResult(
displayText = message,
errorMessage = message,
failureJson = payload,
)
}
return WorkerBenchmarkResult(displayText = display, errorMessage = null, failureJson = null)
}
}
private data class BenchParams(
val function: String,
val iterations: UInt,
val warmup: UInt,
val timeoutSecs: Long,
val heartbeatIntervalSecs: Long,
val runId: String? = null,
val nonce: String? = null,
val logicalSessionId: String? = null,
val functionId: String? = null,
val producer: String? = null,
)
private data class WorkerBenchmarkResult(
val displayText: String,
val errorMessage: String?,
val failureJson: String?,
)
private class WorkerHeartbeat(
private val receiver: ResultReceiver?,
private val params: 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(BENCH_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 NativeBenchRunner {
fun run(params: BenchParams): JSONObject {
val spec = JSONObject()
.put("name", params.function)
.put("iterations", params.iterations.toInt())
.put("warmup", params.warmup.toInt())
val specBytes = spec.toString().toByteArray(Charsets.UTF_8)
val out = MobenchBuf()
val status = MobenchNativeLibrary.api.mobench_run_benchmark_json(
specBytes,
NativeLong(specBytes.size.toLong()),
out
)
out.read()
if (status != 0) {
val error = MobenchNativeLibrary.lastError()
MobenchNativeLibrary.api.mobench_free_buf(out)
throw IllegalStateException(error)
}
return try {
val ptr = out.ptr ?: throw IllegalStateException("native benchmark returned a null report buffer")
val bytes = ptr.getByteArray(0, out.len.toInt())
JSONObject(bytes.toString(Charsets.UTF_8))
} finally {
MobenchNativeLibrary.api.mobench_free_buf(out)
}
}
}
private interface MobenchNativeAbi : Library {
fun mobench_run_benchmark_json(specPtr: ByteArray, specLen: NativeLong, out: MobenchBuf): Int
fun mobench_free_buf(buf: MobenchBuf)
fun mobench_last_error_message(): Pointer?
}
@Suppress("unused")
private class MobenchBuf : Structure() {
@JvmField var ptr: Pointer? = null
@JvmField var len: NativeLong = NativeLong(0)
@JvmField var cap: NativeLong = NativeLong(0)
override fun getFieldOrder(): List<String> = listOf("ptr", "len", "cap")
}
private object MobenchNativeLibrary {
val api: MobenchNativeAbi by lazy {
Native.load("{{LIBRARY_NAME}}", MobenchNativeAbi::class.java)
}
fun lastError(): String {
return api.mobench_last_error_message()?.getString(0) ?: "native benchmark failed"
}
}
@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]
}
}
private fun formatDuration(ns: Long): String {
val ms = ns.toDouble() / 1_000_000.0
return if (ms >= 1000.0) {
String.format("%.3fs", ms / 1000.0)
} else {
String.format("%.3fms", ms)
}
}
private fun buildNativeBenchReportJson(
report: JSONObject,
params: BenchParams,
runProcessPeakMemoryKb: Long?,
): JSONObject {
val json = JSONObject(report.toString())
val spec = json.optJSONObject("spec") ?: JSONObject()
val function = spec.optString("name", DEFAULT_FUNCTION)
json.put("function", function)
val samples = json.optJSONArray("samples") ?: JSONArray()
val durations = mutableListOf<Long>()
val cpuSamplesMs = mutableListOf<Long>()
val peakSamplesKb = mutableListOf<Long>()
val processPeakSamplesKb = mutableListOf<Long>()
val samplesNs = JSONArray()
for (index in 0 until samples.length()) {
val sample = samples.optJSONObject(index) ?: continue
val duration = sample.optLong("duration_ns", -1L)
if (duration >= 0L) {
durations.add(duration)
samplesNs.put(duration)
}
optionalLong(sample, "cpu_time_ms")?.let { cpuSamplesMs.add(it) }
optionalLong(sample, "peak_memory_kb")?.let { peakSamplesKb.add(it) }
optionalLong(sample, "process_peak_memory_kb")?.let { processPeakSamplesKb.add(it) }
}
json.put("samples_ns", samplesNs)
applyV2Envelope(json, params, samplesNs, "success", null, null)
if (durations.isNotEmpty()) {
val stats = JSONObject()
stats.put("min_ns", durations.minOrNull() ?: 0L)
stats.put("max_ns", durations.maxOrNull() ?: 0L)
stats.put("avg_ns", durations.sum().toDouble() / durations.size.toDouble())
stats.put("mean_ns", (durations.sum().toDouble() / durations.size.toDouble()).toLong())
stats.put("median_ns", median(durations))
json.put("stats", stats)
}
val resources = JSONObject()
resources.put("platform", "android")
resources.put("timestamp_ms", System.currentTimeMillis())
resources.put("memory_process", "isolated_worker")
if (cpuSamplesMs.isNotEmpty()) {
val total = cpuSamplesMs.sum()
resources.put("cpu_total_ms", total)
resources.put("cpu_median_ms", median(cpuSamplesMs))
resources.put("elapsed_cpu_ms", total)
}
peakSamplesKb.maxOrNull()?.let {
resources.put("peak_memory_kb", it)
resources.put("peak_memory_growth_kb", it)
}
(processPeakSamplesKb.maxOrNull() ?: runProcessPeakMemoryKb)?.let {
resources.put("process_peak_memory_kb", it)
}
json.put("resources", resources)
return json
}
private fun buildFailureJson(
context: Context,
params: BenchParams,
kind: String,
message: String,
startedAtMs: Long,
lastProgressAtMs: Long?,
pid: Int?,
processName: String?,
): JSONObject {
val elapsedNow = android.os.SystemClock.elapsedRealtime()
val json = JSONObject()
.put("schema_version", FAILURE_SCHEMA_VERSION)
.put("platform", "android")
.put("device", "${Build.MANUFACTURER} ${Build.MODEL}".trim())
.put("function_name", params.function)
.put("kind", kind)
.put("message", message)
.put("elapsed_ms", elapsedNow - startedAtMs)
.put("pid", pid ?: JSONObject.NULL)
.put("process_name", processName ?: JSONObject.NULL)
.put("last_progress_at_ms", lastProgressAtMs ?: JSONObject.NULL)
.put("memory", currentMemoryJson())
.put("android_exit_info", androidExitInfoJson(context, pid, processName) ?: JSONObject.NULL)
applyV2Envelope(json, params, JSONArray(), "failure", kind, message)
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"
}
}
private fun applyV2Envelope(
json: JSONObject,
params: BenchParams,
samplesNs: JSONArray,
status: String,
errorCode: String?,
message: String?,
) {
val runId = params.runId ?: return
val nonce = params.nonce ?: return
val logicalSessionId = params.logicalSessionId ?: return
val functionId = params.functionId ?: return
val producer = params.producer ?: return
json.put("schema_version", REPORT_SCHEMA_V2)
.put("run_id", runId)
.put("nonce", nonce)
.put("logical_session_id", logicalSessionId)
.put("function_id", functionId)
.put("producer", producer)
.put("requested", JSONObject()
.put("iterations", params.iterations.toInt())
.put("warmup", params.warmup.toInt()))
.put("observed", JSONObject()
.put("iterations", samplesNs.length())
.put("warmup", if (status == "success") params.warmup.toInt() else 0))
.put("samples_ns", samplesNs)
.put("outcome", JSONObject().put("status", status).also {
if (errorCode != null && message != null) {
it.put("error", JSONObject().put("code", errorCode).put("message", message))
}
})
}
private fun optionalLong(json: JSONObject, key: String): Long? {
if (!json.has(key) || json.isNull(key)) {
return null
}
return json.optLong(key).takeIf { it >= 0L }
}
private fun formatNativeBenchReport(report: JSONObject): String = buildString {
val spec = report.optJSONObject("spec") ?: JSONObject()
val function = spec.optString("name", DEFAULT_FUNCTION)
val iterations = spec.optInt("iterations", DEFAULT_ITERATIONS.toInt())
val warmup = spec.optInt("warmup", DEFAULT_WARMUP.toInt())
val samples = report.optJSONArray("samples") ?: JSONArray()
appendLine("=== Benchmark Results ===")
appendLine()
appendLine("Function: $function")
appendLine("Iterations: $iterations")
appendLine("Warmup: $warmup")
appendLine()
appendLine("Samples (${samples.length()}):")
for (index in 0 until samples.length()) {
val duration = samples.optJSONObject(index)?.optLong("duration_ns", 0L) ?: 0L
appendLine(" ${index + 1}. ${formatDuration(duration)}")
}
val stats = report.optJSONObject("stats")
if (stats != null) {
appendLine()
appendLine("Statistics:")
appendLine(" Min: ${formatDuration(stats.optLong("min_ns", 0L))}")
appendLine(" Max: ${formatDuration(stats.optLong("max_ns", 0L))}")
appendLine(" Avg: ${formatDuration(stats.optLong("mean_ns", 0L))}")
}
}