Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions Maccy/ApplicationImageCache.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ class ApplicationImageCache {
"com.apple.finder.Open-iCloudDrive"
private let fallback = ApplicationImage(bundleIdentifier: nil)
private var cache: [String: ApplicationImage] = [:]

// Limit cache to 100 entries to prevent unbounded memory growth
// This is more than enough for typical application bundle identifiers
private let maxCacheSize = 100

// Track insertion order for LRU eviction
private var cacheOrder: [String] = []

func getImage(item: HistoryItem) -> ApplicationImage {
guard let bundleIdentifier = bundleIdentifier(for: item) else {
Expand All @@ -16,7 +23,18 @@ class ApplicationImageCache {
}

let image = ApplicationImage(bundleIdentifier: bundleIdentifier)

// Add to cache with LRU eviction if needed
if cache.count >= maxCacheSize {
// Remove least recently used (first inserted)
if let oldestKey = cacheOrder.first {
cache.removeValue(forKey: oldestKey)
cacheOrder.removeFirst()
}
}

cache[bundleIdentifier] = image
cacheOrder.append(bundleIdentifier)

return image
}
Expand Down
33 changes: 32 additions & 1 deletion Maccy/Models/HistoryItem.swift
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,11 @@ class HistoryItem {
@Relationship(deleteRule: .cascade, inverse: \HistoryItemContent.item)
var contents: [HistoryItemContent] = []

// MARK: - In-Memory Caches (not persisted)
// Cache the NSImage to avoid recreating it from data on every access
@Transient private var cachedImage: NSImage?
@Transient private var imageDataCache: Data?

init(contents: [HistoryItemContent] = []) {
self.firstCopiedAt = firstCopiedAt
self.lastCopiedAt = lastCopiedAt
Expand Down Expand Up @@ -148,21 +153,39 @@ class HistoryItem {
}

var imageData: Data? {
// Check if we already have cached data
if let cached = imageDataCache {
return cached
}

var data: Data?
data = contentData([.tiff, .png, .jpeg, .heic])
if data == nil, universalClipboardImage, let url = fileURLs.first {
data = try? Data(contentsOf: url)
}

// Cache the data if we found any
if data != nil {
imageDataCache = data
}

return data
}

var image: NSImage? {
// Return cached image if available
if let cached = cachedImage {
return cached
}

guard let data = imageData else {
return nil
}

return NSImage(data: data)
// Create and cache the NSImage
let newImage = NSImage(data: data)
cachedImage = newImage
return newImage
}

var rtfData: Data? { contentData([.rtf]) }
Expand Down Expand Up @@ -240,4 +263,12 @@ class HistoryItem {

self.title = recognizedStrings.joined(separator: "\n")
}

// MARK: - Memory Management
/// Clears cached image data to free memory. Called when item is being deleted or memory is needed.
func clearImageCache() {
cachedImage?.recache()
cachedImage = nil
imageDataCache = nil
}
}
4 changes: 4 additions & 0 deletions Maccy/Observables/History.swift
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,8 @@ class History: ItemsContainer { // swiftlint:disable:this type_body_length
Storage.shared.context.delete(existingHistoryItem)
removedItemIndex = all.firstIndex(where: { $0.item == existingHistoryItem })
if let removedItemIndex {
// Clean up the decorator before removing it
cleanup(all[removedItemIndex])
all.remove(at: removedItemIndex)
}
} else {
Expand Down Expand Up @@ -288,6 +290,8 @@ class History: ItemsContainer { // swiftlint:disable:this type_body_length
@MainActor
private func cleanup(_ item: HistoryItemDecorator) {
item.cleanupImages()
// Clear the underlying image cache to free memory
item.item.clearImageCache()
}

private func currentModifierFlags() -> NSEvent.ModifierFlags {
Expand Down
9 changes: 9 additions & 0 deletions Maccy/Observables/HistoryItemDecorator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,15 @@ class HistoryItemDecorator: Identifiable, Hashable, HasVisibility {
synchronizeItemTitle()
}

deinit {
// Avoid MainActor-isolated calls from deinit; tear down lightweight resources directly.
thumbnailImageGenerationTask?.cancel()
previewImageGenerationTask?.cancel()
thumbnailImage = nil
previewImage = nil
item.clearImageCache()
}

@MainActor
func ensureThumbnailImage() {
guard item.image != nil else {
Expand Down