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,108 @@
|
||||
package com.example.aiapp
|
||||
|
||||
import android.content.Context
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
|
||||
/**
|
||||
* P0's benchmark gate (see docs/RUST.md and docs/DECISIONS.md's 2026-09-05 entry): an in-process
|
||||
* fake of the backend, so the `bench` build type can drive a real session screen -- the real
|
||||
* [TranscriptSource], the real fold, the real paging -- with no server and no network permission.
|
||||
*
|
||||
* Only ever installed when [BuildConfig.FIXTURE_MODE] is true (see [MainActivity]); everything else
|
||||
* in this build compiles it in but never calls it, since Kotlin has no per-build-type source set
|
||||
* that both [MainActivity] (which every variant compiles) and this can share without one.
|
||||
*
|
||||
* The design: [requestFromServer] and [Sse] talk to `https://$FIXTURE_HOST:$FIXTURE_PORT` through
|
||||
* ordinary `java.net.URL`, exactly as they would talk to a real server. A
|
||||
* [java.net.URLStreamHandlerFactory] registered once for the whole process intercepts every
|
||||
* `https://` connection to that host and answers from this object's in-memory event log instead of
|
||||
* opening a socket -- see BenchNetwork.kt. Everything above that (TranscriptSource, SessionScreen,
|
||||
* the fold, uniqueItems) never learns the difference.
|
||||
*/
|
||||
object BenchFixture {
|
||||
const val FIXTURE_HOST = "bench.fixture.invalid"
|
||||
const val FIXTURE_PORT = 1
|
||||
|
||||
/** How many of the fixture's events are the opening backlog; see bench-fixture/README.md. */
|
||||
private const val BACKLOG_COUNT = 3200
|
||||
|
||||
val settings = ServerSettings(FIXTURE_HOST, FIXTURE_PORT, "bench")
|
||||
|
||||
/** The session id every bench run opens; nothing else in this build ever mints one. */
|
||||
const val SESSION_ID = "bench-fixture-session"
|
||||
|
||||
/**
|
||||
* The whole transcript, seq order, growing as [pushLive] is called during the streaming phase.
|
||||
* Read by both the REST page handler and the SSE handler, so a page requested mid- stream and a
|
||||
* live frame agree on what has "already happened" -- the same thing a real server's own
|
||||
* transcript file guarantees.
|
||||
*/
|
||||
private val log = CopyOnWriteArrayList<Pair<String, SeqEvent>>()
|
||||
|
||||
/** The events not yet appended to [log] -- the streaming phase's own source. */
|
||||
private var streamTail: List<Pair<String, SeqEvent>> = emptyList()
|
||||
|
||||
private val images = mutableMapOf<String, ByteArray>()
|
||||
|
||||
@Volatile private var loaded = false
|
||||
|
||||
/**
|
||||
* Parses the bundled fixture once. Safe to call more than once; only the first does anything.
|
||||
*/
|
||||
@Synchronized
|
||||
fun ensureLoaded(context: Context) {
|
||||
if (loaded) return
|
||||
val lines =
|
||||
context.assets.open("transcript.jsonl").bufferedReader().readLines().filter {
|
||||
it.isNotBlank()
|
||||
}
|
||||
val parsed = lines.map { it to parseSeqEvent(it) }
|
||||
log.addAll(parsed.take(BACKLOG_COUNT))
|
||||
streamTail = parsed.drop(BACKLOG_COUNT)
|
||||
for (name in listOf("bench1.png", "bench2.png")) {
|
||||
images[name] = context.assets.open(name).readBytes()
|
||||
}
|
||||
loaded = true
|
||||
}
|
||||
|
||||
/** The events the streaming phase has left to send. */
|
||||
fun remainingStreamEvents(): Int = streamTail.size
|
||||
|
||||
/** Sends the next fixture event onto the live log, as a real SSE frame would arrive. */
|
||||
fun pushNextLiveEvent(): Boolean {
|
||||
val next = streamTail.firstOrNull() ?: return false
|
||||
streamTail = streamTail.drop(1)
|
||||
log.add(next)
|
||||
return true
|
||||
}
|
||||
|
||||
/** Undoes [pushNextLiveEvent] and reloads the opening backlog, for running the bench twice. */
|
||||
@Synchronized
|
||||
fun resetToBacklog(context: Context) {
|
||||
loaded = false
|
||||
log.clear()
|
||||
ensureLoaded(context)
|
||||
}
|
||||
|
||||
fun fileBytes(name: String): ByteArray? = images[name]
|
||||
|
||||
/**
|
||||
* Raw JSON lines with seq > [after], in order -- what an `/events?after=` connection replays.
|
||||
*/
|
||||
fun linesAfter(after: Long): List<String> =
|
||||
log.filter { it.second.seq > after }.map { it.first }
|
||||
|
||||
/**
|
||||
* One REST page: [fetchTranscript]'s `before`/`limit`/`after`, against the growing log. Ignores
|
||||
* `coalesce` -- the fixture's own deltas are already split the way a real reply streams, and
|
||||
* what the benchmark exercises is the fold and the paging, not the server's row-joining, which
|
||||
* client-core's own port tracks separately (CLIENT_CORE.md).
|
||||
*/
|
||||
fun page(before: Long?, limit: Int, after: Long?): List<String> {
|
||||
val upper = before ?: (log.lastOrNull()?.second?.seq?.plus(1) ?: 1L)
|
||||
val candidates = log.filter {
|
||||
it.second.seq < upper && (after == null || it.second.seq > after)
|
||||
}
|
||||
return candidates.takeLast(limit).map { it.first }
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user