-
Notifications
You must be signed in to change notification settings - Fork 1.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add ImagePrefetcher (and swift-collection) and integrate in Reader #23928
Conversation
Generated by 🚫 Danger |
📲 You can test the changes from this Pull Request in WordPress Alpha by scanning the QR code below to install the corresponding build.
|
📲 You can test the changes from this Pull Request in Jetpack Alpha by scanning the QR code below to install the corresponding build.
|
import Collections | ||
|
||
@ImageDownloaderActor | ||
public final class ImagePrefetcher { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any particular reason of using a global actor, instead of making ImagePrefetcher
an actor?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's much easier to reason about code when every part of the system – image downloader in that case – is synchronized on the same actor. The main reason is that you can avoid a lot of async
calls because when you are on an actor (@ImageDownloaderActor
), you don't need async
to invoke methods on other classes synchronized on the same actor.
It's also beneficial for performance as you can avoid unnecessary context switches. For example, when ImagePrefetcher
calls _ = try? await downloader.image(for: request)
, there is no context switch as the code already runs on @ImageDownloaderActor
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure if there are any situation where a new actor would be preferable to a global actor or another form of synchronization.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmmm, I'm kind of on the opposite side of this ⬆️ . I would always define actor
types, rather than define global actors. I see global actors as a way to synchronize multiple types, which I rarely need.
It's also beneficial for performance as you can avoid unnecessary context switches. For example, when ImagePrefetcher calls _ = try? await downloader.image(for: request), there is no context switch as the code already runs on @ImageDownloaderActor.
Ah, I missed that ImageDownloaderActor
is used by other types. Now it makes sense to me why ImageDownloaderActor
global actor is used here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree, I think creating a new actor
is a good starting point if there is no synchronization needed across different types/methods.
} | ||
performPendingTasks() | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This function looks a bit weird to me. Although the function signature says it's non-isolated, semantically, the implementation is isolated.
IMHO, it makes more sense to remove nonisolated
and make callers create Task { await prefetcher.startPrefectching(...) }
instead.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, this doesn't feel quite right, but in this case, you will have to write Task { @ImageDownloaderActor in
in 100% of the places where you use it, so I opted to make it nonisolated
and callable from any isolation domain, including the main actor. It's easier and cleaner from the caller point of view.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would you need the @ImageDownloaderActor
part? I thought the func
in Task { await actor.func() }
would just run in the actor, regardless actor
is an actor
type or bound to an global actor.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The rules for isolation inheritance are a bit tricky. In this case, since that method that creates a Task
is nonisolated
, so is the task.
nonisolated func startPrefetching(...) {
Task {
// The closure is **not** isolated because the containing method is
// ``nonisolated``. It will require an `await` (async invocation).
// await startPrefetching(for: request)
}
}
nonisolated func startPrefetching(...) {
Task { @ImageDownloaderActor in
// OK since the closure is explicitly isolated to `@ImageDownloaderActor`
startPrefetching(for: request)
}
}
func startPrefetching(...) {
Task {
// The closure is isolated to the containing isolation domain (of the containing type)
startPrefetching(for: request)
}
}
public final class ImagePrefetcher { | ||
private let downloader: ImageDownloader | ||
private let maxConcurrentTasks: Int | ||
private var queue = OrderedDictionary<PrefetchKey, PrefetchTask>() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I feel like OperationQueue
might be a better suit here. It manages two key things here: concurrency and queue.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
concurrency
This part we don't really need because the entire systems already runs in the background.
If you need an async Operation
, I think these have outlived their usefulness in the Swift Concurrency world. There needs to be a new primitive in the standard library. But it's relatively easy to limit a maximum number of tasks even without one.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For me personally, I probably would choose OperationQueue
over implementing task orchestration (which is the core implementation here) myself, even though it does not feel like a "modern tech". 😆
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should find a good open-source implementation that's sound and covered with tests. If you have something in mind, please let me know. I also recently took a stab at it https://github.com/kean/Nuke/blob/task-queue/Sources/Nuke/Internal/WorkQueue.swift, but it's not finished yet, and I'm not sure if there is a way to make it general-purpose by allowing you to customize it with an actor.
} | ||
queue.removeAll() | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should maxConcurrentTasks
value be updated in stopPrefetching
and stopAll
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's probably worth writing a few unit tests for this class.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed. Yeah, it needs some tests 😩
Fixes #4288 by adding prefetching to the Reader stream. This PR also adds swift-collection as a dependency.
To test:
Regression Notes
Potential unintended areas of impact
What I did to test those areas of impact (or what existing automated tests I relied on)
What automated tests I added (or what prevented me from doing so)
PR submission checklist:
RELEASE-NOTES.txt
if necessary.Testing checklist: