Skip to content

Commit

Permalink
Some improvements to throttling (#3079)
Browse files Browse the repository at this point in the history
* Improvements to throttling.

* wip
  • Loading branch information
mbrandonw authored Jun 21, 2024
1 parent 476050c commit 3d343f1
Show file tree
Hide file tree
Showing 4 changed files with 111 additions and 45 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -122,8 +122,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-perception",
"state" : {
"revision" : "d8340521e532cffdf75a64468ff9362de8bd2bb9",
"version" : "1.2.3"
"revision" : "d3ab98dc2887d1cc3bed676f6fa354da4cb22b3c",
"version" : "1.2.4"
}
},
{
Expand Down Expand Up @@ -158,8 +158,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swiftui-navigation.git",
"state" : {
"revision" : "7ab04c6e2e6a73d34d5a762970ef88bf0aedb084",
"version" : "1.4.0"
"revision" : "b7c9a79f6f6b1fefb87d3e5a83a9c2fe7cdc9720",
"version" : "1.5.0"
}
},
{
Expand Down
4 changes: 2 additions & 2 deletions Package.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-identified-collections",
"state" : {
"revision" : "2481e39ea43e14556ca9628259fa6b377427730c",
"version" : "1.0.1"
"revision" : "2f5ab6e091dd032b63dacbda052405756010dc3b",
"version" : "1.1.0"
}
},
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,14 @@ public final class FileStorageKey<Value: Codable & Sendable>: PersistenceKey, Se
private let storage: FileStorage
private let isSetting = LockIsolated(false)
private let url: URL
private let value = LockIsolated<Value?>(nil)
private let workItem = LockIsolated<DispatchWorkItem?>(nil)
fileprivate let state = LockIsolated(State())
// private let value = LockIsolated<Value?>(nil)
// private let workItem = LockIsolated<DispatchWorkItem?>(nil)

fileprivate struct State {
var value: Value?
var workItem: DispatchWorkItem?
}

public var id: AnyHashable {
FileStorageKeyID(url: self.url, storage: self.storage)
Expand All @@ -42,28 +48,32 @@ public final class FileStorageKey<Value: Codable & Sendable>: PersistenceKey, Se
}

