mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-11 08:47:33 +00:00
feat(ledger): live memory card on the usage screen; retire the debug-only chip
Moves the debug-build memory chip (home top bar) into the resource-usage screen as a "Memory right now" card visible to everyone: app heap, native heap, Coil RAM/disk cache fill, LocalCache note/profile/addressable/ chatroom counts, and the device memory class — refreshed every 2s only while the screen is composed (same off-main collection the chip used, DiskLruCache.size() contends with journal I/O). The same snapshot is appended to the NIP-17 usage report (both from the screen's send button and the high-consumption dialog), so a report now also shows whether the device was under memory pressure at send time. MemoryUsageChip.kt is deleted and the chip call removed from UserDrawerSearchTopBar — the debug feature is retired from the home UI. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016RJ8EAsdkHx5WHHU2eQJ1P
This commit is contained in:
+10
-4
@@ -29,9 +29,11 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.pluralStringResource
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.collectMemorySnapshot
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.routeToMessage
|
||||
@@ -60,6 +62,7 @@ fun DisplayResourceUsageAlert(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val alert = remember { mutableStateOf<ResourceUsageAlerts.Alert?>(null) }
|
||||
|
||||
LaunchedEffect(accountViewModel) {
|
||||
@@ -109,10 +112,13 @@ fun DisplayResourceUsageAlert(
|
||||
Button(onClick = {
|
||||
nav.nav {
|
||||
val report =
|
||||
ResourceUsageReportAssembler().buildReport(
|
||||
Amethyst.instance.resourceUsage.allDaysIncludingLive(),
|
||||
Amethyst.instance.resourceUsage.today(),
|
||||
)
|
||||
withContext(Dispatchers.IO) {
|
||||
ResourceUsageReportAssembler().buildReport(
|
||||
Amethyst.instance.resourceUsage.allDaysIncludingLive(),
|
||||
Amethyst.instance.resourceUsage.today(),
|
||||
collectMemorySnapshot(context),
|
||||
)
|
||||
}
|
||||
routeToMessage(
|
||||
user = LocalCache.getOrCreateUser(DEV_REPORT_PUBKEY),
|
||||
draftMessage = report,
|
||||
|
||||
+14
@@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.service.resourceusage
|
||||
|
||||
import android.os.Build
|
||||
import com.vitorpamplona.amethyst.BuildConfig
|
||||
import com.vitorpamplona.amethyst.MemorySnapshot
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
@@ -35,6 +36,7 @@ class ResourceUsageReportAssembler {
|
||||
fun buildReport(
|
||||
days: Map<Long, Map<String, Long>>,
|
||||
today: Long,
|
||||
memory: MemorySnapshot? = null,
|
||||
): String {
|
||||
val sb = StringBuilder()
|
||||
sb.append("Resource Usage Report: ")
|
||||
@@ -59,6 +61,18 @@ class ResourceUsageReportAssembler {
|
||||
sb.append("\n**Last 7 days**\n\n")
|
||||
sb.append(summaryTable(UsageSummary.fromDays(weekCounters)))
|
||||
|
||||
if (memory != null) {
|
||||
sb.append("\n**Memory right now**\n\n")
|
||||
sb.append("| Metric | Value |\n")
|
||||
sb.append("| --- | --- |\n")
|
||||
sb.append("| Device class | ${memory.memoryClassMb} MB |\n")
|
||||
sb.append("| App heap | ${memory.heapUsedMb} / ${memory.heapMaxMb} MB |\n")
|
||||
sb.append("| Native heap | ${memory.nativeHeapUsedMb} MB |\n")
|
||||
sb.append("| Image cache (RAM) | ${memory.imageCacheUsedMb} / ${memory.imageCacheMaxMb} MB |\n")
|
||||
sb.append("| Image cache (disk) | ${memory.imageDiskUsedMb} / ${memory.imageDiskMaxMb} MB |\n")
|
||||
sb.append("| Cached notes/users/addressables/chatrooms | ${memory.noteCount}/${memory.userCount}/${memory.addressableCount}/${memory.chatroomCount} |\n")
|
||||
}
|
||||
|
||||
sb.append("\nTechnical details (per epoch-day):\n")
|
||||
sb.append("```\n")
|
||||
days.toSortedMap().forEach { (day, counters) ->
|
||||
|
||||
-116
@@ -1,116 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.navigation.topbars
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.MemorySnapshot
|
||||
import com.vitorpamplona.amethyst.collectMemorySnapshot
|
||||
import com.vitorpamplona.amethyst.isDebug
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
@Composable
|
||||
fun MemoryUsageChip() {
|
||||
if (!isDebug) return
|
||||
|
||||
val context = LocalContext.current
|
||||
var showDetail by remember { mutableStateOf(false) }
|
||||
|
||||
val snapshot by produceState<MemorySnapshot?>(null) {
|
||||
while (true) {
|
||||
// collectMemorySnapshot reads coil3.disk.DiskLruCache.size(), which is @Synchronized and
|
||||
// contends with the disk cache's own journal I/O. On cold start that lock is held by a
|
||||
// background worker for seconds (initial journal read + the burst of image writes), so
|
||||
// running this on the produceState default (main) dispatcher froze the UI thread —
|
||||
// the "Loading account" frame couldn't repaint until size() returned. Collect off-main.
|
||||
value = withContext(Dispatchers.IO) { collectMemorySnapshot(context) }
|
||||
delay(2_000)
|
||||
}
|
||||
}
|
||||
|
||||
val s = snapshot ?: return
|
||||
|
||||
val chipColor =
|
||||
when {
|
||||
s.heapFraction > 0.80f -> Color(0xFFE53935) // red
|
||||
s.heapFraction > 0.60f -> Color(0xFFFFA000) // amber
|
||||
else -> Color(0xFF43A047) // green
|
||||
}
|
||||
|
||||
Text(
|
||||
text = "${s.heapUsedMb}/${s.heapMaxMb}MB",
|
||||
color = chipColor,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(horizontal = 4.dp)
|
||||
.clickable { showDetail = true },
|
||||
)
|
||||
|
||||
if (showDetail) {
|
||||
MemoryDetailDialog(snapshot = s, onDismiss = { showDetail = false })
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MemoryDetailDialog(
|
||||
snapshot: MemorySnapshot,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text("Memory Usage") },
|
||||
text = {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
Text("Device class: ${snapshot.memoryClassMb} MB")
|
||||
Text("JVM heap: ${snapshot.heapUsedMb} / ${snapshot.heapMaxMb} MB")
|
||||
Text("Native heap: ${snapshot.nativeHeapUsedMb} MB")
|
||||
Text("Coil memory: ${snapshot.imageCacheUsedMb} / ${snapshot.imageCacheMaxMb} MB")
|
||||
Text("Coil disk: ${snapshot.imageDiskUsedMb} / ${snapshot.imageDiskMaxMb} MB")
|
||||
Text("Notes: ${snapshot.noteCount}")
|
||||
Text("Users: ${snapshot.userCount}")
|
||||
Text("Addressables: ${snapshot.addressableCount}")
|
||||
Text("Chatrooms: ${snapshot.chatroomCount}")
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = onDismiss) { Text("OK") }
|
||||
},
|
||||
)
|
||||
}
|
||||
-1
@@ -75,7 +75,6 @@ fun UserDrawerSearchTopBar(
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
MemoryUsageChip()
|
||||
IconButton(onClick = { nav.nav(Route.Search) }) {
|
||||
SearchIcon(modifier = Size22Modifier, MaterialTheme.colorScheme.placeholderText)
|
||||
}
|
||||
|
||||
+43
-2
@@ -36,14 +36,18 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.MemorySnapshot
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.collectMemorySnapshot
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.service.resourceusage.DEV_REPORT_PUBKEY
|
||||
import com.vitorpamplona.amethyst.service.resourceusage.ResourceUsageReportAssembler
|
||||
@@ -57,6 +61,7 @@ import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarWithBackButton
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
/**
|
||||
@@ -79,6 +84,16 @@ fun ResourceUsageScreen(
|
||||
}
|
||||
}
|
||||
|
||||
// Live memory snapshot, refreshed only while this screen is composed.
|
||||
// Collected off-main: DiskLruCache.size() contends with journal I/O.
|
||||
val context = LocalContext.current
|
||||
val memory by produceState<MemorySnapshot?>(null) {
|
||||
while (true) {
|
||||
value = withContext(Dispatchers.IO) { collectMemorySnapshot(context) }
|
||||
delay(2_000)
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = { TopBarWithBackButton(stringRes(id = R.string.resource_usage_title), nav) },
|
||||
) { padding ->
|
||||
@@ -105,7 +120,8 @@ fun ResourceUsageScreen(
|
||||
UsageSummarySection(R.string.resource_usage_today, todaySummary)
|
||||
UsageSummarySection(R.string.resource_usage_week, weekSummary)
|
||||
SubsystemSection(weekSummary)
|
||||
SendReportSection(accountViewModel, nav, loaded, today)
|
||||
MemorySection(memory)
|
||||
SendReportSection(accountViewModel, nav, loaded, today, memory)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -191,12 +207,37 @@ private fun MetricRow(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MemorySection(memory: MemorySnapshot?) {
|
||||
if (memory == null) return
|
||||
SettingsSection(R.string.resource_usage_memory_section) {
|
||||
MetricRow(R.string.resource_usage_memory_heap, "${memory.heapUsedMb} / ${memory.heapMaxMb} MB")
|
||||
SettingsDivider()
|
||||
MetricRow(R.string.resource_usage_memory_native, "${memory.nativeHeapUsedMb} MB")
|
||||
SettingsDivider()
|
||||
MetricRow(R.string.resource_usage_memory_image_cache, "${memory.imageCacheUsedMb} / ${memory.imageCacheMaxMb} MB")
|
||||
SettingsDivider()
|
||||
MetricRow(R.string.resource_usage_memory_image_disk, "${memory.imageDiskUsedMb} / ${memory.imageDiskMaxMb} MB")
|
||||
SettingsDivider()
|
||||
MetricRow(R.string.resource_usage_memory_notes, memory.noteCount.toString())
|
||||
SettingsDivider()
|
||||
MetricRow(R.string.resource_usage_memory_users, memory.userCount.toString())
|
||||
SettingsDivider()
|
||||
MetricRow(R.string.resource_usage_memory_addressables, memory.addressableCount.toString())
|
||||
SettingsDivider()
|
||||
MetricRow(R.string.resource_usage_memory_chatrooms, memory.chatroomCount.toString())
|
||||
SettingsDivider()
|
||||
MetricRow(R.string.resource_usage_memory_device_class, "${memory.memoryClassMb} MB")
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SendReportSection(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
days: Map<Long, Map<String, Long>>,
|
||||
today: Long,
|
||||
memory: MemorySnapshot?,
|
||||
) {
|
||||
SettingsSection(R.string.resource_usage_send_section) {
|
||||
Column(
|
||||
@@ -210,7 +251,7 @@ private fun SendReportSection(
|
||||
)
|
||||
Button(
|
||||
onClick = {
|
||||
val report = ResourceUsageReportAssembler().buildReport(days, today)
|
||||
val report = ResourceUsageReportAssembler().buildReport(days, today, memory)
|
||||
nav.nav {
|
||||
routeToMessage(
|
||||
user = LocalCache.getOrCreateUser(DEV_REPORT_PUBKEY),
|
||||
|
||||
@@ -3208,6 +3208,16 @@
|
||||
<string name="resource_usage_subsystem_push">Push registration</string>
|
||||
<string name="resource_usage_subsystem_other">Other</string>
|
||||
<string name="resource_usage_empty">No usage recorded yet.</string>
|
||||
<string name="resource_usage_memory_section">Memory right now</string>
|
||||
<string name="resource_usage_memory_heap">App memory (heap)</string>
|
||||
<string name="resource_usage_memory_native">Native memory</string>
|
||||
<string name="resource_usage_memory_image_cache">Image cache (RAM)</string>
|
||||
<string name="resource_usage_memory_image_disk">Image cache (disk)</string>
|
||||
<string name="resource_usage_memory_notes">Notes in memory</string>
|
||||
<string name="resource_usage_memory_users">Profiles in memory</string>
|
||||
<string name="resource_usage_memory_addressables">Addressable events in memory</string>
|
||||
<string name="resource_usage_memory_chatrooms">Chatroom lists in memory</string>
|
||||
<string name="resource_usage_memory_device_class">Device memory class</string>
|
||||
<string name="resource_usage_send_section">Share with the developers</string>
|
||||
<string name="resource_usage_send_explanation">If Amethyst seems to drain battery or data, you can send this report to the developers in an encrypted DM. It contains only the numbers on this screen and the technical counters behind them \u2014 no posts, contacts, or browsing details. Nothing is sent until you tap Send in the message screen.</string>
|
||||
<string name="resource_usage_send_button">Send report via DM</string>
|
||||
|
||||
Reference in New Issue
Block a user