Skip to content

Conversation

@crazytonyli
Copy link
Contributor

Description

Here is a recording.

ScreenRecording_09-26-2025.22-53-19_1.MP4

@crazytonyli crazytonyli added this to the 26.4 milestone Sep 26, 2025
<string>spectrum-&apos;22-icon-app-83.5x83.5</string>
<string>spectrum-'22-icon-app-60x60</string>
<string>spectrum-'22-icon-app-76x76</string>
<string>spectrum-'22-icon-app-83.5x83.5</string>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Xcode made these changes. Xcode also made the original changes. Somehow, Xcode decides to revert changes that were made by itself...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What version of Xcode are you on?

@wpmobilebot
Copy link
Contributor

wpmobilebot commented Sep 26, 2025

App Icon📲 You can test the changes from this Pull Request in WordPress by scanning the QR code below to install the corresponding build.
App NameWordPress
ConfigurationRelease-Alpha
Build Number29655
VersionPR #24891
Bundle IDorg.wordpress.alpha
Commit1ce20c0
Installation URL0uf9jjmkolktg
Automatticians: You can use our internal self-serve MC tool to give yourself access to those builds if needed.

@wpmobilebot
Copy link
Contributor

wpmobilebot commented Sep 26, 2025

App Icon📲 You can test the changes from this Pull Request in Jetpack by scanning the QR code below to install the corresponding build.
App NameJetpack
ConfigurationRelease-Alpha
Build Number29655
VersionPR #24891
Bundle IDcom.jetpack.alpha
Commit1ce20c0
Installation URL15cpksdkb4hfo
Automatticians: You can use our internal self-serve MC tool to give yourself access to those builds if needed.

Copy link
Contributor

@jkmassel jkmassel left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm glad we're doing this, but I'm concerned about how many errors we're ignoring.

Any IO error shouldn't be ignored, otherwise we end up with impossible-to-debug issues.

I'm also curious what happens if a user schedules an image for upload then immediately deletes it before it starts uploading (or while it's in progress)

@wpmobilebot wpmobilebot modified the milestones: 26.4, 26.5 Oct 2, 2025
@wpmobilebot
Copy link
Contributor

Version 26.4 has now entered code-freeze, so the milestone of this PR has been updated to 26.5.

Copy link
Contributor

@kean kean left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks pretty neat! LGTM, but disclaimer I'm not familiar with this API.

I left one main comment about actor vs MainActor and I'd also consider monitoring using MediaCoordinator (latest example).

In terms of features, I'm not sure it should be shown for quick operations like uploading a site image or a featured image for a post – it may be too overwhelming, and other apps don't do this. I think it should be reserved only for actually long background operations like uploading large videos or multiple files.

<string>spectrum-&apos;22-icon-app-83.5x83.5</string>
<string>spectrum-'22-icon-app-60x60</string>
<string>spectrum-'22-icon-app-76x76</string>
<string>spectrum-'22-icon-app-83.5x83.5</string>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What version of Xcode are you on?


