perf: reduce main thread CPU usage and SSD wear from frequent JSON serialization

This commit is contained in:
nimbold
2026-06-03 07:03:18 +03:30
parent 1d22b16119
commit 6750cce6aa
3 changed files with 49 additions and 19 deletions
+11 -2
View File
@@ -135,6 +135,7 @@ final class AppSettings: ObservableObject {
private let defaults: UserDefaults private let defaults: UserDefaults
private let storageKey = "Firelink.AppSettings.v1" private let storageKey = "Firelink.AppSettings.v1"
private var saveTask: Task<Void, Never>?
init(defaults: UserDefaults = .standard) { init(defaults: UserDefaults = .standard) {
self.defaults = defaults self.defaults = defaults
@@ -247,9 +248,17 @@ final class AppSettings: ObservableObject {
downloadDirectories: Dictionary(uniqueKeysWithValues: downloadDirectories.map { ($0.key.rawValue, $0.value) }), downloadDirectories: Dictionary(uniqueKeysWithValues: downloadDirectories.map { ($0.key.rawValue, $0.value) }),
siteLogins: siteLogins siteLogins: siteLogins
) )
let defaults = self.defaults
let storageKey = self.storageKey
if let data = try? JSONEncoder().encode(stored) { saveTask?.cancel()
defaults.set(data, forKey: storageKey) 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)
} }
} }
+18 -8
View File
@@ -23,6 +23,7 @@ final class DownloadController: ObservableObject {
let supportDir = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first ?? URL(fileURLWithPath: NSHomeDirectory()) let supportDir = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first ?? URL(fileURLWithPath: NSHomeDirectory())
return supportDir.appendingPathComponent("Firelink").appendingPathComponent("downloads.json") return supportDir.appendingPathComponent("Firelink").appendingPathComponent("downloads.json")
}() }()
private var saveTask: Task<Void, Never>?
init(settings: AppSettings) { init(settings: AppSettings) {
self.settings = settings self.settings = settings
@@ -674,14 +675,23 @@ final class DownloadController: ObservableObject {
} }
private func saveDownloads() { private func saveDownloads() {
do { let queuesCopy = queues
let directory = storageURL.deletingLastPathComponent() let downloadsCopy = downloads
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true, attributes: nil) let storageURL = self.storageURL
let state = StoredDownloadState(queues: queues, downloads: downloads)
let data = try JSONEncoder().encode(state) saveTask?.cancel()
try data.write(to: storageURL, options: .atomic) saveTask = Task.detached(priority: .background) {
} catch { do {
print("Failed to save downloads: \(error)") 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)")
}
} }
} }
+20 -9
View File
@@ -64,6 +64,7 @@ final class TableSettings: ObservableObject {
private let defaults = UserDefaults.standard private let defaults = UserDefaults.standard
private let storageKey = "Firelink.TableSettings.v1" private let storageKey = "Firelink.TableSettings.v1"
private var saveTask: Task<Void, Never>?
init() { init() {
let defaultVisibleColumns: Set<DownloadColumn> = [.fileName, .size, .progress, .speed, .eta, .dateAdded] let defaultVisibleColumns: Set<DownloadColumn> = [.fileName, .size, .progress, .speed, .eta, .dateAdded]
@@ -90,8 +91,16 @@ final class TableSettings: ObservableObject {
sortColumn: sortColumn, sortColumn: sortColumn,
sortDirection: sortDirection sortDirection: sortDirection
) )
if let data = try? JSONEncoder().encode(stored) { let storageKey = self.storageKey
defaults.set(data, forKey: 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() @StateObject private var tableSettings = TableSettings()
@State private var pendingDeleteItems: Set<DownloadItem.ID>? @State private var pendingDeleteItems: Set<DownloadItem.ID>?
@State private var resizeBaseWidths: [DownloadColumn: CGFloat] = [:] @State private var dragOffsets: [DownloadColumn: CGFloat] = [:]
@State private var lastSelectedIndex: Int? @State private var lastSelectedIndex: Int?
@State private var draggedItemID: DownloadItem.ID? @State private var draggedItemID: DownloadItem.ID?
@@ -283,12 +292,12 @@ struct DownloadTable: View {
.gesture( .gesture(
DragGesture(minimumDistance: 1) DragGesture(minimumDistance: 1)
.onChanged { value in .onChanged { value in
let baseWidth = resizeBaseWidths[column] ?? width(for: column) dragOffsets[column] = value.translation.width
resizeBaseWidths[column] = baseWidth
tableSettings.columnWidths[column] = max(70, baseWidth + value.translation.width)
} }
.onEnded { _ in .onEnded { value in
resizeBaseWidths[column] = nil 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 { 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] { private var sortedItems: [DownloadItem] {