damus

nostr ios client
git clone git://jb55.com/damus
Log | Files | Refs | README | LICENSE

commit 7744787c51fa615951ff2e369f1a20fc3836d13a
parent 5d90b497f00b3d3562c1debbdbe53ae5ba67e095
Author: Daniel D’Aquino <daniel@daquino.me>
Date:   Fri,  6 Oct 2023 17:44:00 +0000

storage: Improve clear cache functionality

This patch improves clear cache functionality by:
- Reducing kingfisher cache removal to one command (The two commands running async was leading to warning logs. One was a subset of the other)
- Removing all files under the cache folder where not currently used by other processes

Full Functionality test
-----------------------

PASS

Device: iPhone 13 mini (Physical device)
iOS: 17.0.3
Damus: This commit
Special remarks:
- I had to locally delete other unit tests to be able to build the test target
- Unit test run on an earlier version of the patch. Test coverage should still apply since this newer patch is a subset of the previous.

Setup: Run Damus with debugger connection to Xcode

Test steps:

1. Follow multiple active accounts (Skip if local Damus is already filled up with GBs of data)
2. Scroll down on the feed for a couple of minutes (or until you have seen at least a few images, a few videos, and link previews) (Skip if local Damus is filled up with GBs of data)
3. In Xcode, download a storage container (Window > Devices and Simulators > Select the device > Select Damus > click on (...) > Download container)
    - Note: Even though you see the file, it does not download instantly. Monitor the file size until it roughly reaches the size reported in iOS storage settings, as the download may still be in progress. This may take a few minutes in some cases.
    - Also take note of storage usage in iOS settings
4. Open the app data package using terminal
5. Run `du -h . | sort -hr`
6. Clear cache and check logs. Logs should indicate the caches being cleared, and there should be no storage-related warning/error logs. PASS
7. Download a new storage container. Remember to wait until it completes download.
8. Run `du -h . | sort -hr` on it.
9. Compare. There should be much less data. Also check iOS settings storage usage. PASS
10. Go back to the home feed and start scrolling, browsing, follow some other people, etc. Look at your own profile as well. Everything should appear to be working as expected with no crashes or important data loss
11. Check bookmarks are still present. PASS
12. Run `DamusCacheManagerTests`. Should pass. PASS* (*See special remarks)

Results:
- Storage usage goes from 3.9GB to 394.7MB
- Damus works as normal after clearing cache, and after restarting the app as well. It becomes slower for a moment, but after a bit it loads as normal again.
- No warning or error logs pertaining to clearing cache
- Unit test passes

My storage container disk usage stats after clearing cache:
```
% du -h | sort -hr
359M	./AppData
359M	.
336M	./AppData/Documents
 23M	./AppData/Library
 20M	./AppData/Library/Caches
7.9M	./AppData/Library/Caches/com.jb55.damus2
2.4M	./AppData/Library/SplashBoard/Snapshots
2.4M	./AppData/Library/SplashBoard
1.8M	./AppData/Library/SplashBoard/Snapshots/com.jb55.damus2 - {DEFAULT GROUP}
1.6M	./AppData/Library/Caches/com.jb55.damus2/fsCachedData
636K	./AppData/Library/SplashBoard/Snapshots/sceneID:com.jb55.damus2-ecc156b1-eb9c-4439-b219-e1eebf2b4c36
596K	./AppData/Library/Caches/com.apple.WebKit.GPU/com.apple.metal
596K	./AppData/Library/Caches/com.apple.WebKit.GPU
452K	./AppData/Library/Caches/com.jb55.damus2/com.apple.metal
296K	./AppData/Library/SplashBoard/Snapshots/sceneID:com.jb55.damus2-ecc156b1-eb9c-4439-b219-e1eebf2b4c36/downscaled
224K	./AppData/Library/HTTPStorages/com.jb55.damus2
224K	./AppData/Library/HTTPStorages
164K	./AppData/Library/Caches/com.onevcat.Kingfisher.ImageCache.default
156K	./AppData/Library/Caches/RelayLogs
112K	./AppData/Library/Caches/com.apple.dyld
 92K	./AppData/Library/Preferences
 60K	./AppData/Library/Caches/com.jb55.damus2/com.apple.metal/archiveUsage.db
 12K	./AppData/Library/Saved Application State/com.jb55.damus2.savedState
 12K	./AppData/Library/Saved Application State
8.0K	./AppData/StoreKit
8.0K	./AppData/Library/Saved Application State/com.jb55.damus2.savedState/ecc156b1-eb9c-4439-b219-e1eebf2b4c36
4.0K	./AppData/Library/Saved Application State/com.jb55.damus2.savedState/KnownSceneSessions
4.0K	./AppData/Library/LanguageModeling/en-dynamic.lm
4.0K	./AppData/Library/LanguageModeling
4.0K	./AppData/Library/Cookies
  0B	./AppData/SystemData/com.apple.SafariViewService/Library/WebKit/WebsiteData
  0B	./AppData/SystemData/com.apple.SafariViewService/Library/WebKit
  0B	./AppData/SystemData/com.apple.SafariViewService/Library
  0B	./AppData/SystemData/com.apple.SafariViewService
```

