Skip to content
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

Merged
merged 2 commits into from
Dec 30, 2024

Conversation

kean
Copy link
Contributor

@kean kean commented Dec 30, 2024

Fixes #4288 by adding prefetching to the Reader stream. This PR also adds swift-collection as a dependency.

To test:

Regression Notes

  1. Potential unintended areas of impact

  2. What I did to test those areas of impact (or what existing automated tests I relied on)

  3. What automated tests I added (or what prevented me from doing so)

PR submission checklist:

  • I have completed the Regression Notes.
  • I have considered adding unit tests for my changes.
  • I have considered adding accessibility improvements for my changes.
  • I have considered if this change warrants user-facing release notes and have added them to RELEASE-NOTES.txt if necessary.

Testing checklist:

  • WordPress.com sites and self-hosted Jetpack sites.
  • Portrait and landscape orientations.
  • Light and dark modes.
  • Fonts: Larger, smaller and bold text.
  • High contrast.
  • VoiceOver.
  • Languages with large words or with letters/accents not frequently used in English.
  • Right-to-left languages. (Even if translation isn’t complete, formatting should still respect the right-to-left layout)
  • iPhone and iPad.
  • Multi-tasking: Split view and Slide over. (iPad)

@kean kean added the Reader label Dec 30, 2024
@kean kean added this to the 25.7 milestone Dec 30, 2024
@dangermattic
Copy link
Collaborator

1 Warning
⚠️ Modules/Package.swift was changed without updating its corresponding Package.resolved. Please resolve the Swift packages as appropriate to your project setup (e.g. in Xcode or by running swift package resolve).

Generated by 🚫 Danger

@kean kean merged commit 21a133e into christmas-feature-branch Dec 30, 2024
10 of 22 checks passed
@kean kean deleted the task/image-prefetcher branch December 30, 2024 19:11
@kean kean mentioned this pull request Dec 30, 2024
14 tasks
@kean kean changed the title Add ImagePrefetcher Add ImagePrefetcher (and swift-collection)and integrate in Reader Dec 30, 2024
@kean kean changed the title Add ImagePrefetcher (and swift-collection)and integrate in Reader Add ImagePrefetcher (and swift-collection) and integrate in Reader Dec 30, 2024
@wpmobilebot
Copy link
Contributor

WordPress Alpha📲 You can test the changes from this Pull Request in WordPress Alpha by scanning the QR code below to install the corresponding build.
App NameWordPress Alpha WordPress Alpha
ConfigurationRelease-Alpha
Build Numberpr23928-65f91b0
Version25.6
Bundle IDorg.wordpress.alpha
Commit65f91b0
App Center BuildWPiOS - One-Offs #11234
Automatticians: You can use our internal self-serve MC tool to give yourself access to App Center if needed.

@wpmobilebot
Copy link
Contributor

Jetpack Alpha📲 You can test the changes from this Pull Request in Jetpack Alpha by scanning the QR code below to install the corresponding build.
App NameJetpack Alpha Jetpack Alpha
ConfigurationRelease-Alpha
Build Numberpr23928-65f91b0
Version25.6
Bundle IDcom.jetpack.alpha
Commit65f91b0
App Center Buildjetpack-installable-builds #10272
Automatticians: You can use our internal self-serve MC tool to give yourself access to App Center if needed.

import Collections

@ImageDownloaderActor
public final class ImagePrefetcher {
Copy link
Contributor

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?

Copy link
Contributor Author

@kean kean Jan 13, 2025

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.

Copy link
Contributor Author

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.

Copy link
Contributor

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.

Copy link
Contributor Author

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()
}
}
Copy link
Contributor

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.

Copy link
Contributor Author

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.

Copy link
Contributor

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.

Copy link
Contributor Author

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>()
Copy link
Contributor

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.

Copy link
Contributor Author

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.

Copy link
Contributor

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". 😆

Copy link
Contributor Author

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()
}
}
Copy link
Contributor

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?

Copy link
Contributor

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.

Copy link
Contributor Author

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 😩

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants