From 0587c64598267ecace6259241198425cdc284f3a Mon Sep 17 00:00:00 2001 From: Fuwn Date: Wed, 18 Feb 2026 12:33:44 -0800 Subject: perf: reduce suggestion and image handling hot-path overhead --- Sora/Data/ImageCacheManager.swift | 77 +++++++++++++++++++++++++++++---------- 1 file changed, 57 insertions(+), 20 deletions(-) (limited to 'Sora/Data/ImageCacheManager.swift') diff --git a/Sora/Data/ImageCacheManager.swift b/Sora/Data/ImageCacheManager.swift index 8a14198..5561f24 100644 --- a/Sora/Data/ImageCacheManager.swift +++ b/Sora/Data/ImageCacheManager.swift @@ -1,4 +1,3 @@ -import Combine @preconcurrency import SwiftUI @MainActor @@ -13,14 +12,13 @@ final class ImageCacheManager { directory: FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first? .appendingPathComponent("SoraImageCache", isDirectory: true) ) - private var cancellables = Set() private let downloadQueue = OperationQueue() private var preloadingURLs = Set() private var memoryWarningObserver: NSObjectProtocol? // MARK: - Initialisation private init() { - downloadQueue.maxConcurrentOperationCount = 5 + downloadQueue.maxConcurrentOperationCount = 3 downloadQueue.qualityOfService = .utility #if os(iOS) @@ -38,46 +36,85 @@ final class ImageCacheManager { // MARK: - Public Methods func preloadImages(_ urls: [URL]) { - let newURLs = urls.filter { !preloadingURLs.contains($0) } + let newURLs = urls.filter { url in + !preloadingURLs.contains(url) && cache.cachedResponse(for: URLRequest(url: url)) == nil + } for url in newURLs { preloadingURLs.insert(url) - downloadQueue.addOperation { - let cancellable = URLSession.shared.dataTaskPublisher(for: url) - .map { CachedURLResponse(response: $0.response, data: $0.data) } - .sink( - receiveCompletion: { _ in - DispatchQueue.main.async { [weak self] in - self?.preloadingURLs.remove(url) - } - }, - receiveValue: { [weak self] cachedResponse in - self?.cache.storeCachedResponse(cachedResponse, for: URLRequest(url: url)) + downloadQueue.addOperation { [weak self] in + guard let self else { return } + + let completionSignal = DispatchSemaphore(value: 0) + + URLSession.shared.dataTask(with: url) { data, response, _ in + Task { @MainActor [weak self] in + guard let self else { + completionSignal.signal() + return + } + + defer { + preloadingURLs.remove(url) + completionSignal.signal() } - ) - DispatchQueue.main.async { [weak self] in - self?.cancellables.insert(cancellable) + guard let data, let response else { return } + + storeCachedResponse( + data: data, + response: response, + for: url + ) + } } + .resume() + + completionSignal.wait() } } } func clearCache() { cache.removeAllCachedResponses() - cancellables.removeAll() preloadingURLs.removeAll() } + func storeCachedResponse(data: Data, response: URLResponse, for url: URL) { + cache.storeCachedResponse( + CachedURLResponse(response: response, data: data), + for: URLRequest(url: url) + ) + } + func getCachedResponse(for url: URL) -> CachedURLResponse? { cache.cachedResponse(for: URLRequest(url: url)) } + func loadImageData(for url: URL) async -> Data? { + if let cachedResponse = getCachedResponse(for: url) { + return cachedResponse.data + } + + do { + let (data, response) = try await URLSession.shared.data(from: url) + + storeCachedResponse( + data: data, + response: response, + for: url + ) + + return data + } catch { + return nil + } + } + private func handleMemoryPressure() { cache.removeAllCachedResponses() downloadQueue.cancelAllOperations() - cancellables.removeAll() preloadingURLs.removeAll() } -- cgit v1.2.3