Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 71 additions & 4 deletions core/src/main/java/org/monogram/core/perf/ChatOpenPerfBridge.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,19 @@ data class ChatOpenPerfSessionSnapshot(
val requestCount: Int,
val replyFetchCount: Int,
val persistCount: Int,
val persistSkippedCount: Int
val persistSkippedCount: Int,
val firstContentLatencyMs: Long? = null,
val settledLatencyMs: Long? = null,
val shadowMismatchCount: Int = 0
)

data class ChatOpenPerfReportSnapshot(
val completedSessions: List<ChatOpenPerfSessionSnapshot>
) {
val sessionCount: Int get() = completedSessions.size
val shadowMismatchCount: Int get() = completedSessions.sumOf { it.shadowMismatchCount }
}

object ChatOpenPerfBridge {
private data class SessionRecord(
val sessionId: String,
Expand All @@ -21,10 +31,15 @@ object ChatOpenPerfBridge {
var requestCount: Int = 0,
var replyFetchCount: Int = 0,
var persistCount: Int = 0,
var persistSkippedCount: Int = 0
var persistSkippedCount: Int = 0,
val startedAtMs: Long = nowMs(),
var firstContentLatencyMs: Long? = null,
var settledLatencyMs: Long? = null,
var shadowMismatchCount: Int = 0
)

private val sessions = ConcurrentHashMap<String, SessionRecord>()
private val completed = ArrayDeque<ChatOpenPerfSessionSnapshot>()

fun startSession(
chatId: Long,
Expand Down Expand Up @@ -87,14 +102,61 @@ object ChatOpenPerfBridge {
}
}

fun markFirstContent(chatId: Long, threadId: Long?): ChatOpenPerfSessionSnapshot? =
markSession(chatId, threadId) { record ->
if (record.firstContentLatencyMs == null) {
record.firstContentLatencyMs = elapsedSince(record.startedAtMs)
}
}

fun markSettled(chatId: Long, threadId: Long?): ChatOpenPerfSessionSnapshot? =
markSession(chatId, threadId) { record ->
if (record.settledLatencyMs == null) {
record.settledLatencyMs = elapsedSince(record.startedAtMs)
}
}

fun recordShadowMismatch(chatId: Long, threadId: Long?): ChatOpenPerfSessionSnapshot? =
markSession(chatId, threadId) { it.shadowMismatchCount += 1 }

/** Returns a bounded process-local report for device/reference-corpus harnesses. */
fun report(): ChatOpenPerfReportSnapshot = synchronized(completed) {
ChatOpenPerfReportSnapshot(completed.toList())
}

fun resetReport() = synchronized(completed) { completed.clear() }

fun clearSession(chatId: Long, threadId: Long?, sessionId: String? = null) {
val key = key(chatId, threadId)
val record = sessions[key] ?: return
if (sessionId == null || record.sessionId == sessionId) {
sessions.remove(key, record)
if (sessions.remove(key, record)) {
synchronized(record) {
synchronized(completed) {
if (completed.size == MAX_COMPLETED_SESSIONS) completed.removeFirst()
completed.addLast(record.snapshot())
}
}
}
}
}

private fun markSession(
chatId: Long,
threadId: Long?,
block: (SessionRecord) -> Unit
): ChatOpenPerfSessionSnapshot? {
val record = findRecord(chatId, threadId) ?: return null
synchronized(record) {
block(record)
return record.snapshot()
}
}

private fun elapsedSince(startedAtMs: Long): Long = (nowMs() - startedAtMs).coerceAtLeast(0L)

private fun nowMs(): Long = System.nanoTime() / 1_000_000L

private fun findRecord(chatId: Long, threadId: Long?): SessionRecord? {
sessions[key(chatId, threadId)]?.let { return it }
return sessions.entries.firstOrNull { entry ->
Expand All @@ -112,7 +174,12 @@ object ChatOpenPerfBridge {
requestCount = requestCount,
replyFetchCount = replyFetchCount,
persistCount = persistCount,
persistSkippedCount = persistSkippedCount
persistSkippedCount = persistSkippedCount,
firstContentLatencyMs = firstContentLatencyMs,
settledLatencyMs = settledLatencyMs,
shadowMismatchCount = shadowMismatchCount
)
}

private const val MAX_COMPLETED_SESSIONS = 256
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package org.monogram.core.perf

import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Test

class ChatOpenPerfBridgeTest {

@Test
fun `completed session keeps latency counters and report is resettable`() {
ChatOpenPerfBridge.resetReport()
ChatOpenPerfBridge.startSession(42L, null, "initial", "resolving")
ChatOpenPerfBridge.recordHistoryRequest(42L, null)
ChatOpenPerfBridge.markFirstContent(42L, null)
ChatOpenPerfBridge.recordShadowMismatch(42L, null)
ChatOpenPerfBridge.markSettled(42L, null)
ChatOpenPerfBridge.clearSession(42L, null)

val report = ChatOpenPerfBridge.report()
assertEquals(1, report.sessionCount)
val session = report.completedSessions.single()
assertEquals(1, session.requestCount)
assertNotNull(session.firstContentLatencyMs)
assertNotNull(session.settledLatencyMs)
assertEquals(1, session.shadowMismatchCount)

ChatOpenPerfBridge.resetReport()
assertEquals(0, ChatOpenPerfBridge.report().sessionCount)
assertNull(ChatOpenPerfBridge.currentSession(42L, null))
}
}
11 changes: 11 additions & 0 deletions data/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ android {

defaultConfig {
minSdk = 25
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles("consumer-rules.pro")

ndk {
Expand Down Expand Up @@ -48,6 +49,9 @@ android {
getByName("main") {
jniLibs.directories.clear()
}
getByName("androidTest") {
assets.srcDir(file("schemas"))
}
getByName("official") {
jniLibs.directories.add("src/official/jniLibs")
}
Expand Down Expand Up @@ -87,6 +91,10 @@ android {
}
}

ksp {
arg("room.schemaLocation", file("schemas").path)
}

dependencies {
implementation(project(":core"))
implementation(project(":domain"))
Expand All @@ -109,4 +117,7 @@ dependencies {
testImplementation(libs.junit)
testImplementation(libs.ktor.client.mock)
testImplementation(libs.kotlinx.coroutines.test)
androidTestImplementation(libs.androidx.room.testing)
androidTestImplementation(libs.androidx.test.ext.junit)
androidTestImplementation(libs.androidx.test.runner)
}
Loading