Implement a basic LRU cache to limit memory usage
- ID
777309f- date
2023-06-14 21:38:34+00:00- author
Alex Chan <alex@alexwlchan.net>- parent
ca1cb3f- message
Implement a basic LRU cache to limit memory usage- changed files
Changed files
BlinkReviewer/Blink.xcodeproj/project.pbxproj (31950) → BlinkReviewer/Blink.xcodeproj/project.pbxproj (33529)
diff --git a/BlinkReviewer/Blink.xcodeproj/project.pbxproj b/BlinkReviewer/Blink.xcodeproj/project.pbxproj
index f9cbf6a..d90a971 100644
--- a/BlinkReviewer/Blink.xcodeproj/project.pbxproj
+++ b/BlinkReviewer/Blink.xcodeproj/project.pbxproj
@@ -34,6 +34,8 @@
94D751222A31BD8E005859E7 /* PhotoReviewer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94D751212A31BD8E005859E7 /* PhotoReviewer.swift */; };
94D7512B2A31D6AC005859E7 /* AssetHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94D7512A2A31D6AC005859E7 /* AssetHelpers.swift */; };
94F7E39E2A331A9E00763DB9 /* Statistics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94F7E39D2A331A9E00763DB9 /* Statistics.swift */; };
+ 94FCD4F42A3A62A800D884A1 /* OrderedCollections in Frameworks */ = {isa = PBXBuildFile; productRef = 94FCD4F32A3A62A800D884A1 /* OrderedCollections */; };
+ 94FCD4F62A3A64F800D884A1 /* LRUCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94FCD4F52A3A64F800D884A1 /* LRUCache.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -85,6 +87,7 @@
94D751212A31BD8E005859E7 /* PhotoReviewer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoReviewer.swift; sourceTree = "<group>"; };
94D7512A2A31D6AC005859E7 /* AssetHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetHelpers.swift; sourceTree = "<group>"; };
94F7E39D2A331A9E00763DB9 /* Statistics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Statistics.swift; sourceTree = "<group>"; };
+ 94FCD4F52A3A64F800D884A1 /* LRUCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LRUCache.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -92,6 +95,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
+ 94FCD4F42A3A62A800D884A1 /* OrderedCollections in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -159,6 +163,7 @@
children = (
94D2C8B82A320E6F00BEE15B /* ReviewState.swift */,
94C5FFF12A33ADD4004ADDF5 /* PHFetchResultCollection.swift */,
+ 94FCD4F52A3A64F800D884A1 /* LRUCache.swift */,
);
path = Model;
sourceTree = "<group>";
@@ -261,6 +266,9 @@
dependencies = (
);
name = Blink;
+ packageProductDependencies = (
+ 94FCD4F32A3A62A800D884A1 /* OrderedCollections */,
+ );
productName = BlinkReviewer;
productReference = 94D750EC2A31A796005859E7 /* Blink.app */;
productType = "com.apple.product-type.application";
@@ -333,6 +341,9 @@
Base,
);
mainGroup = 94D750E32A31A796005859E7;
+ packageReferences = (
+ 94FCD4F22A3A62A800D884A1 /* XCRemoteSwiftPackageReference "swift-collections" */,
+ );
productRefGroup = 94D750ED2A31A796005859E7 /* Products */;
projectDirPath = "";
projectRoot = "";
@@ -394,6 +405,7 @@
945F17B82A33DAC7004FC479 /* ReviewStateSaturation.swift in Sources */,
94D751222A31BD8E005859E7 /* PhotoReviewer.swift in Sources */,
94D2C8B92A320E6F00BEE15B /* ReviewState.swift in Sources */,
+ 94FCD4F62A3A64F800D884A1 /* LRUCache.swift in Sources */,
94F7E39E2A331A9E00763DB9 /* Statistics.swift in Sources */,
94D2C8C12A32FCE300BEE15B /* PHAssetImage.swift in Sources */,
941E18FA2A35362600A2EA98 /* Info.swift in Sources */,
@@ -706,6 +718,25 @@
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
+
+/* Begin XCRemoteSwiftPackageReference section */
+ 94FCD4F22A3A62A800D884A1 /* XCRemoteSwiftPackageReference "swift-collections" */ = {
+ isa = XCRemoteSwiftPackageReference;
+ repositoryURL = "https://github.com/apple/swift-collections.git";
+ requirement = {
+ kind = upToNextMajorVersion;
+ minimumVersion = 1.0.0;
+ };
+ };
+/* End XCRemoteSwiftPackageReference section */
+
+/* Begin XCSwiftPackageProductDependency section */
+ 94FCD4F32A3A62A800D884A1 /* OrderedCollections */ = {
+ isa = XCSwiftPackageProductDependency;
+ package = 94FCD4F22A3A62A800D884A1 /* XCRemoteSwiftPackageReference "swift-collections" */;
+ productName = OrderedCollections;
+ };
+/* End XCSwiftPackageProductDependency section */
};
rootObject = 94D750E42A31A796005859E7 /* Project object */;
}
BlinkReviewer/Blink.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved (0) → BlinkReviewer/Blink.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved (316)
diff --git a/BlinkReviewer/Blink.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/BlinkReviewer/Blink.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
new file mode 100644
index 0000000..3b7a76a
--- /dev/null
+++ b/BlinkReviewer/Blink.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
@@ -0,0 +1,14 @@
+{
+ "pins" : [
+ {
+ "identity" : "swift-collections",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/apple/swift-collections.git",
+ "state" : {
+ "revision" : "937e904258d22af6e447a0b72c0bc67583ef64a2",
+ "version" : "1.0.4"
+ }
+ }
+ ],
+ "version" : 2
+}
BlinkReviewer/Blink/Model/LRUCache.swift (0) → BlinkReviewer/Blink/Model/LRUCache.swift (1255)
diff --git a/BlinkReviewer/Blink/Model/LRUCache.swift b/BlinkReviewer/Blink/Model/LRUCache.swift
new file mode 100644
index 0000000..36007e6
--- /dev/null
+++ b/BlinkReviewer/Blink/Model/LRUCache.swift
@@ -0,0 +1,43 @@
+import OrderedCollections
+
+/// This implements a very basic LRU (least-recently used) cache.
+///
+/// The cache has a maximum size, and objects get evicted when the cache grows
+/// too large. This is primarily used to cache images, and avoid the memory
+/// footprint of the application growing forever.
+struct LRUCache<Key: Hashable, Value> {
+ private var contents: Dictionary<Key, Value>
+ private var keyHistory: OrderedSet<Key>
+
+ var maxSize: Int
+
+ init(withMaxSize maxSize: Int) {
+ self.maxSize = maxSize
+
+ self.contents = Dictionary()
+ self.keyHistory = OrderedSet()
+ }
+
+ subscript(key: Key) -> Value? {
+ get {
+ contents[key]
+ }
+
+ set {
+ contents[key] = newValue
+
+ // Move the key to the beginning of the key history
+ keyHistory.remove(key)
+ keyHistory.insert(key, at: 0)
+
+ assert(contents.count == keyHistory.count)
+
+ while contents.count > self.maxSize {
+ let lastKey = keyHistory.last!
+
+ contents.removeValue(forKey: lastKey)
+ keyHistory.remove(lastKey)
+ }
+ }
+ }
+}
BlinkReviewer/Blink/Photos/PhotosLibrary.swift (8762) → BlinkReviewer/Blink/Photos/PhotosLibrary.swift (9005)
diff --git a/BlinkReviewer/Blink/Photos/PhotosLibrary.swift b/BlinkReviewer/Blink/Photos/PhotosLibrary.swift
index be05ac5..cc79a6a 100644
--- a/BlinkReviewer/Blink/Photos/PhotosLibrary.swift
+++ b/BlinkReviewer/Blink/Photos/PhotosLibrary.swift
@@ -1,4 +1,5 @@
import Foundation
+import OrderedCollections
import Photos
/// Manage most of the interactions with the Photos Library.
@@ -174,26 +175,29 @@ class PhotosLibrary: NSObject, ObservableObject, PHPhotoLibraryChangeObserver {
// smart enough to debug that. If I don't cache it, there's a "flash" as
// it reloads the thumbnails every time.
//
- // TODO: Investigate using SwiftUI to do this.
- // TODO: If that doesn't work, replace this Dictionary with NSCache or an
- // LRU cache. For some reason NSCache didn't store entries when I tried it,
- // but I didn't try for very long.
- private var thumbnailCache = Dictionary<PHAsset, PHAssetImage>()
+ // TODO: Investigate the SwiftUI caching behaviour.
+ //
+ // Note: the size of both this and the following cache are designed to balance
+ // memory usage and performance. Everything on the screen and just off it
+ // should be kept in cache, so I can e.g. switch between all the variants
+ // of a single shot, but I don't need more than that.
+ //
+ // On my M2 MacBook Air, these numbers mean the app peaks at ~250MB of memory,
+ // which seems pretty reasonable.
+ private var thumbnailCache = LRUCache<PHAsset, PHAssetImage>(withMaxSize: 100)
func getThumbnail(for asset: PHAsset) -> PHAssetImage {
- if let cachedThumbnail = thumbnailCache[asset] {
- return cachedThumbnail
+ if thumbnailCache[asset] == nil {
+ let newImage = PHAssetImage(
+ asset,
+ size: CGSize(width: 70, height: 70),
+ deliveryMode: .opportunistic
+ )
+
+ thumbnailCache[asset] = newImage
}
-
- let newThumbnail = PHAssetImage(
- asset,
- size: CGSize(width: 70, height: 70),
- deliveryMode: .opportunistic
- )
-
- thumbnailCache[asset] = newThumbnail
-
- return newThumbnail
+
+ return thumbnailCache[asset]!
}
// Implement a similar cache for full-sized images.
@@ -204,21 +208,19 @@ class PhotosLibrary: NSObject, ObservableObject, PHPhotoLibraryChangeObserver {
//
// TODO: Surely it should be possible to make SwiftUI cache views like
// this for us?
- private var fullSizeImageCache = Dictionary<PHAsset, PHAssetImage>()
+ private var fullSizeImageCache = LRUCache<PHAsset, PHAssetImage>(withMaxSize: 10)
func getFullSizedImage(for asset: PHAsset) -> PHAssetImage {
- if let cachedImage = fullSizeImageCache[asset] {
- return cachedImage
+ if fullSizeImageCache[asset] == nil {
+ let newImage = PHAssetImage(
+ asset,
+ size: PHImageManagerMaximumSize,
+ deliveryMode: .opportunistic
+ )
+
+ fullSizeImageCache[asset] = newImage
}
-
- let newImage = PHAssetImage(
- asset,
- size: PHImageManagerMaximumSize,
- deliveryMode: .opportunistic
- )
-
- fullSizeImageCache[asset] = newImage
-
- return newImage
+
+ return fullSizeImageCache[asset]!
}
}