mobench-sdk 0.1.28

Rust SDK for mobile benchmarking with timing harness and Android/iOS builders
Documentation
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
        }
    }
}