diff --git a/Tusker.xcodeproj/project.pbxproj b/Tusker.xcodeproj/project.pbxproj index 6c476a48..e51401ec 100644 --- a/Tusker.xcodeproj/project.pbxproj +++ b/Tusker.xcodeproj/project.pbxproj @@ -113,6 +113,7 @@ D62D2426217ABF63005076CC /* UserActivityType.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62D2425217ABF63005076CC /* UserActivityType.swift */; }; D62FF04823D7CDD700909D6E /* AttributedStringHelperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62FF04723D7CDD700909D6E /* AttributedStringHelperTests.swift */; }; D6311C5025B3765B00B27539 /* ImageDataCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6311C4F25B3765B00B27539 /* ImageDataCache.swift */; }; + D6311C5625B4CEA000B27539 /* CachingDiskStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6311C5525B4CEA000B27539 /* CachingDiskStorage.swift */; }; D6333B372137838300CE884A /* AttributedString+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6333B362137838300CE884A /* AttributedString+Helpers.swift */; }; D6333B792139AEFD00CE884A /* Date+TimeAgo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6333B782139AEFD00CE884A /* Date+TimeAgo.swift */; }; D63569E023908A8D003DD353 /* StatusState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60A4FFB238B726A008AC647 /* StatusState.swift */; }; @@ -470,6 +471,7 @@ D62D2425217ABF63005076CC /* UserActivityType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserActivityType.swift; sourceTree = ""; }; D62FF04723D7CDD700909D6E /* AttributedStringHelperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributedStringHelperTests.swift; sourceTree = ""; }; D6311C4F25B3765B00B27539 /* ImageDataCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageDataCache.swift; sourceTree = ""; }; + D6311C5525B4CEA000B27539 /* CachingDiskStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CachingDiskStorage.swift; sourceTree = ""; }; D6333B362137838300CE884A /* AttributedString+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AttributedString+Helpers.swift"; sourceTree = ""; }; D6333B782139AEFD00CE884A /* Date+TimeAgo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+TimeAgo.swift"; sourceTree = ""; }; D63661BF2381C144004B9E16 /* PreferencesNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesNavigationController.swift; sourceTree = ""; }; @@ -1496,6 +1498,7 @@ D6F1F84C2193B56E00F5FE67 /* Cache.swift */, 04DACE8D212CC7CC009840C4 /* ImageCache.swift */, D6311C4F25B3765B00B27539 /* ImageDataCache.swift */, + D6311C5525B4CEA000B27539 /* CachingDiskStorage.swift */, ); path = Caching; sourceTree = ""; @@ -1874,6 +1877,7 @@ D6A3BC8A2321F79B00FD64D5 /* AccountTableViewCell.swift in Sources */, D66A77BB233838DC0058F1EC /* UIFont+Traits.swift in Sources */, D68FEC4F232C5BC300C84F23 /* SegmentedPageViewController.swift in Sources */, + D6311C5625B4CEA000B27539 /* CachingDiskStorage.swift in Sources */, D62275AA24F1E01C00B82A16 /* ComposeTextView.swift in Sources */, 0450531F22B0097E00100BA2 /* Timline+UI.swift in Sources */, D667E5F52135BCD50057A976 /* ConversationTableViewController.swift in Sources */, diff --git a/Tusker/Caching/CachingDiskStorage.swift b/Tusker/Caching/CachingDiskStorage.swift new file mode 100644 index 00000000..e666d20b --- /dev/null +++ b/Tusker/Caching/CachingDiskStorage.swift @@ -0,0 +1,71 @@ +// +// CachingDiskStorage.swift +// Tusker +// +// Created by Shadowfacts on 1/17/21. +// Copyright © 2021 Shadowfacts. All rights reserved. +// + +import Foundation +import Cache + +/// This class wraps a `DiskStorage` and maintains an in-memory cache of whether objects +/// exist on disk to avoid unnecessary disk I/O when calling synchronous methods like +/// `existsObject(forKey:)`. +class CachingDiskStorage: StorageAware { + + private let storage: DiskStorage + private var files = [String: FileState]() + + init(config: DiskConfig, transformer: Transformer) throws { + storage = try DiskStorage(config: config, transformer: transformer) + } + + private func state(for key: String) -> FileState { + return files[key] ?? .unknown + } + + func entry(forKey key: String) throws -> Entry { + if state(for: key) == .doesNotExist { + throw StorageError.notFound + } + return try storage.entry(forKey: key) + } + + func existsObject(forKey key: String) throws -> Bool { + switch state(for: key) { + case .unknown: + return try storage.existsObject(forKey: key) + case .exists: + return true + case .doesNotExist: + return false + } + } + + func removeObject(forKey key: String) throws { + try storage.removeObject(forKey: key) + files[key] = .doesNotExist + } + + func setObject(_ object: T, forKey key: String, expiry: Expiry? = nil) throws { + try storage.setObject(object, forKey: key, expiry: expiry) + files[key] = .exists + } + + func removeAll() throws { + try storage.removeAll() + files.removeAll() + } + + func removeExpiredObjects() throws { + try storage.removeExpiredObjects() + } + +} + +extension CachingDiskStorage { + private enum FileState { + case unknown, exists, doesNotExist + } +} diff --git a/Tusker/Caching/ImageCache.swift b/Tusker/Caching/ImageCache.swift index 2441b5e6..2fb58bde 100644 --- a/Tusker/Caching/ImageCache.swift +++ b/Tusker/Caching/ImageCache.swift @@ -37,10 +37,6 @@ class ImageCache { func get(_ url: URL, completion: ((Data?, UIImage?) -> Void)?) -> Request? { let key = url.absoluteString if !ImageCache.disableCaching, - // todo: calling object(forKey: key) does disk I/O and this method is often called from the main thread - // in performance sensitive paths. a nice optimization to DiskStorage would be adding an internal cache - // of the state (unknown/exists/does not exist) of whether or not objects exist on disk so that the slow, disk I/O - // path can be avoided most of the time let entry = try? cache.get(key) { if let completion = completion { backgroundQueue.async { diff --git a/Tusker/Caching/ImageDataCache.swift b/Tusker/Caching/ImageDataCache.swift index a8a9e87d..201ef63b 100644 --- a/Tusker/Caching/ImageDataCache.swift +++ b/Tusker/Caching/ImageDataCache.swift @@ -12,7 +12,7 @@ import Cache class ImageDataCache { private let memory: MemoryStorage - private let disk: DiskStorage? + private let disk: CachingDiskStorage? private let storeOriginalDataInMemory: Bool private let desiredPixelSize: CGSize? @@ -23,7 +23,7 @@ class ImageDataCache { if let diskExpiry = diskExpiry { let diskConfig = DiskConfig(name: name, expiry: diskExpiry) - self.disk = try! DiskStorage(config: diskConfig, transformer: TransformerFactory.forData()) + self.disk = try! CachingDiskStorage(config: diskConfig, transformer: TransformerFactory.forData()) } else { self.disk = nil }