package {{PACKAGE_NAME}}
import android.os.Bundle
import android.os.Debug
import android.os.Process
import android.os.SystemClock
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
class MainActivity : AppCompatActivity() {
companion object {
private const val DEFAULT_FUNCTION = "{{DEFAULT_FUNCTION}}"
private const val DEFAULT_ITERATIONS = 20u
private const val DEFAULT_WARMUP = 3u
private const val FUNCTION_EXTRA = "bench_function"
private const val ITERATIONS_EXTRA = "bench_iterations"
private const val WARMUP_EXTRA = "bench_warmup"
private const val SPEC_ASSET = "bench_spec.json"
init {
System.loadLibrary("{{LIBRARY_NAME}}")
}
}
private data class BenchParams(
val function: String,
val iterations: UInt,
val warmup: UInt,
)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val params = resolveBenchParams()
val display = try {
val spec = BenchSpec(
name = params.function,
iterations = params.iterations,
warmup = params.warmup
)
val report = runBenchmark(spec)
// Debug: Log first sample's raw nanoseconds
if (report.samples.isNotEmpty()) {
android.util.Log.d("MainActivity", "First sample duration_ns: ${report.samples[0].durationNs}")
}
logBenchReport(report)
formatBenchReport(report)
} catch (e: BenchException) {
// Generic handler for all benchmark errors (InvalidIterations, UnknownFunction, etc.)
android.util.Log.e("BenchRunner", "Benchmark error: ${e.message}", e)
"Benchmark error: ${e.message}"
} catch (e: Exception) {
android.util.Log.e("BenchRunner", "Unexpected error during benchmark execution", e)
"Unexpected error: ${e.message}"
}
findViewById<TextView>(R.id.result_text)?.text = display
// Keep the report visible briefly so local smoke runs and remote automation
// have a stable window to read the results.
android.util.Log.i("BenchRunner", "Displaying results for 5 seconds for capture output...")
Thread.sleep(5000)
android.util.Log.i("BenchRunner", "Display hold complete")
}
/**
* 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 logBenchReport(report: BenchReport) {
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 samples = report.samples.map { it.durationNs.toLong() }
val sampleArray = JSONArray()
samples.forEach { sampleArray.put(it) }
json.put("samples_ns", sampleArray)
val phases = JSONArray()
report.phases.forEach { phase ->
val phaseJson = JSONObject()
phaseJson.put("name", phase.name)
phaseJson.put("duration_ns", phase.durationNs.toLong())
phases.put(phaseJson)
}
json.put("phases", phases)
if (samples.isNotEmpty()) {
val min = samples.minOrNull() ?: 0L
val max = samples.maxOrNull() ?: 0L
val avg = samples.sum().toDouble() / samples.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 memInfo = Debug.MemoryInfo()
Debug.getMemoryInfo(memInfo)
val resources = JSONObject()
report.resourceUsage?.cpuMedianMs?.let { resources.put("cpu_median_ms", it.toLong()) }
report.resourceUsage?.peakMemoryKb?.let { resources.put("peak_memory_kb", it.toLong()) }
resources.put("uptime_ms", SystemClock.elapsedRealtime())
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)
android.util.Log.i("BenchRunner", "BENCH_JSON ${json}")
}
private fun resolveBenchParams(): BenchParams {
val assetParams = loadBenchParamsFromAssets()
val defaults = assetParams ?: BenchParams(
DEFAULT_FUNCTION,
DEFAULT_ITERATIONS,
DEFAULT_WARMUP
)
// 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
// 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)
}
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
}
android.util.Log.i("BenchRunner", "Loaded config from bench_spec.json: function=$function, iterations=$iterations, warmup=$warmup")
BenchParams(function, iterations, warmup)
}
} 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
}
}
}