feat: implement LMDB compact solution for storage optimization
- Add `compact(to:)` method on `Ndb` using `mdb_env_copy2` / `ndb_snapshot` with `MDB_CP_COMPACT` flag to produce a smaller compacted database copy - Add `compact_if_needed(db_path:)` static startup method that reads a UserDefaults flag, opens a temp Ndb, compacts to a sibling temp dir, atomically replaces `data.mdb`, cleans up, and clears the flag - Add `set_compact_on_next_launch()` to schedule compaction from the UI - Call `Ndb.compact_if_needed()` in `ContentView.connect()` before the main Ndb instance is opened - Add `CompactDatabaseButton` to `StorageSettingsView` with a confirmation dialog that informs the user a restart is needed - Add `NdbCompactionTests` unit tests covering flag API, no-op path, missing-DB path, and full round-trip compaction Closes: https://github.com/damus-io/damus/issues/3680 Changelog-Added: Added "Compact Database" button in Settings → Storage that reclaims unused space by compacting the NostrDB on the next app launch Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
This commit is contained in:
+14
-1
@@ -140,6 +140,18 @@ struct ContentView: View {
|
||||
// connect retry timer
|
||||
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
|
||||
|
||||
init(keypair: Keypair, appDelegate: AppDelegate?) {
|
||||
// Compact the database if requested from the previous session.
|
||||
// This runs before opening the main Ndb instance so that it works on an idle database.
|
||||
// This also gets run here instead of `connect` because we should anticipate this to add a few seconds of delay in worst case scenarios.
|
||||
// If we were to add this in the `connect` function, parallel functions that depend on `damus_state!` could cause crashes in the app.
|
||||
// By placing this here, we only delay the splash screen a bit
|
||||
Ndb.compact_if_needed()
|
||||
|
||||
self.keypair = keypair
|
||||
self.appDelegate = appDelegate
|
||||
}
|
||||
|
||||
func navIsAtRoot() -> Bool {
|
||||
return navigationCoordinator.isAtRoot()
|
||||
}
|
||||
@@ -1168,4 +1180,5 @@ func logout(_ state: DamusState?)
|
||||
{
|
||||
state?.close()
|
||||
notify(.logout)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,12 @@ fileprivate enum CacheClearingState {
|
||||
case cleared
|
||||
}
|
||||
|
||||
/// A simple type to keep track of the compact scheduling state
|
||||
fileprivate enum CompactSchedulingState {
|
||||
case not_scheduled
|
||||
case scheduled
|
||||
}
|
||||
|
||||
/// Storage category for display in list and chart
|
||||
struct StorageCategory: Identifiable {
|
||||
let id: String
|
||||
@@ -46,6 +52,8 @@ struct StorageSettingsView: View {
|
||||
@State private var isPreparingExport: Bool = false
|
||||
@State fileprivate var cache_clearing_state: CacheClearingState = .not_cleared
|
||||
@State var showing_cache_clear_alert: Bool = false
|
||||
@State fileprivate var compact_scheduling_state: CompactSchedulingState = .not_scheduled
|
||||
@State var showing_compact_alert: Bool = false
|
||||
|
||||
/// Storage categories with cumulative ranges for angle selection (iOS 17+)
|
||||
private var categoryRanges: [(category: String, range: Range<Double>)] {
|
||||
@@ -164,6 +172,7 @@ struct StorageSettingsView: View {
|
||||
// Clear Cache Section
|
||||
Section {
|
||||
self.ClearCacheButton
|
||||
self.CompactDatabaseButton
|
||||
}
|
||||
}
|
||||
|
||||
@@ -215,6 +224,10 @@ struct StorageSettingsView: View {
|
||||
if stats == nil {
|
||||
loadStorageStats()
|
||||
}
|
||||
// Reflect any previously scheduled compaction in the button state.
|
||||
if UserDefaults.standard.bool(forKey: Ndb.compact_on_next_launch_key) {
|
||||
compact_scheduling_state = .scheduled
|
||||
}
|
||||
}
|
||||
.onReceive(handle_notify(.switched_timeline)) { _ in
|
||||
dismiss()
|
||||
@@ -338,6 +351,38 @@ struct StorageSettingsView: View {
|
||||
secondaryButton: .cancel())
|
||||
}
|
||||
}
|
||||
|
||||
/// Compact database button view with confirmation dialog.
|
||||
///
|
||||
/// Schedules a one-time database compaction to run on the next app launch. The user
|
||||
/// is informed that the app will need to restart to complete the operation.
|
||||
var CompactDatabaseButton: some View {
|
||||
Button(action: { self.showing_compact_alert = true }, label: {
|
||||
HStack(spacing: 6) {
|
||||
switch compact_scheduling_state {
|
||||
case .not_scheduled:
|
||||
Text("Compact Database", comment: "Button to compact the NostrDB database on next launch.")
|
||||
case .scheduled:
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.foregroundColor(.green)
|
||||
.accessibilityHidden(true)
|
||||
Text("Compaction scheduled. Restart app to continue.", comment: "Message indicating that a database compaction has been scheduled for the next app launch.")
|
||||
}
|
||||
}
|
||||
})
|
||||
.disabled(self.compact_scheduling_state != .not_scheduled)
|
||||
.alert(isPresented: $showing_compact_alert) {
|
||||
Alert(
|
||||
title: Text("Compact Database", comment: "Confirmation dialog title for database compaction"),
|
||||
message: Text("This will reclaim unused space in the database. The app will need to restart to complete the operation. Proceed?", comment: "Message explaining what database compaction does and that a restart is required."),
|
||||
primaryButton: .default(Text("OK", comment: "Button label indicating user wants to proceed.")) {
|
||||
Ndb.set_compact_on_next_launch()
|
||||
compact_scheduling_state = .scheduled
|
||||
},
|
||||
secondaryButton: .cancel()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Pie chart displaying storage usage distribution (iOS 17+)
|
||||
|
||||
Reference in New Issue
Block a user