commit 87acbbac53e25e83a336d8a0af2b93dc524c41c0
parent c798cf50578bb04104be1e7dbc3f5a367dc4a14b
Author: t3sserakt <t3ss@posteo.de>
Date: Mon, 27 Apr 2026 20:50:31 +0200
WIP: two account perf measure
Diffstat:
2 files changed, 113 insertions(+), 28 deletions(-)
diff --git a/GNUnetMessenger/app/src/androidTest/java/org/gnunet/gnunetmessenger/perf/MessagePerformanceTest.kt b/GNUnetMessenger/app/src/androidTest/java/org/gnunet/gnunetmessenger/perf/MessagePerformanceTest.kt
@@ -9,6 +9,7 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeout
+import kotlin.time.Duration.Companion.minutes
import org.gnunet.gnunetmessenger.model.ChatAccount
import org.gnunet.gnunetmessenger.model.ChatContext
import org.gnunet.gnunetmessenger.model.ChatHandle
@@ -58,19 +59,20 @@ class MessagePerformanceTest {
launch.addFlags(android.content.Intent.FLAG_ACTIVITY_NEW_TASK)
appContext.startActivity(launch)
// Give the server process and its native daemon some time to come up.
- Thread.sleep(5_000)
+ Thread.sleep(15_000)
}
}
@After
- fun tearDown() = runTest {
+ fun tearDown() = runTest(timeout = 1.minutes) {
gnunetChat.unbind()
delay(500)
}
@Test
- fun singleAccountGroupLoopback_measuresLatencyAndThroughput() = runTest {
+ fun singleAccountGroupLoopback_measuresLatencyAndThroughput() = runTest(timeout = 5.minutes) {
val allDone = CompletableDeferred<Unit>()
+ val probeDone = CompletableDeferred<Unit>()
val expectedTotal = warmupMessages + measuredMessages
var seenCount = 0
var firstRecvAtNs: Long = 0L
@@ -96,6 +98,12 @@ class MessagePerformanceTest {
}
val seq = extractPerfSeq(msg.text) ?: return@startChat
+ // Probe message — separate signal, doesn't count as measurement traffic.
+ if (seq == PROBE_SEQ) {
+ if (!probeDone.isCompleted) probeDone.complete(Unit)
+ return@startChat
+ }
+
val now = System.nanoTime()
val wasFirst = (firstRecvAtNs == 0L)
if (wasFirst) firstRecvAtNs = now
@@ -134,7 +142,27 @@ class MessagePerformanceTest {
val group = gnunetChat.createGroup(handle, "perf-group-${System.currentTimeMillis()}")
val groupCtx: ChatContext = gnunetChat.getGroupContext(group)
// Give the group a moment to be fully set up on the server side.
- withContext(Dispatchers.Default.limitedParallelism(1)) { delay(500) }
+ withContext(Dispatchers.Default.limitedParallelism(1)) { delay(2_000) }
+
+ // Connectivity probe: send one message and wait for it to round-trip
+ // before doing the perf run. If this fails, the daemon is wedged and
+ // measuring 500 doomed sends would just be a 60s silent timeout.
+ Log.i(TAG, "Sending loopback probe…")
+ withContext(Dispatchers.IO) { gnunetChat.sendText(groupCtx, "${perfTag(PROBE_SEQ)} probe") }
+ try {
+ withContext(Dispatchers.Default.limitedParallelism(1)) {
+ withTimeout(30_000) { probeDone.await() }
+ }
+ Log.i(TAG, "Probe round-tripped — daemon loopback is alive.")
+ } catch (t: Throwable) {
+ throw AssertionError(
+ "Loopback probe did not return within 30s. The daemon is not " +
+ "echoing TEXT messages back through the callback. " +
+ "Try: `adb shell am force-stop $SERVER_PACKAGE && " +
+ "adb shell pm clear $SERVER_PACKAGE` and re-run.",
+ t,
+ )
+ }
Log.i(TAG, "Starting perf run: warmup=$warmupMessages measured=$measuredMessages")
@@ -192,7 +220,7 @@ class MessagePerformanceTest {
if (pre != null) {
Log.i(TAG, "Account '$name' already exists; using it.")
gnunetChat.connect(handle, pre)
- withContext(Dispatchers.Default.limitedParallelism(1)) { delay(1_500) }
+ withContext(Dispatchers.Default.limitedParallelism(1)) { delay(5_000) }
return pre
}
@@ -201,30 +229,41 @@ class MessagePerformanceTest {
assertEquals("createAccount('$name')", GnunetReturnValue.OK, res)
val account = withContext(Dispatchers.Default.limitedParallelism(1)) {
- withTimeout(15_000) {
+ withTimeout(120_000) {
var found: ChatAccount? = null
var attempt = 0
+ val acceptAnyAfterMs = 30_000L
+ val started = System.currentTimeMillis()
while (found == null) {
val accounts = runCatching { gnunetChat.listAccounts(handle) }
.getOrDefault(emptyList())
- if (attempt % 5 == 0) {
- Log.i(TAG, "listAccounts attempt=$attempt size=${accounts.size} " +
- accounts.joinToString(",") { "'${it.name}'" })
- }
+ Log.i(TAG, "listAccounts attempt=$attempt size=${accounts.size} " +
+ accounts.joinToString(",") { "'${it.name}'" })
found = accounts.firstOrNull { it.name.equals(name, ignoreCase = true) }
- if (found == null) delay(200)
+ if (found == null && accounts.isNotEmpty() &&
+ System.currentTimeMillis() - started > acceptAnyAfterMs
+ ) {
+ // Daemon didn't surface our exact name fast enough but it
+ // does have *some* account — use it. The perf test only
+ // needs a usable account, not specifically '$name'.
+ Log.w(TAG, "Falling back to first available account '${accounts[0].name}' " +
+ "after ${acceptAnyAfterMs/1000}s waiting for '$name'.")
+ found = accounts[0]
+ }
+ if (found == null) delay(500)
attempt++
}
found
}
}
gnunetChat.connect(handle, account)
- withContext(Dispatchers.Default.limitedParallelism(1)) { delay(1_500) }
+ withContext(Dispatchers.Default.limitedParallelism(1)) { delay(5_000) }
return account
}
companion object {
private const val TAG = "MessagePerfTest"
private const val SERVER_PACKAGE = "org.gnu.gnunet"
+ private const val PROBE_SEQ = Long.MAX_VALUE
}
}
diff --git a/GNUnetMessenger/app/src/androidTest/java/org/gnunet/gnunetmessenger/perf/TwoAccountPerformanceTest.kt b/GNUnetMessenger/app/src/androidTest/java/org/gnunet/gnunetmessenger/perf/TwoAccountPerformanceTest.kt
@@ -12,6 +12,7 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeout
+import kotlin.time.Duration.Companion.minutes
import org.gnunet.gnunetmessenger.model.ChatAccount
import org.gnunet.gnunetmessenger.model.ChatContext
import org.gnunet.gnunetmessenger.model.ChatHandle
@@ -58,7 +59,7 @@ class TwoAccountPerformanceTest {
private val recorder = LatencyRecorder()
private val warmupPerClient = 10
- private val measuredPerClient = 500
+ private val measuredPerClient = 100
/**
* Same boot-trick as Phase 1: the GNUnet scheduler is started by the
@@ -71,19 +72,19 @@ class TwoAccountPerformanceTest {
if (launch != null) {
launch.addFlags(android.content.Intent.FLAG_ACTIVITY_NEW_TASK)
appContext.startActivity(launch)
- Thread.sleep(5_000)
+ Thread.sleep(15_000)
}
}
@After
- fun tearDown() = runTest {
+ fun tearDown() = runTest(timeout = 1.minutes) {
runCatching { clientAlpha.unbind() }
runCatching { clientBeta.unbind() }
delay(500)
}
@Test
- fun twoSessionParallelLoopback_measuresLatencyAndThroughput() = runTest {
+ fun twoSessionParallelLoopback_measuresLatencyAndThroughput() = runTest(timeout = 5.minutes) {
val pm = appContext.packageManager
val serverInstalled = runCatching {
pm.getPackageInfo(SERVER_PACKAGE, 0); true
@@ -101,6 +102,8 @@ class TwoAccountPerformanceTest {
val alphaDone = CompletableDeferred<Unit>()
val betaDone = CompletableDeferred<Unit>()
+ val alphaProbe = CompletableDeferred<Unit>()
+ val betaProbe = CompletableDeferred<Unit>()
var alphaSeen = 0
var betaSeen = 0
var alphaFirstRecvNs = 0L
@@ -114,6 +117,10 @@ class TwoAccountPerformanceTest {
return@startChat
}
val seq = extractPerfSeq(msg.text) ?: return@startChat
+ if (seq == ALPHA_PROBE_SEQ) {
+ if (!alphaProbe.isCompleted) alphaProbe.complete(Unit)
+ return@startChat
+ }
// Only count messages this session emitted (its own loopback).
if (seq < alphaSeqStart || seq >= alphaSeqStart + perClientTotal) return@startChat
@@ -135,6 +142,10 @@ class TwoAccountPerformanceTest {
return@startChat
}
val seq = extractPerfSeq(msg.text) ?: return@startChat
+ if (seq == BETA_PROBE_SEQ) {
+ if (!betaProbe.isCompleted) betaProbe.complete(Unit)
+ return@startChat
+ }
if (seq < betaSeqStart || seq >= betaSeqStart + perClientTotal) return@startChat
val now = System.nanoTime()
@@ -182,7 +193,34 @@ class TwoAccountPerformanceTest {
)
val ctxAlpha: ChatContext = clientAlpha.getGroupContext(groupAlpha)
val ctxBeta: ChatContext = clientBeta.getGroupContext(groupBeta)
- withContext(Dispatchers.Default.limitedParallelism(1)) { delay(500) }
+ withContext(Dispatchers.Default.limitedParallelism(1)) { delay(2_000) }
+
+ // Connectivity probe per session before measuring.
+ Log.i(TAG, "Sending loopback probes…")
+ withContext(Dispatchers.IO) {
+ clientAlpha.sendText(ctxAlpha, "${perfTag(ALPHA_PROBE_SEQ)} probe")
+ clientBeta.sendText(ctxBeta, "${perfTag(BETA_PROBE_SEQ)} probe")
+ }
+ try {
+ withContext(Dispatchers.Default.limitedParallelism(2)) {
+ withTimeout(30_000) {
+ coroutineScope {
+ val a = async { alphaProbe.await() }
+ val b = async { betaProbe.await() }
+ awaitAll(a, b)
+ }
+ }
+ }
+ Log.i(TAG, "Both probes round-tripped — daemon loopback is alive.")
+ } catch (t: Throwable) {
+ throw AssertionError(
+ "Loopback probe did not return for one or both sessions within 30s. " +
+ "alphaProbe=${alphaProbe.isCompleted} betaProbe=${betaProbe.isCompleted}. " +
+ "Try `adb shell am force-stop $SERVER_PACKAGE && " +
+ "adb shell pm clear $SERVER_PACKAGE` and re-run.",
+ t,
+ )
+ }
Log.i(
TAG,
@@ -290,7 +328,7 @@ class TwoAccountPerformanceTest {
if (pre != null) {
Log.i(TAG, "[$name] account already exists; reusing.")
client.connect(handle, pre)
- withContext(Dispatchers.Default.limitedParallelism(1)) { delay(1_500) }
+ withContext(Dispatchers.Default.limitedParallelism(1)) { delay(5_000) }
return pre
}
@@ -299,33 +337,41 @@ class TwoAccountPerformanceTest {
assertEquals("createAccount('$name')", GnunetReturnValue.OK, res)
val account = withContext(Dispatchers.Default.limitedParallelism(1)) {
- withTimeout(15_000) {
+ withTimeout(120_000) {
var found: ChatAccount? = null
var attempt = 0
+ val acceptAnyAfterMs = 30_000L
+ val started = System.currentTimeMillis()
while (found == null) {
val accounts = runCatching { client.listAccounts(handle) }
.getOrDefault(emptyList())
- if (attempt % 5 == 0) {
- Log.i(
- TAG,
- "[$name] listAccounts attempt=$attempt size=${accounts.size} " +
- accounts.joinToString(",") { "'${it.name}'" },
- )
- }
+ Log.i(
+ TAG,
+ "[$name] listAccounts attempt=$attempt size=${accounts.size} " +
+ accounts.joinToString(",") { "'${it.name}'" },
+ )
found = accounts.firstOrNull { it.name.equals(name, ignoreCase = true) }
- if (found == null) delay(200)
+ if (found == null && accounts.isNotEmpty() &&
+ System.currentTimeMillis() - started > acceptAnyAfterMs
+ ) {
+ Log.w(TAG, "[$name] Falling back to '${accounts[0].name}'.")
+ found = accounts[0]
+ }
+ if (found == null) delay(500)
attempt++
}
found
}
}
client.connect(handle, account)
- withContext(Dispatchers.Default.limitedParallelism(1)) { delay(1_500) }
+ withContext(Dispatchers.Default.limitedParallelism(1)) { delay(5_000) }
return account
}
companion object {
private const val TAG = "TwoAcctPerfTest"
private const val SERVER_PACKAGE = "org.gnu.gnunet"
+ private const val ALPHA_PROBE_SEQ = Long.MAX_VALUE
+ private const val BETA_PROBE_SEQ = Long.MAX_VALUE - 1
}
}