Add storage usage stats settings view
This commit implements a new Storage settings view that displays storage usage statistics for NostrDB, snapshot database, and Kingfisher image cache. Key features: - Interactive pie chart visualization (iOS 17+) with tap-to-select functionality - Pull-to-refresh gesture to recalculate storage - Categorized list showing each storage type with size and percentage - Total storage sum displayed at bottom - Conditional compilation for iOS 16/17+ compatibility - All calculations run on background thread to avoid blocking main thread - NostrDB storage breakdown Changelog-Added: Storage usage statistics view in Settings Changelog-Changed: Moved clear cache button to storage settings Closes: https://github.com/damus-io/damus/issues/3649 Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
This commit is contained in:
@@ -84,8 +84,8 @@ class Ndb {
|
||||
return remove_file_prefix(containerURL.appendingPathComponent("snapshot", conformingTo: .directory).absoluteString)
|
||||
}
|
||||
|
||||
static private let main_db_file_name: String = "data.mdb"
|
||||
static private let db_files: [String] = ["data.mdb", "lock.mdb"]
|
||||
static let main_db_file_name: String = "data.mdb"
|
||||
static let db_files: [String] = ["data.mdb", "lock.mdb"]
|
||||
|
||||
static var empty: Ndb {
|
||||
print("txn: NOSTRDB EMPTY")
|
||||
@@ -1151,6 +1151,72 @@ extension Ndb {
|
||||
}
|
||||
}
|
||||
|
||||
extension Ndb {
|
||||
/// Get detailed storage statistics for this database
|
||||
///
|
||||
/// This method calls the C `ndb_stat` function to retrieve per-database
|
||||
/// storage statistics from the underlying LMDB storage. Each database
|
||||
/// (notes, profiles, indices, etc.) is reported with its key and value sizes.
|
||||
///
|
||||
/// Any unaccounted space between the sum of database stats and the physical
|
||||
/// file size is reported as "Other Data".
|
||||
///
|
||||
/// - Parameter physicalSize: The physical file size in bytes from the filesystem
|
||||
/// - Returns: NdbStats with detailed per-database breakdown, or nil if stat collection fails
|
||||
func getStats(physicalSize: UInt64) -> NdbStats? {
|
||||
// All of this must be done under withNdb to avoid races with close()/ndb_destroy
|
||||
let copiedStats: ([NdbDatabaseStats], UInt64)? = try? withNdb({
|
||||
var stat = ndb_stat()
|
||||
// Call C ndb_stat function
|
||||
let result = ndb_stat(self.ndb.ndb, &stat)
|
||||
guard result != 0 else {
|
||||
Log.error("ndb_stat failed", for: .storage)
|
||||
return nil
|
||||
}
|
||||
|
||||
var databaseStats: [NdbDatabaseStats] = []
|
||||
var accountedSize: UInt64 = 0
|
||||
|
||||
// Extract per-database stats from stat.dbs array
|
||||
withUnsafePointer(to: &stat.dbs) { dbsPtr in
|
||||
let dbsBuffer = UnsafeRawPointer(dbsPtr).assumingMemoryBound(to: ndb_stat_counts.self)
|
||||
|
||||
for dbIndex in 0..<Int(NDB_DBS.rawValue) {
|
||||
let dbStat = dbsBuffer[dbIndex]
|
||||
// Skip databases with no data
|
||||
guard dbStat.key_size > 0 || dbStat.value_size > 0 else { continue }
|
||||
// Get database type from index
|
||||
let database = NdbDatabase(fromIndex: dbIndex)
|
||||
let dbStats = NdbDatabaseStats(
|
||||
database: database,
|
||||
keySize: UInt64(dbStat.key_size),
|
||||
valueSize: UInt64(dbStat.value_size)
|
||||
)
|
||||
databaseStats.append(dbStats)
|
||||
accountedSize += dbStats.totalSize
|
||||
}
|
||||
}
|
||||
return (databaseStats, accountedSize)
|
||||
})
|
||||
guard let (databaseStatsRaw, accountedSize) = copiedStats else { return nil }
|
||||
var databaseStats = databaseStatsRaw
|
||||
|
||||
// Add "Other Data" for any unaccounted space
|
||||
if physicalSize > accountedSize {
|
||||
let otherSize = physicalSize - accountedSize
|
||||
databaseStats.append(NdbDatabaseStats(
|
||||
database: .other,
|
||||
keySize: 0,
|
||||
valueSize: otherSize
|
||||
))
|
||||
}
|
||||
// Sort by total size descending to show largest databases first
|
||||
databaseStats.sort { $0.totalSize > $1.totalSize }
|
||||
|
||||
return NdbStats(databaseStats: databaseStats)
|
||||
}
|
||||
}
|
||||
|
||||
/// This callback "trampoline" function will be called when new notes arrive for NostrDB subscriptions.
|
||||
///
|
||||
/// This is needed as a separate global function in order to allow us to pass it to the C code as a callback (We can't pass native Swift fuctions directly as callbacks).
|
||||
@@ -1180,3 +1246,73 @@ func getDebugCheckedRoot<T: FlatBufferObject>(byteBuffer: inout ByteBuffer) thro
|
||||
func remove_file_prefix(_ str: String) -> String {
|
||||
return str.replacingOccurrences(of: "file://", with: "")
|
||||
}
|
||||
|
||||
// MARK: - NostrDB Storage Statistics
|
||||
|
||||
/// NostrDB database types corresponding to the ndb_dbs C enum
|
||||
enum NdbDatabase: Int, Hashable, CaseIterable, Identifiable {
|
||||
case note = 0 // NDB_DB_NOTE
|
||||
case meta = 1 // NDB_DB_META
|
||||
case profile = 2 // NDB_DB_PROFILE
|
||||
case noteId = 3 // NDB_DB_NOTE_ID
|
||||
case profileKey = 4 // NDB_DB_PROFILE_PK
|
||||
case ndbMeta = 5 // NDB_DB_NDB_META
|
||||
case profileSearch = 6 // NDB_DB_PROFILE_SEARCH
|
||||
case profileLastFetch = 7 // NDB_DB_PROFILE_LAST_FETCH
|
||||
case noteKind = 8 // NDB_DB_NOTE_KIND
|
||||
case noteText = 9 // NDB_DB_NOTE_TEXT
|
||||
case noteBlocks = 10 // NDB_DB_NOTE_BLOCKS
|
||||
case noteTags = 11 // NDB_DB_NOTE_TAGS
|
||||
case notePubkey = 12 // NDB_DB_NOTE_PUBKEY
|
||||
case notePubkeyKind = 13 // NDB_DB_NOTE_PUBKEY_KIND
|
||||
case noteRelayKind = 14 // NDB_DB_NOTE_RELAY_KIND
|
||||
case noteRelays = 15 // NDB_DB_NOTE_RELAYS
|
||||
case other // For unaccounted data
|
||||
|
||||
var id: String {
|
||||
return String(self.rawValue)
|
||||
}
|
||||
|
||||
/// Database index matching the C ndb_dbs enum
|
||||
var index: Int {
|
||||
return self.rawValue
|
||||
}
|
||||
|
||||
/// Initialize from database index (matching ndb_dbs C enum order)
|
||||
init(fromIndex index: Int) {
|
||||
if let db = NdbDatabase(rawValue: index) {
|
||||
self = db
|
||||
} else {
|
||||
self = .other
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Per-database storage statistics from NostrDB
|
||||
struct NdbDatabaseStats: Hashable {
|
||||
/// Database type
|
||||
let database: NdbDatabase
|
||||
|
||||
/// Total key bytes for this database
|
||||
let keySize: UInt64
|
||||
|
||||
/// Total value bytes for this database
|
||||
let valueSize: UInt64
|
||||
|
||||
/// Total storage used by this database (keys + values)
|
||||
var totalSize: UInt64 {
|
||||
return keySize + valueSize
|
||||
}
|
||||
}
|
||||
|
||||
/// Detailed NostrDB storage statistics with per-database breakdown
|
||||
struct NdbStats: Hashable {
|
||||
/// Per-database breakdown of storage (notes, profiles, indices, etc.)
|
||||
let databaseStats: [NdbDatabaseStats]
|
||||
|
||||
/// Total storage across all databases
|
||||
var totalSize: UInt64 {
|
||||
return databaseStats.reduce(0) { $0 + $1.totalSize }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
91
nostrdb/NdbDatabase+UI.swift
Normal file
91
nostrdb/NdbDatabase+UI.swift
Normal file
@@ -0,0 +1,91 @@
|
||||
//
|
||||
// NdbDatabase+UI.swift
|
||||
// (UI/Features target)
|
||||
//
|
||||
// This extension adds UI-specific properties to NdbDatabase for presentation purposes.
|
||||
// It should only be included in targets involving SwiftUI/UI presentation.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
extension NdbDatabase {
|
||||
/// Human-readable database name
|
||||
var displayName: String {
|
||||
switch self {
|
||||
case .note:
|
||||
return NSLocalizedString("Notes (NDB_DB_NOTE)", comment: "Database name for notes")
|
||||
case .meta:
|
||||
return NSLocalizedString("Metadata (NDB_DB_META)", comment: "Database name for metadata")
|
||||
case .profile:
|
||||
return NSLocalizedString("Profiles (NDB_DB_PROFILE)", comment: "Database name for profiles")
|
||||
case .noteId:
|
||||
return NSLocalizedString("Note ID Index", comment: "Database name for note ID index")
|
||||
case .profileKey:
|
||||
return NSLocalizedString("Profile Key Index", comment: "Database name for profile key index")
|
||||
case .ndbMeta:
|
||||
return NSLocalizedString("NostrDB Metadata", comment: "Database name for NostrDB metadata")
|
||||
case .profileSearch:
|
||||
return NSLocalizedString("Profile Search Index", comment: "Database name for profile search")
|
||||
case .profileLastFetch:
|
||||
return NSLocalizedString("Profile Last Fetch", comment: "Database name for profile last fetch")
|
||||
case .noteKind:
|
||||
return NSLocalizedString("Note Kind Index", comment: "Database name for note kind index")
|
||||
case .noteText:
|
||||
return NSLocalizedString("Note Text Index", comment: "Database name for note text index")
|
||||
case .noteBlocks:
|
||||
return NSLocalizedString("Note Blocks", comment: "Database name for note blocks")
|
||||
case .noteTags:
|
||||
return NSLocalizedString("Note Tags Index", comment: "Database name for note tags index")
|
||||
case .notePubkey:
|
||||
return NSLocalizedString("Note Pubkey Index", comment: "Database name for note pubkey index")
|
||||
case .notePubkeyKind:
|
||||
return NSLocalizedString("Note Pubkey+Kind Index", comment: "Database name for note pubkey+kind index")
|
||||
case .noteRelayKind:
|
||||
return NSLocalizedString("Note Relay+Kind Index", comment: "Database name for note relay+kind index")
|
||||
case .noteRelays:
|
||||
return NSLocalizedString("Note Relays", comment: "Database name for note relays")
|
||||
case .other:
|
||||
return NSLocalizedString("Other Data", comment: "Database name for other/unaccounted data")
|
||||
}
|
||||
}
|
||||
|
||||
/// SF Symbol icon name for this database type
|
||||
var icon: String {
|
||||
switch self {
|
||||
case .note:
|
||||
return "text.bubble.fill"
|
||||
case .profile:
|
||||
return "person.circle.fill"
|
||||
case .meta, .ndbMeta:
|
||||
return "info.circle.fill"
|
||||
case .noteBlocks:
|
||||
return "square.stack.3d.up.fill"
|
||||
case .noteId, .profileKey, .profileSearch, .noteKind, .noteText, .noteTags, .notePubkey, .notePubkeyKind, .noteRelayKind:
|
||||
return "list.bullet.indent"
|
||||
case .noteRelays:
|
||||
return "antenna.radiowaves.left.and.right"
|
||||
case .profileLastFetch, .other:
|
||||
return "internaldrive.fill"
|
||||
}
|
||||
}
|
||||
|
||||
/// Color for chart and UI display
|
||||
var color: Color {
|
||||
switch self {
|
||||
case .note:
|
||||
return .green
|
||||
case .profile:
|
||||
return .blue
|
||||
case .noteBlocks:
|
||||
return .purple
|
||||
case .meta, .ndbMeta:
|
||||
return .orange
|
||||
case .noteId, .profileKey, .profileSearch, .noteKind, .noteText, .noteTags, .notePubkey, .notePubkeyKind, .noteRelayKind:
|
||||
return .gray
|
||||
case .noteRelays:
|
||||
return .cyan
|
||||
case .profileLastFetch, .other:
|
||||
return .secondary
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user