app: fixture-mode session screen and a "Run benchmark" control
BenchFixture.kt/BenchNetwork.kt fake the backend for the bench build: a URLStreamHandlerFactory installed only under BuildConfig.FIXTURE_MODE answers TranscriptSource/EventStream's requests from an in-memory copy of the bundled fixture instead of opening a socket, so the fold, the paging and uniqueItems under test are the screen's real ones rather than a shortcut built for this. MainActivity opens straight onto that session when FIXTURE_MODE is set, with no enrollment and no permission prompts. BenchRun.kt drives the same scroll loop and streaming phase transcript-bench.sh/stream-bench.sh drive over ui-trace, but in-process (24 swipes through the real LazyListState, then 400 fixture events appended at 20/s through the real live-fold path), and adds process CPU time, peak RSS and battery current to the render report -- "unavailable" rather than a fabricated number where the device can't answer. "Run benchmark" sits beside the existing "Copy" in session settings, found by that exact label the way every other control here is (SessionSettingsDialog's onRunBenchmark, null on every build but bench). debugReport gained an optional `extra` section for this; empty and invisible on every other build's report. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
a6cb9a9082
commit
e6c884a0cd
7 files changed
+537
-1
No files matched your search
@@ -0,0 +1,143 @@
|
||||
package com.example.aiapp
|
||||
|
||||
import android.content.Context
|
||||
import android.os.BatteryManager
|
||||
import android.os.Process
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.gestures.animateScrollBy
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import java.io.File
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* P0's scripted benchmark, run in-process instead of by a shell script: the phone has no usable
|
||||
* system tracing (this-machine-android's skill) and no agent can drive it, so the same scroll loop
|
||||
* and streaming phase `transcript-bench.sh`/`stream-bench.sh` drive over `ui-trace` are reproduced
|
||||
* here against [LazyListState] and [BenchFixture] directly. Only reachable from the `bench` build
|
||||
* (see [SessionSettingsDialog]'s `onRunBenchmark`), but compiled into every build for the reason
|
||||
* [BenchFixture]'s doc comment gives.
|
||||
*/
|
||||
object BenchRun {
|
||||
/** transcript-bench.sh's default: 6 cycles of 4 swipes each, 900px over 200ms, 500ms apart. */
|
||||
private const val CYCLES = 6
|
||||
private const val SWIPE_PX = 900f
|
||||
private const val SWIPE_MS = 200
|
||||
private const val SWIPE_PAUSE_MS = 500L
|
||||
|
||||
/** stream-bench.sh's shape: a real reply arrives as many small deltas, not one big write. */
|
||||
private const val STREAM_EVENTS_PER_SEC = 20
|
||||
private const val STREAM_SECONDS = 20
|
||||
|
||||
/**
|
||||
* Scrolls, then streams, then returns the extra report lines P0 asked for (CPU time, peak RSS,
|
||||
* battery current) -- [FrameStats] and [DebugStats] are reset first, exactly as
|
||||
* `copyRenderReport` resets them, so the two accountings cover the same stretch of work.
|
||||
*/
|
||||
suspend fun run(
|
||||
context: Context,
|
||||
scope: CoroutineScope,
|
||||
listState: LazyListState,
|
||||
): List<String> {
|
||||
FrameStats.reset()
|
||||
DebugStats.reset()
|
||||
val cpuStartMs = Process.getElapsedCpuTime()
|
||||
|
||||
val battery = BatterySampler(context)
|
||||
// Launched in the caller's scope rather than a fresh coroutineScope{} here, which would
|
||||
// suspend this function until the sampler job ended -- and it only ends when told to.
|
||||
val samplerJob = scope.launch {
|
||||
while (isActive) {
|
||||
battery.sample()
|
||||
delay(1000)
|
||||
}
|
||||
}
|
||||
|
||||
// The swipe loop: transcript-bench.sh's four swipes per cycle are two drags toward newer
|
||||
// content and two back, so a cycle returns to where it started and the whole loop measures
|
||||
// steady-state scrolling rather than travelling somewhere new each time.
|
||||
repeat(CYCLES) {
|
||||
repeat(2) {
|
||||
listState.animateScrollBy(SWIPE_PX, tween(SWIPE_MS))
|
||||
delay(SWIPE_PAUSE_MS)
|
||||
}
|
||||
repeat(2) {
|
||||
listState.animateScrollBy(-SWIPE_PX, tween(SWIPE_MS))
|
||||
delay(SWIPE_PAUSE_MS)
|
||||
}
|
||||
}
|
||||
|
||||
// Pinned to the newest end before streaming starts, the way stream-bench.sh's "Jump to
|
||||
// latest" tap is -- a reply streamed into a list parked further back arrives off-screen and
|
||||
// the report would show nothing happened.
|
||||
listState.scrollToItem(0)
|
||||
|
||||
var sent = 0
|
||||
val total = STREAM_EVENTS_PER_SEC * STREAM_SECONDS
|
||||
while (sent < total && BenchFixture.remainingStreamEvents() > 0) {
|
||||
BenchFixture.pushNextLiveEvent()
|
||||
sent++
|
||||
delay(1000L / STREAM_EVENTS_PER_SEC)
|
||||
}
|
||||
// Lets the last few deltas land and draw before the report is read.
|
||||
delay(300)
|
||||
|
||||
samplerJob.cancel()
|
||||
val cpuMs = Process.getElapsedCpuTime() - cpuStartMs
|
||||
val rssLine = peakRssLine()
|
||||
val batteryLine = battery.finish()
|
||||
|
||||
return listOf(
|
||||
" scroll: $CYCLES cycles (${CYCLES * 4} swipes), streamed $sent/$total fixture events",
|
||||
" process CPU time over this run: ${cpuMs}ms",
|
||||
rssLine,
|
||||
batteryLine,
|
||||
)
|
||||
}
|
||||
|
||||
/** VmHWM from /proc/self/status: the process's high-water mark, in kB, since it started. */
|
||||
private fun peakRssLine(): String {
|
||||
val kb =
|
||||
try {
|
||||
File("/proc/self/status")
|
||||
.readLines()
|
||||
.firstOrNull { it.startsWith("VmHWM:") }
|
||||
?.trim()
|
||||
?.removePrefix("VmHWM:")
|
||||
?.trim()
|
||||
?.removeSuffix("kB")
|
||||
?.trim()
|
||||
?.toLongOrNull()
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
return " peak RSS: " +
|
||||
(kb?.let { "${it}kB" } ?: "unavailable (/proc/self/status unreadable)")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Samples [BatteryManager.BATTERY_PROPERTY_CURRENT_NOW] (microamps) once a second for the length of
|
||||
* a run. The property returns `Int.MIN_VALUE` on hardware that does not support it -- most
|
||||
* emulators -- and that is reported as "unavailable" rather than folded into an average with the
|
||||
* real samples, which would silently understate every number after it. See UI_RULES: never present
|
||||
* an inferred value as a measured one.
|
||||
*/
|
||||
private class BatterySampler(context: Context) {
|
||||
private val manager = context.getSystemService(BatteryManager::class.java)
|
||||
private val samples = mutableListOf<Int>()
|
||||
|
||||
fun sample() {
|
||||
val value = manager?.getIntProperty(BatteryManager.BATTERY_PROPERTY_CURRENT_NOW)
|
||||
if (value != null && value != Int.MIN_VALUE) samples.add(value)
|
||||
}
|
||||
|
||||
fun finish(): String {
|
||||
if (samples.isEmpty()) return " battery current: unavailable on this device"
|
||||
val meanUa = samples.sum() / samples.size
|
||||
return " battery current: mean ${meanUa}µA over ${samples.size} samples" +
|
||||
" (min ${samples.min()}, max ${samples.max()})"
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user