Biggest storage used remaining is in the Documents folder where NostrDB is stored. However, we do not want to clear NostrDB, so this is expected behavior.

Changelog-Changed: Improve clear cache functionality
Closes: https://github.com/damus-io/damus/issues/1472
Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
Signed-off-by: William Casarin <jb55@jb55.com>

Diffstat:
Mdamus.xcodeproj/project.pbxproj | 8++++++++
Adamus/Models/DamusCacheManager.swift | 59+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mdamus/Util/Log.swift | 9+++++++++
Mdamus/Views/ConfigView.swift | 20--------------------
Mdamus/Views/Settings/AppearanceSettingsView.swift | 2+-
AdamusTests/DamusCacheManagerTests.swift | 31+++++++++++++++++++++++++++++++
6 files changed, 108 insertions(+), 21 deletions(-)

diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj @@ -425,6 +425,8 @@ BAB68BED29543FA3007BA466 /* SelectWalletView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAB68BEC29543FA3007BA466 /* SelectWalletView.swift */; }; D2277EEA2A089BD5006C3807 /* Router.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2277EE92A089BD5006C3807 /* Router.swift */; }; D71DC1EC2A9129C3006E207C /* PostViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D71DC1EB2A9129C3006E207C /* PostViewTests.swift */; }; + D7315A2A2ACDF3B70036E30A /* DamusCacheManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7315A292ACDF3B70036E30A /* DamusCacheManager.swift */; }; + D7315A2C2ACDF4DA0036E30A /* DamusCacheManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7315A2B2ACDF4DA0036E30A /* DamusCacheManagerTests.swift */; }; D723C38E2AB8D83400065664 /* ContentFilters.swift in Sources */ = {isa = PBXBuildFile; fileRef = D723C38D2AB8D83400065664 /* ContentFilters.swift */; }; D78525252A7B2EA4002FA637 /* NoteContentViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D78525242A7B2EA4002FA637 /* NoteContentViewTests.swift */; }; D7870BC12AC4750B0080BA88 /* MentionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7870BC02AC4750B0080BA88 /* MentionView.swift */; }; @@ -1105,6 +1107,8 @@ BA37598B2ABCCE500018D73B /* PhotoCaptureProcessor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoCaptureProcessor.swift; sourceTree = "<group>"; }; BA37598C2ABCCE500018D73B /* VideoCaptureProcessor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoCaptureProcessor.swift; sourceTree = "<group>"; }; BA3759962ABCCF360018D73B /* CameraPreview.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CameraPreview.swift; sourceTree = "<group>"; }; + D7315A292ACDF3B70036E30A /* DamusCacheManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusCacheManager.swift; sourceTree = "<group>"; }; + D7315A2B2ACDF4DA0036E30A /* DamusCacheManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusCacheManagerTests.swift; sourceTree = "<group>"; }; BA4AB0AD2A63B9270070A32A /* AddEmojiView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEmojiView.swift; sourceTree = "<group>"; }; BA4AB0AF2A63B94D0070A32A /* EmojiListItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiListItemView.swift; sourceTree = "<group>"; }; BA693073295D649800ADDB87 /* UserSettingsStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSettingsStore.swift; sourceTree = "<group>"; }; @@ -1300,6 +1304,7 @@ 3A5E47C42A4A6CF400C0D090 /* Trie.swift */, 3A90B1802A4EA3AF00000D94 /* UserSearchCache.swift */, D723C38D2AB8D83400065664 /* ContentFilters.swift */, + D7315A292ACDF3B70036E30A /* DamusCacheManager.swift */, ); path = Models; sourceTree = "<group>"; @@ -2164,6 +2169,7 @@ 4C684A562A7FFAE6005E6031 /* UrlTests.swift */, D7DEEF2E2A8C021E00E0C99F /* NostrEventTests.swift */, D71DC1EB2A9129C3006E207C /* PostViewTests.swift */, + D7315A2B2ACDF4DA0036E30A /* DamusCacheManagerTests.swift */, ); path = damusTests; sourceTree = "<group>"; @@ -2681,6 +2687,7 @@ 4C32B95C2A9AD44700DC3548 /* String+extension.swift in Sources */, 4C3BEFD22819DB9B00B3DE84 /* ProfileModel.swift in Sources */, 4CA352AA2A76BF3A003BB08B /* LocalNotificationNotify.swift in Sources */, + D7315A2A2ACDF3B70036E30A /* DamusCacheManager.swift in Sources */, 4C7D09682A0AE9B200943473 /* NWCScannerView.swift in Sources */, 4CA352A42A76AFF3003BB08B /* UpdateStatsNotify.swift in Sources */, 4C0A3F93280F66F5000448DE /* ReplyMap.swift in Sources */, @@ -2938,6 +2945,7 @@ 4C7D097E2A0C58B900943473 /* WalletConnectTests.swift in Sources */, 4CB883AA297612FF00DC99E7 /* ZapTests.swift in Sources */, 4CB8839A297322D200DC99E7 /* DMTests.swift in Sources */, + D7315A2C2ACDF4DA0036E30A /* DamusCacheManagerTests.swift in Sources */, 4C9054852A6AEAA000811EEC /* NdbTests.swift in Sources */, 75AD872B2AA23A460085EF2C /* Block+Tests.swift in Sources */, F944F56E29EA9CCC0067B3BF /* DamusParseContentTests.swift in Sources */, diff --git a/damus/Models/DamusCacheManager.swift b/damus/Models/DamusCacheManager.swift @@ -0,0 +1,59 @@ +// +// DamusCacheManager.swift +// damus +// +// Created by Daniel D’Aquino on 2023-10-04. +// + +import Foundation +import Kingfisher + +struct DamusCacheManager { + static var shared: DamusCacheManager = DamusCacheManager() + + func clear_cache(damus_state: DamusState, completion: (() -> Void)? = nil) { + Log.info("Clearing all caches", for: .storage) + clear_kingfisher_cache(completion: { + clear_cache_folder(completion: { + Log.info("All caches cleared", for: .storage) + completion?() + }) + }) + } + + func clear_kingfisher_cache(completion: (() -> Void)? = nil) { + Log.info("Clearing Kingfisher cache", for: .storage) + KingfisherManager.shared.cache.clearMemoryCache() + KingfisherManager.shared.cache.clearDiskCache { + Log.info("Kingfisher cache cleared", for: .storage) + completion?() + } + } + + func clear_cache_folder(completion: (() -> Void)? = nil) { + Log.info("Clearing entire cache folder", for: .storage) + let cacheURL = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0] + + do { + let fileNames = try FileManager.default.contentsOfDirectory(atPath: cacheURL.path) + + for fileName in fileNames { + let filePath = cacheURL.appendingPathComponent(fileName) + + // Prevent issues by double-checking if files are in use, and do not delete them if they are. + // This is not perfect. There is still a small chance for a race condition if a file is opened between this check and the file removal. + let isBusy = (!(access(filePath.path, F_OK) == -1 && errno == ETXTBSY)) + if isBusy { + continue + } + + try FileManager.default.removeItem(at: filePath) + } + + Log.info("Cache folder cleared successfully.", for: .storage) + completion?() + } catch { + Log.error("Could not clear cache folder", for: .storage) + } + } +} diff --git a/damus/Util/Log.swift b/damus/Util/Log.swift @@ -12,6 +12,7 @@ import os.log enum LogCategory: String { case nav case render + case storage } /// Damus structured logger @@ -44,4 +45,12 @@ class Log { static func info(_ msg: StaticString, for logcat: LogCategory, _ args: CVarArg...) { Log.log(msg, for: logger(for: logcat), type: OSLogType.info, args) } + + static func debug(_ msg: StaticString, for logcat: LogCategory, _ args: CVarArg...) { + Log.log(msg, for: logger(for: logcat), type: OSLogType.debug, args) + } + + static func error(_ msg: StaticString, for logcat: LogCategory, _ args: CVarArg...) { + Log.log(msg, for: logger(for: logcat), type: OSLogType.error, args) + } } diff --git a/damus/Views/ConfigView.swift b/damus/Views/ConfigView.swift @@ -174,23 +174,3 @@ func handle_string_amount(new_value: String) -> Int? { return amt } - -func clear_kingfisher_cache(completion: (() -> Void)? = nil) { - KingfisherManager.shared.cache.clearMemoryCache() - - let group = DispatchGroup() - - group.enter() - KingfisherManager.shared.cache.clearDiskCache { - group.leave() - } - - group.enter() - KingfisherManager.shared.cache.cleanExpiredDiskCache { - group.leave() - } - - group.notify(queue: .main) { - completion?() - } -} diff --git a/damus/Views/Settings/AppearanceSettingsView.swift b/damus/Views/Settings/AppearanceSettingsView.swift @@ -115,7 +115,7 @@ struct AppearanceSettingsView: View { let group = DispatchGroup() group.enter() - clear_kingfisher_cache(completion: { + DamusCacheManager.shared.clear_cache(damus_state: self.damus_state, completion: { group.leave() }) diff --git a/damusTests/DamusCacheManagerTests.swift b/damusTests/DamusCacheManagerTests.swift @@ -0,0 +1,31 @@ +// +// DamusCacheManagerTests.swift +// damusTests +// +// Created by Daniel D’Aquino on 2023-10-04. +// + +import Foundation + +import Foundation +import XCTest +@testable import damus +import SwiftUI + +final class DamusCacheManagerTests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + /// Simple smoke test to check if clearing cache will crash the system + func testCacheManagerSmoke() throws { + for _ in Range(0...20) { + DamusCacheManager.shared.clear_cache(damus_state: test_damus_state) + } + } +}