public func save(_ value: Value) {
if self.workItem.value == nil {
self.isSetting.setValue(true)
try? self.storage.save(JSONEncoder().encode(value), self.url)
let workItem = DispatchWorkItem { [weak self] in
guard let self else { return }
defer {
self.value.setValue(nil)
self.workItem.setValue(nil)
}
guard let value = self.value.value
else { return }
self.state.withValue { state in
if state.workItem == nil {
self.isSetting.setValue(true)
try? self.storage.save(JSONEncoder().encode(value), self.url)
}
self.workItem.setValue(workItem)
if canListenForResignActive {
self.storage.asyncAfter(.seconds(1), workItem)
let workItem = DispatchWorkItem { [weak self] in
guard let self else { return }
self.state.withValue { state in
defer {
state.value = nil
state.workItem = nil
}
guard let value = state.value
else { return }
self.isSetting.setValue(true)
try? self.storage.save(JSONEncoder().encode(value), self.url)
}
}
state.workItem = workItem
if canListenForResignActive {
self.storage.asyncAfter(.seconds(1), workItem)
} else {
self.storage.async(workItem)
}
} else {
self.storage.async(workItem)
state.value = value
}
} else {
self.value.setValue(value)
}
}

Expand All @@ -82,17 +92,21 @@ public final class FileStorageKey<Value: Codable & Sendable>: PersistenceKey, Se
try? self.storage.save(Data(), self.url)
}
let writeCancellable = self.storage.fileSystemSource(self.url, [.write]) {
if self.isSetting.value == true {
self.isSetting.setValue(false)
} else {
self.workItem.withValue {
$0?.cancel()
$0 = nil
self.state.withValue { state in
if self.isSetting.value == true {
self.isSetting.setValue(false)
} else {
state.workItem?.cancel()
state.workItem = nil
didSet(self.load(initialValue: initialValue))
}
didSet(self.load(initialValue: initialValue))
}
}
let deleteCancellable = self.storage.fileSystemSource(self.url, [.delete, .rename]) {
self.state.withValue { state in
state.workItem?.cancel()
state.workItem = nil
}
`didSet`(self.load(initialValue: initialValue))
setUpSources()
}
Expand Down Expand Up @@ -143,17 +157,19 @@ public final class FileStorageKey<Value: Codable & Sendable>: PersistenceKey, Se
}

private func performImmediately() {
guard let workItem = self.workItem.value
else { return }
self.storage.async(workItem)
self.storage.async(
DispatchWorkItem {
self.workItem.withValue {
$0?.cancel()
$0 = nil
self.state.withValue { state in
guard let workItem = state.workItem
else { return }
self.storage.async(workItem)
self.storage.async(
DispatchWorkItem {
self.state.withValue { state in
state.workItem?.cancel()
state.workItem = nil
}
}
}
)
)
}
}
}

Expand Down Expand Up @@ -270,7 +286,9 @@ public struct FileStorage: Hashable, Sendable {
asyncAfter: { scheduler.schedule(after: scheduler.now.advanced(by: .init($0)), $1.perform) },
createDirectory: { _, _ in },
fileExists: { fileSystem.keys.contains($0) },
fileSystemSource: { url, _, handler in
fileSystemSource: { url, event, handler in
guard event.contains(.write)
else { return AnyCancellable {} }
let handler = Handler(operation: handler)
sourceHandlers.withValue { _ = $0[url, default: []].insert(handler) }
return AnyCancellable {
Expand Down
50 changes: 49 additions & 1 deletion Tests/ComposableArchitectureTests/FileStorageTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ final class FileStorageTests: XCTestCase {
}

@MainActor
func testWriteFileWhileDebouncing() throws {
func testWriteFileWhileThrottling() throws {
let fileSystem = LockIsolated<[URL: Data]>([:])
let scheduler = DispatchQueue.test
let fileStorage = FileStorage.inMemory(
Expand All @@ -235,6 +235,8 @@ final class FileStorageTests: XCTestCase {
@Shared(.fileStorage(.fileURL)) var users = [User]()

users.append(.blob)
try XCTAssertNoDifference(fileSystem.value.users(for: .fileURL), [.blob])

try fileStorage.save(Data(), .fileURL)
scheduler.run()
XCTAssertNoDifference(users, [])
Expand Down Expand Up @@ -386,6 +388,52 @@ final class FileStorageTests: XCTestCase {
XCTAssertEqual(shared1.wrappedValue.name, "Blob Jr")
XCTAssertEqual(shared2.wrappedValue.name, "Blob Sr")
}

func testCancelThrottleWhenFileIsDeleted() async throws {
try await withMainSerialExecutor {
try? FileManager.default.removeItem(at: .fileURL)

try await withDependencies {
$0.defaultFileStorage = .fileSystem
} operation: {
@Shared(.fileStorage(.fileURL)) var users = [User.blob]
await Task.yield()
XCTAssertNoDifference(users, [.blob])

$users.withLock { $0 = [.blobJr] } // NB: Saved immediately
$users.withLock { $0 = [.blobSr] } // NB: Throttled for 1 second
try FileManager.default.removeItem(at: .fileURL)
try await Task.sleep(nanoseconds: 1_200_000_000)
XCTAssertNoDifference(users, [.blob])
try XCTAssertEqual(Data(contentsOf: .fileURL), Data())
}
}
}

func testWritesFromManyThreads() async {
let fileSystem = LockIsolated<[URL: Data]>([:])
let fileStorage = FileStorage.inMemory(
fileSystem: fileSystem,
scheduler: DispatchQueue.main.eraseToAnyScheduler()
)

await withDependencies {
$0.defaultFileStorage = fileStorage
} operation: {
@Shared(.fileStorage(.fileURL)) var count = 0
let max = 10_000
await withTaskGroup(of: Void.self) { group in
for index in (1...max) {
group.addTask { [count = $count] in
try? await Task.sleep(for: .milliseconds(Int.random(in: 200...3_000)))
await count.withLock { $0 += index }
}
}
}

XCTAssertEqual(count, max * (max + 1) / 2)
}
}
}

extension URL {
Expand Down

0 comments on commit 3d343f1

Please sign in to comment.