@available(iOS 26.0, *)
/// Utilize `BGContinuedProcessingTask` to show the uploading media activity.
private actor ConcreteMediaUploadBackgroundTracker: MediaUploadBackgroundTracker {
Copy link
Contributor

@kean kean Oct 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mixing actor isolation with @MainActor functions (updateMessaging, updateResult) adds complexity. I would suggest to synchronize ConcreteMediaUploadBackgroundTracker itself on the main thread as it doesn't perform any work and potentially creates more CPU overhead by adding context switches and by re-reading stuff from the Core Data. MediaCoordinator, PostMediaUploadsViewModel, etc are also all synchronized on main also for simplicity.

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 would suggest to synchronize ConcreteMediaUploadBackgroundTracker itself on the main thread

I'd prefer not to use the main thread since the Background Task API does not require it. I used the @MainActor on those functions to simplify Core Data query code. But I can have a look to see if it's possible to synchronize the Core Data calls.

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 need to stick with the main context because of how the media uploading is implemented. But I have removed the @MainActor from the function declaration, and moved it to the limited scopes of the Core Data queries.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In that case, would it be better to synchronize ConcreteMediaUploadBackgroundTracker on the main thread? There are disadvantages to making it an actor. This relatively small class currently requires 13 await calls that could all be eliminated.

private var state: BGTaskState = .idle

private init?() {
let taskId = (Bundle.main.infoDictionary?["BGTaskSchedulerPermittedIdentifiers"] as? [String])?.first {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(nit) I'd suggest making init non-optional to remove the need for the code using this class to deal with the optional.

}

let success = mediaItems.allSatisfy { $0.remoteStatus == .sync }
await setTaskCompleted(success: success)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(not) this is the kind of complexity – one function with three concurrent code sections – I mentioned earlier that could be easily eliminated by synchronizing the class on main. You may end up code sections from updateMessaging and updateResult being intertwined in any order, so it's hard to reason about.

@crazytonyli
Copy link
Contributor Author

I'm concerned about how many errors we're ignoring.

@jkmassel Do you mean the try? statements on the Core Data calls? They are actually intentional. Those statements should only throw errors when the media object is deleted. That's related to your comments later about deleting uploaded images. If the Media object is deleted, we don't need to count them in the progress/status updates.

I didn't add code to observe Core Data changes to immediately handle deleting uploading images, because the Core Data notifications get fired pretty frequently. That's one issue I noticed in the UI, where the uploading status is not updated immediately when deleting uploading images.

@crazytonyli crazytonyli requested review from jkmassel and kean October 6, 2025 01:03
Copy link
Contributor

@kean kean left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved, although I wasn't able to verify if there aren't any logic race conditions in the coordinator. I strongly suggest simply putting it on the main actor, as making it a standalone actor doesn't seem to achieve anything but adds a significant amount of complexity. I opened a prototype PR just to show how much simpler it is if it's on main – it eliminates 12 await s: #24943.

private var coreDataChangesObserver: NSObjectProtocol?

private init() {
let taskId = Bundle.main.bundleIdentifier! + ".mediaUpload"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's remove the face unwrapping just in case.

@crazytonyli
Copy link
Contributor Author

I strongly suggest simply putting it on the main actor

@kean I had a go myself and noticed that it's very easy to make the mistake of calling MainActor bound functions from background threads. Take the taskCreated function as an example, it should be running in the MainActor since the class is marked as @MainActor. But you can call it without wrapping it in a Task, too. The compiler does not complain about it, but at runtime, it crashes. This kind of error can probably be caught at compile time by complete strict concurrency checks, which is not set in the app target.

I spent quite a while trying to avoid that issue: declaring the type as @MainActor class, but ensuring its functions are only called from the main thread. I couldn't make it work.

I opened a prototype PR just to show how much simpler it is if it's on main – it eliminates 12 awaits.

I agree that we should avoid jumping between actors if we can, especially in this particular case, where we have to use the MainActor frequently. However, fewer awaits is not necessarily better. Like the example I used above, we'd want the await keyword to be there when calling taskCreated, otherwise the function won't be executed from the main thread.

@kean
Copy link
Contributor

kean commented Nov 4, 2025

but ensuring its functions are only called from the main thread. I couldn't make it work.

If there is some pre-concurrency code, which I assume BGTaskScheduler, it'll might need to use Task { @MainActor. It's an exception.

very easy to make the mistake of calling MainActor bound functions from background threads

It is what MainActor is generally for. There may be some exceptions with pre-concurrency code, but these are just exceptions. I don't see how it would translate into "therefore, we should never use MainActor for synchronization"? It enforces isolation extremely well.

However, fewer awaits is not necessarily better

It's easier to follow code that has less concurrency. If it's possible to eliminate 12 async calls in a ~100 lines of code, it's a big difference. If I needed to debug and fix something in ConcreteMediaUploadBackgroundTaskScheduler, I would probably start by simplifying it – it is hard to follow in the current version.

@crazytonyli
Copy link
Contributor Author

Maybe using a practical example can help illustrate my concerns.

Let's use this one single happy path as the test method: Upload an image from the Media screen, the OS shows an "Uploading Media" banner, updates the progress, and finally dismisses the banner once uploaded.

Obviously, the test works on this PR branch.

I have refactored the code to make everything run on the main thread in the media-upload-using-bgtask-mainactor branch. The code is very similar to your POC code in #24943. Your PR does not work, so I pushed my own changes.

The test works on the media-upload-using-bgtask-mainactor branch too.

If you check out the diff, you'll see there is no Task { ... } and await calls. Everything is sync-ed and runs on the main thread, which is, again, just like your POC code. But if you remove two places that call DispatchQueue.main, and perform the test again, the app crashes due to Core Data concurrency issues. BTW, this is one of the reasons that your POC code does not work.

My concern is that Xcode does not warn us about removing DispatchQueue.main code changes, even though we have declared @MainActor, and the functions are supposed to be running on the main thread. That means, we have to make the conscious effort of writing threadsafe code: calling the MainActor-bound functions from the main thread (using DispatchQueue.main, wrapping in Task { ... }, etc), which is no different than before the "Swift structured concurrency" era.

That brings us to this point: if you reset your working copy to the media-upload-using-bgtask-mainactor branch, and remove the @MainActor declaration, the test continues to work. The @MainActor declaration does almost nothing to the compilation time behaviour and runtime behaviour. The fact that the code is thread-safe is due to the code being carefully written to call certain functions on the main thread, not because the class is declared as @MainActor.

If we have the complete strict concurrency checks turned on in the app target, it's a different story. I'd expect the compiler to warn us about the code changes of removing DispatchQueue.main. And, it'd be much safer to use @MainActor then.

@crazytonyli
Copy link
Contributor Author

It's easier to follow code that has less concurrency. If it's possible to eliminate 12 async calls in a ~100 lines of code, it's a big difference.

Yes, I agree. Readability is important. I'm happy to push the changes in media-upload-using-bgtask-mainactor onto this branch, and make this new class do things the old, simple, and "boring" way, and stop using Swift actor, async, etc. The downside is, of course, we have to be careful about calling functions from the expected thread.

@crazytonyli crazytonyli force-pushed the media-upload-using-bgtask branch from d072677 to 153eb2b Compare November 4, 2025 22:44
@crazytonyli
Copy link
Contributor Author

In terms of features, I'm not sure it should be shown for quick operations like uploading a site image or a featured image for a post – it may be too overwhelming

@kean @jkmassel Regarding this, I feel like it's too much to show the banner immediately after selecting images to upload, too. In the latest commit, I have changed to only show the banner when the app goes to the background. That means we can get extra background time for the uploads, at the same time, we don't confuse the user with the new banner when the app is active. Let me know what you think.

@kean
Copy link
Contributor

kean commented Nov 4, 2025

expect the compiler to warn us about the code changes of removing DispatchQueue.main.

Sure, it is designed to work with Swift Concurrency. If you have legacy APIs with closures that do not enforce actor isolation, you will be able to call syncronous methods on a MainActor-isolated class without warnings. You have to check it yourself and add runtime checks. The reason actor works is because every method on it is async, so you have to have an async context.

I suggested (as a nit and after approval) to isolated it on main mainly to reduce the number of async calls to make the code easier to follow, and not necessarily for the compile-time checks. I understand the appeal of actors that they guarantee isolation, but, to me, it doesn't seem like this advantage is bigger than the disadvantages of making so much of the code concurrent and harder to follow. It's pretty easy to ensure the few places where DispatchQueue.main are necessary are correct.

he old, simple, and "boring" way,

Nothing beats the good old "just synchronize everything on main thread" 😃

Btw, not so well-known fact about actor: it does not guarantee the order in which async tasks schedule on it are executed. So it's not a one-to-one replacement for a serial GCD queue. I don't know if it's necessarily a problem for ConcreteMediaUploadBackgroundTaskScheduler, but it adds fuel to the fire by making it even harder to reason about async methods in actors. They do guarantee isolation. They do no guarantee that the tasks will execute in the order they were "scheduled". You end up with progress, status, context changes updates all being executed out of the order in which they arrived.

@crazytonyli
Copy link
Contributor Author

@kean I have merged the media-upload-using-bgtask-mainactor branch into this PR.

@sonarqubecloud
Copy link

sonarqubecloud bot commented Nov 5, 2025

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.

5 participants