messenger-android

Android graphical user interfaces for GNUnet Messenger
Log | Files | Refs | README | LICENSE

commit 87acbbac53e25e83a336d8a0af2b93dc524c41c0
parent c798cf50578bb04104be1e7dbc3f5a367dc4a14b
Author: t3sserakt <t3ss@posteo.de>
Date:   Mon, 27 Apr 2026 20:50:31 +0200

WIP: two account perf measure

Diffstat:
MGNUnetMessenger/app/src/androidTest/java/org/gnunet/gnunetmessenger/perf/MessagePerformanceTest.kt | 63+++++++++++++++++++++++++++++++++++++++++++++++++++------------
MGNUnetMessenger/app/src/androidTest/java/org/gnunet/gnunetmessenger/perf/TwoAccountPerformanceTest.kt | 78++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------
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 } }