From 6750cce6aab10414938e8b5481135c54b3bb5594 Mon Sep 17 00:00:00 2001 From: nimbold <11913706+nimbold@users.noreply.github.com> Date: Wed, 3 Jun 2026 07:03:18 +0330 Subject: [PATCH] perf: reduce main thread CPU usage and SSD wear from frequent JSON serialization --- Sources/Firelink/AppSettings.swift | 13 ++++++++-- Sources/Firelink/DownloadController.swift | 26 +++++++++++++------- Sources/Firelink/DownloadTable.swift | 29 ++++++++++++++++------- 3 files changed, 49 insertions(+), 19 deletions(-) diff --git a/Sources/Firelink/AppSettings.swift b/Sources/Firelink/AppSettings.swift index fc00bfa..4b95ada 100644 --- a/Sources/Firelink/AppSettings.swift +++ b/Sources/Firelink/AppSettings.swift @@ -135,6 +135,7 @@ final class AppSettings: ObservableObject { private let defaults: UserDefaults private let storageKey = "Firelink.AppSettings.v1" + private var saveTask: Task? init(defaults: UserDefaults = .standard) { self.defaults = defaults @@ -247,9 +248,17 @@ final class AppSettings: ObservableObject { downloadDirectories: Dictionary(uniqueKeysWithValues: downloadDirectories.map { ($0.key.rawValue, $0.value) }), siteLogins: siteLogins ) + let defaults = self.defaults + let storageKey = self.storageKey - if let data = try? JSONEncoder().encode(stored) { - defaults.set(data, forKey: storageKey) + saveTask?.cancel() + saveTask = Task { @MainActor [defaults, storageKey] in + let data = await Task.detached(priority: .background) { + try? JSONEncoder().encode(stored) + }.value + + guard !Task.isCancelled, let encoded = data else { return } + defaults.set(encoded, forKey: storageKey) } } diff --git a/Sources/Firelink/DownloadController.swift b/Sources/Firelink/DownloadController.swift index f3f3f85..fc2560e 100644 --- a/Sources/Firelink/DownloadController.swift +++ b/Sources/Firelink/DownloadController.swift @@ -23,6 +23,7 @@ final class DownloadController: ObservableObject { let supportDir = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first ?? URL(fileURLWithPath: NSHomeDirectory()) return supportDir.appendingPathComponent("Firelink").appendingPathComponent("downloads.json") }() + private var saveTask: Task? init(settings: AppSettings) { self.settings = settings @@ -674,14 +675,23 @@ final class DownloadController: ObservableObject { } private func saveDownloads() { - do { - let directory = storageURL.deletingLastPathComponent() - try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true, attributes: nil) - let state = StoredDownloadState(queues: queues, downloads: downloads) - let data = try JSONEncoder().encode(state) - try data.write(to: storageURL, options: .atomic) - } catch { - print("Failed to save downloads: \(error)") + let queuesCopy = queues + let downloadsCopy = downloads + let storageURL = self.storageURL + + saveTask?.cancel() + saveTask = Task.detached(priority: .background) { + do { + let directory = storageURL.deletingLastPathComponent() + try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true, attributes: nil) + let state = StoredDownloadState(queues: queuesCopy, downloads: downloadsCopy) + let data = try JSONEncoder().encode(state) + + guard !Task.isCancelled else { return } + try data.write(to: storageURL, options: .atomic) + } catch { + print("Failed to save downloads: \(error)") + } } } diff --git a/Sources/Firelink/DownloadTable.swift b/Sources/Firelink/DownloadTable.swift index be5275c..be51545 100644 --- a/Sources/Firelink/DownloadTable.swift +++ b/Sources/Firelink/DownloadTable.swift @@ -64,6 +64,7 @@ final class TableSettings: ObservableObject { private let defaults = UserDefaults.standard private let storageKey = "Firelink.TableSettings.v1" + private var saveTask: Task? init() { let defaultVisibleColumns: Set = [.fileName, .size, .progress, .speed, .eta, .dateAdded] @@ -90,8 +91,16 @@ final class TableSettings: ObservableObject { sortColumn: sortColumn, sortDirection: sortDirection ) - if let data = try? JSONEncoder().encode(stored) { - defaults.set(data, forKey: storageKey) + let storageKey = self.storageKey + + saveTask?.cancel() + saveTask = Task { @MainActor in + let data = await Task.detached(priority: .background) { + try? JSONEncoder().encode(stored) + }.value + + guard !Task.isCancelled, let encoded = data else { return } + UserDefaults.standard.set(encoded, forKey: storageKey) } } } @@ -114,7 +123,7 @@ struct DownloadTable: View { @StateObject private var tableSettings = TableSettings() @State private var pendingDeleteItems: Set? - @State private var resizeBaseWidths: [DownloadColumn: CGFloat] = [:] + @State private var dragOffsets: [DownloadColumn: CGFloat] = [:] @State private var lastSelectedIndex: Int? @State private var draggedItemID: DownloadItem.ID? @@ -283,12 +292,12 @@ struct DownloadTable: View { .gesture( DragGesture(minimumDistance: 1) .onChanged { value in - let baseWidth = resizeBaseWidths[column] ?? width(for: column) - resizeBaseWidths[column] = baseWidth - tableSettings.columnWidths[column] = max(70, baseWidth + value.translation.width) + dragOffsets[column] = value.translation.width } - .onEnded { _ in - resizeBaseWidths[column] = nil + .onEnded { value in + let baseWidth = tableSettings.columnWidths[column] ?? column.width + tableSettings.columnWidths[column] = max(70, baseWidth + value.translation.width) + dragOffsets[column] = nil } ) } @@ -445,7 +454,9 @@ struct DownloadTable: View { } private func width(for column: DownloadColumn) -> CGFloat { - max(70, tableSettings.columnWidths[column] ?? column.width) + let baseWidth = tableSettings.columnWidths[column] ?? column.width + let offset = dragOffsets[column] ?? 0 + return max(70, baseWidth + offset) } private var sortedItems: [DownloadItem] {