From 54584c213f7535eb8f15dd9f8cc41b70719f52eb Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 27 Apr 2026 02:30:13 +0000 Subject: [PATCH] perf(audio-rooms): hoist ParticipantsGrid per-cell allocations (audit #9) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Each cell of the participant grid was allocating fresh Modifier chains and lambdas on every recompose. With a 50-speaker room and the grid recomposing on every connectingSpeakers / speakingNow / reactions flip, the per-frame allocation count adds up. Hoist the constants: * gridModifier (fillMaxWidth + height + padding) * cellWidthModifier (avatarSize + 16.dp) * absentAlphaModifier (alpha 0.5f) * speakingBorderModifier (border + CircleShape) * spinnerModifier (size avatarSize - 8.dp) Replace the `.let { ... }.let { ... }` Modifier chain on the avatar with a `when` over the four (isSpeaking × absent) cases — each case picks a pre-built Modifier rather than synthesising a fresh chain. Cache the per-pubkey long-click adapter via remember(pubkey, onLongPressParticipant) so the `{ hex -> cb(hex) }` wrapper isn't re-allocated on every recompose. --- .../audiorooms/room/ParticipantsGrid.kt | 47 ++++++++++++++----- 1 file changed, 35 insertions(+), 12 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/audiorooms/room/ParticipantsGrid.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/audiorooms/room/ParticipantsGrid.kt index 9f85d0487f..27a9cc72bf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/audiorooms/room/ParticipantsGrid.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/audiorooms/room/ParticipantsGrid.kt @@ -36,6 +36,7 @@ import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha @@ -120,6 +121,20 @@ private fun ParticipantsSection( onLongPressParticipant: ((String) -> Unit)?, ) { val ringColor = MaterialTheme.colorScheme.primary + // Hoist the size/spacing modifiers — same Dp values for every + // cell, so allocating them once per section beats allocating + // per-cell-recompose with N speakers. + val gridModifier = + remember(avatarSize) { + Modifier + .fillMaxWidth() + .height(avatarSize + 48.dp) + .padding(top = 4.dp) + } + val cellWidthModifier = remember(avatarSize) { Modifier.width(avatarSize + 16.dp) } + val absentAlphaModifier = remember { Modifier.alpha(0.5f) } + val speakingBorderModifier = remember(ringColor) { Modifier.border(2.dp, ringColor, CircleShape) } + val spinnerModifier = remember(avatarSize) { Modifier.size(avatarSize - 8.dp) } Column(modifier = Modifier.padding(top = 8.dp)) { Text( text = title, @@ -132,28 +147,36 @@ private fun ParticipantsSection( // bubble vertically without clipping. LazyHorizontalGrid( rows = GridCells.Fixed(1), - modifier = - Modifier - .fillMaxWidth() - .height(avatarSize + 48.dp) - .padding(top = 4.dp), + modifier = gridModifier, horizontalArrangement = Arrangement.spacedBy(6.dp), ) { items(items = members, key = { it.pubkey }) { member -> val isSpeaking = member.pubkey in speakingNow val avatarModifier = - Modifier - .let { if (isSpeaking) it.border(2.dp, ringColor, CircleShape) else it } - .let { if (member.absent) it.alpha(0.5f) else it } + when { + isSpeaking && member.absent -> speakingBorderModifier.then(absentAlphaModifier) + isSpeaking -> speakingBorderModifier + member.absent -> absentAlphaModifier + else -> Modifier + } val user = - androidx.compose.runtime.remember(member.pubkey) { + remember(member.pubkey) { com.vitorpamplona.amethyst.model.LocalCache .getOrCreateUser(member.pubkey) } + // Cache the long-click adapter per (pubkey, callback) + // tuple — `onLongPressParticipant?.let { cb -> { hex -> cb(hex) } }` + // would otherwise allocate a fresh lambda on every + // recompose, which adds up across N members during a + // connectingSpeakers / speakingNow flip. + val onLongClick = + remember(member.pubkey, onLongPressParticipant) { + onLongPressParticipant?.let { cb -> { hex: String -> cb(hex) } } + } val isConnecting = member.pubkey in connectingSpeakers Column( horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier.width(avatarSize + 16.dp), + modifier = cellWidthModifier, ) { androidx.compose.foundation.layout.Box(contentAlignment = Alignment.Center) { ClickableUserPicture( @@ -161,7 +184,7 @@ private fun ParticipantsSection( size = avatarSize, accountViewModel = accountViewModel, modifier = avatarModifier, - onLongClick = onLongPressParticipant?.let { cb -> { hex -> cb(hex) } }, + onLongClick = onLongClick, ) if (isConnecting) { // Pre-roll buffering overlay — visible @@ -171,7 +194,7 @@ private fun ParticipantsSection( // the avatar so the user picture stays // recognisable underneath. androidx.compose.material3.CircularProgressIndicator( - modifier = Modifier.size(avatarSize - 8.dp), + modifier = spinnerModifier, strokeWidth = 2.dp, color = MaterialTheme.colorScheme.primary, )