How is BGContinuedProcessingTask intended to be used?

Hello,

I'm trying to adopt the new BGContinuedProcessingTask API, but I'm having a little trouble imagining how the API authors intended it be used. I saw the WWDC talk, but it lacked higher-level details about how to integrate this API, and I can't find a sample project.

I notice that we can list wildcard background task identifiers in our Info.plist files now, and it appears this is to be used with continued tasks - a user might start one video encoding, then while it is ongoing, enqueue another one from the same app, and these tasks would have identifiers such as "MyApp.VideoEncoding.ABCD" and "MyApp.VideoEncoding.EFGH" to distinguish them.

When it comes to implementing this, is the expectation that we:

a) Register a single handler for the wildcard pattern, which then figures out how to fulfil each request from the identifier of the passed-in task instance?

Or

b) Register a unique handler for each instance of the wildcard pattern? Since you can't unregister handlers, any resources captured by the handler would be leaked, so you'd need to make sure you only register immediately before submission - in other words register + submit should always be called as a pair.

Of course, I'd like to design my application to use this API as the authors intended it be used, but I'm just not entirely sure what that is. When I try to register a single handler for a wildcard pattern, the system rejects it at runtime (while allowing registrations for each instance of the pattern, indicating that at least my Info.plist is configured correctly). That points towards option B.

If it is option B, it's potentially worth calling that out in documentation - or even better, perhaps introduce a new call just for BGContinuedProcessingTask instead of the separate register + submit calls?

Thanks for your insight.

K


Aside: Also, it would be really nice if the handler closure would be async. Currently if you need to await on something, you need to launch an unstructured Task, but that causes issues since BGContinuedProcessingTask is not Sendable, so you can't pass it in to that Task to do things like update the title or mark the BGTask as complete.

Answered by DTS Engineer in 854482022

I'm trying to adopt the new BGContinuedProcessingTask API, but I'm having a little trouble imagining how the API authors intended it to be used. I saw the WWDC talk, but it lacked higher-level details about how to integrate this API, and I can't find a sample project.

So, as full disclosure, I helped prepare talk, and part of what we really struggled with here was the balance between presenting a simple, clear overview of the API and the FULL details of what was or was not possible. In general, the presentation tended toward simplicity, but that does mean that it's possible to overlook or misunderstand what's actually possible.

Let me start with what's probably the biggest misunderstanding, which leads to this issue:

Since you can't unregister handlers, any resources captured by the handler would be leaked.

This isn't really an issue as there's no reason your handler has to do any real "work" and, in practice, most apps probably SHOULDN'T actually do their real work there. Just like all of our other BackgroundTask types, all the handler does is tell your app that it can "start working". Returning from that handler does NOT mean that your task is fact "done", which is why you need to explicitly tell us using setTaskCompleted. I'm aware that the code snippet in the "Run the continuous background task" shows that work occurring inside the block the snippet is written that way because it was the simplest way to show the code flow, not because the work actually has to occur in that block.

I notice that we can list wildcard background task identifiers in our Info.plist files now, and it appears this is to be used with continued tasks - a user might start one video encoding, then while it is ongoing, enqueue another one from the same app, and these tasks would have identifiers such as "MyApp.VideoEncoding.ABCD" and "MyApp.VideoEncoding.EFGH" to distinguish them.

First off, keep in mind that there's another option here which is to just have one "Encoding Video" task, which you then "add" additional work into using addChild(_:withPendingUnitCount:). This can be particularly important for operations which involve a mix of work types with different scheduling needs. For example, if an app is going to do something like:

  1. Download a (small) resource.
  2. Encode video based on information from #1.
  3. Upload (small) final result.

...then you'd generally want #1 & #3 to happen for "all" tasks immediately/simultaneously (to make sure you’re using the network efficiently), while #2 uses its own queueing to limit overall load. Putting that in concrete terms, if 8 jobs are submitted at the same time, you'd want to process them like this:

  • All 8 jobs perform #1.
  • Encoding starts on the first "X videos".
  • As encoding completes on one job, the next one immediately starts.
  • As each encoding completes, #3 starts independently of the encoding process.

You can't implement that approach if you use a separate BGContinuedProcessingTask for each job, but you can if you use a single BGContinuedProcessingTask with child progress.

Shifting to here:

...enqueue another one from the same app, and these tasks would have identifiers such as "MyApp.VideoEncoding.ABCD" and "MyApp.VideoEncoding.EFGH" to distinguish them.

Correct, however, what I would actually emphasize here is that, particularly for long-running foreground apps, this is fundamentally an interface issue, not a work scheduling issue*. Putting that in concrete terms, an app doing extended encoding operations on a small number of large videos would probably want to create separate tasks for each operation, while an app doing a large number of much shorter operations would be better off using a single task.

*I'm confirming this with the engineering team, but I think that for large CPU/GPU-bound operations, it's likely that the system will allow your app to start more tasks than you'd actually want to execute simultaneously. In other words, the fact that the system will let you start 10 encode operations doesn't mean that's what you should actually "do".

However, in both cases, the choice largely depends on what looks/feels "right" to the overall user experience, not any particular technical requirement.

When I try to register a single handler for a wildcard pattern, the system rejects it at runtime (while allowing registrations for each instance of the pattern, indicating that at least my Info.plist is configured correctly). That points towards option B.

Yes, "B" is the primary usage pattern, though I can also imagine situations where a more "static" pattern is used.

If it is option B, it's potentially worth calling that out in documentation - or even better, perhaps introduce a new call just for BGContinuedProcessingTask instead of the separate register + submit calls?

I agree. If you haven't already, please file a bug on this and then post the bug number back here.

Aside: Also, it would be really nice if the handler closure would be async. Currently, if you need to await on something, you need to launch an unstructured Task, but that causes issues since BGContinuedProcessingTask is not Sendable, so you can't pass it into that Task to do things like update the title or mark the BGTask as complete.

I don't think this really works for the "general" register case. The BGAppRefreshTask block will often be called multiple times during a given process run, while BGProcessingTask will often not be run at all, neither of which really “fits" the async deferred work model. However, it's certainly worth considering if we add an API specific to BGContinuedProcessingTask.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

I'm trying to adopt the new BGContinuedProcessingTask API, but I'm having a little trouble imagining how the API authors intended it to be used. I saw the WWDC talk, but it lacked higher-level details about how to integrate this API, and I can't find a sample project.

So, as full disclosure, I helped prepare talk, and part of what we really struggled with here was the balance between presenting a simple, clear overview of the API and the FULL details of what was or was not possible. In general, the presentation tended toward simplicity, but that does mean that it's possible to overlook or misunderstand what's actually possible.

Let me start with what's probably the biggest misunderstanding, which leads to this issue:

Since you can't unregister handlers, any resources captured by the handler would be leaked.

This isn't really an issue as there's no reason your handler has to do any real "work" and, in practice, most apps probably SHOULDN'T actually do their real work there. Just like all of our other BackgroundTask types, all the handler does is tell your app that it can "start working". Returning from that handler does NOT mean that your task is fact "done", which is why you need to explicitly tell us using setTaskCompleted. I'm aware that the code snippet in the "Run the continuous background task" shows that work occurring inside the block the snippet is written that way because it was the simplest way to show the code flow, not because the work actually has to occur in that block.

I notice that we can list wildcard background task identifiers in our Info.plist files now, and it appears this is to be used with continued tasks - a user might start one video encoding, then while it is ongoing, enqueue another one from the same app, and these tasks would have identifiers such as "MyApp.VideoEncoding.ABCD" and "MyApp.VideoEncoding.EFGH" to distinguish them.

First off, keep in mind that there's another option here which is to just have one "Encoding Video" task, which you then "add" additional work into using addChild(_:withPendingUnitCount:). This can be particularly important for operations which involve a mix of work types with different scheduling needs. For example, if an app is going to do something like:

  1. Download a (small) resource.
  2. Encode video based on information from #1.
  3. Upload (small) final result.

...then you'd generally want #1 & #3 to happen for "all" tasks immediately/simultaneously (to make sure you’re using the network efficiently), while #2 uses its own queueing to limit overall load. Putting that in concrete terms, if 8 jobs are submitted at the same time, you'd want to process them like this:

  • All 8 jobs perform #1.
  • Encoding starts on the first "X videos".
  • As encoding completes on one job, the next one immediately starts.
  • As each encoding completes, #3 starts independently of the encoding process.

You can't implement that approach if you use a separate BGContinuedProcessingTask for each job, but you can if you use a single BGContinuedProcessingTask with child progress.

Shifting to here:

...enqueue another one from the same app, and these tasks would have identifiers such as "MyApp.VideoEncoding.ABCD" and "MyApp.VideoEncoding.EFGH" to distinguish them.

Correct, however, what I would actually emphasize here is that, particularly for long-running foreground apps, this is fundamentally an interface issue, not a work scheduling issue*. Putting that in concrete terms, an app doing extended encoding operations on a small number of large videos would probably want to create separate tasks for each operation, while an app doing a large number of much shorter operations would be better off using a single task.

*I'm confirming this with the engineering team, but I think that for large CPU/GPU-bound operations, it's likely that the system will allow your app to start more tasks than you'd actually want to execute simultaneously. In other words, the fact that the system will let you start 10 encode operations doesn't mean that's what you should actually "do".

However, in both cases, the choice largely depends on what looks/feels "right" to the overall user experience, not any particular technical requirement.

When I try to register a single handler for a wildcard pattern, the system rejects it at runtime (while allowing registrations for each instance of the pattern, indicating that at least my Info.plist is configured correctly). That points towards option B.

Yes, "B" is the primary usage pattern, though I can also imagine situations where a more "static" pattern is used.

If it is option B, it's potentially worth calling that out in documentation - or even better, perhaps introduce a new call just for BGContinuedProcessingTask instead of the separate register + submit calls?

I agree. If you haven't already, please file a bug on this and then post the bug number back here.

Aside: Also, it would be really nice if the handler closure would be async. Currently, if you need to await on something, you need to launch an unstructured Task, but that causes issues since BGContinuedProcessingTask is not Sendable, so you can't pass it into that Task to do things like update the title or mark the BGTask as complete.

I don't think this really works for the "general" register case. The BGAppRefreshTask block will often be called multiple times during a given process run, while BGProcessingTask will often not be run at all, neither of which really “fits" the async deferred work model. However, it's certainly worth considering if we add an API specific to BGContinuedProcessingTask.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

Hello,

I'm posting in this thread rather than starting a new one so all the information is consolidated... I also have some doubts about BGContinuedProcessingTask...

My main problem is with the Progress object. I've observed that if I add children Progress objects to the object provided by the task, the popup that shows up with the task on the device doesn't update the progress interface until the Progress object has completed at least one unit. It doesn't seem to be using the fractionCompleted property and instead it seems to use the number of completed units.

This is a problem because if I have a group of tasks to complete on one BGContinuedProcessingTask, the interface won't update gradually as the tasks are completed, but only in steps as each task completes and until the first one does it shows a spinning circle. Is this intended? or could it be a bug?

Also, as the original poster said, the actual BGContinuedProcessingTask object is not sendable, so when I start an async Task there's no way to pass this object. It seems the only way to update the task is from the main block of code you register... And the only information that can be passed to this code is through the Progress object. Is that what's intended?

Anyway, it's nice to get some information about this.It's a great new feature. Thanks in advance

This is a problem because if I have a group of tasks to complete on one BGContinuedProcessingTask, the interface won't update gradually as the tasks are completed, but only in steps as each task completes and until the first one does it shows a spinning circle. Is this intended? Or could it be a bug?

I think this is a case of "neither" and "both". That is, I don't think what you’re seeing is a design choice, and not due to any accident or mistake. However, I also think it's very possible that this is something that could be tweaked and improved on. Two broad points I'd make here:

First off, any time you feel like something isn't working the way you want/expect, it's always worth filing a bug on, particularly when you're dealing with a new API. I won't promise that anything will change— like any large-scale engineering organization, properly prioritizing work is a very complicated task and that sometimes means that things don't change or get fixed when they "should". However, any change that happens ALWAYS starts with a bug report, particularly developer bugs. If you do file a bug, please post the bug number back here and I'll make sure it gets to the right place.

Secondly, I think it's important to understand that BGContinuedProcessingTask has two different roles, neither of which QUITE matches the API it presents:

  1. Give an app that enters the background time to complete "work" over an extended time window.

  2. Let the app provide feedback to the user and system about the progress of the app’s "work" so that both are aware that "something" is going on here.

However, notice what's NOT on the list, which is "perform actual work". That's because how your app actually performs its work isn't something the API actually cares about. What's critical about both of these points together is that choosing the "correct" way to interact with BGContinuedProcessingTask is ultimately about what looks and feels best in the system UI, NOT the specifics of the work your app is actually doing.

That leads to here:

Also, as the original poster said, the actual BGContinuedProcessingTask object is not sendable, so when I start an async Task there's no way to pass this object. It seems the only way to update the task is from the main block of code you register... And the only information that can be passed to this code is through the Progress object. Is that what's intended?

I think it's certainly a reasonable request to ask that BGContinuedProcessingTask be made sendable; however, I also think that shouldn't really matter in the larger context of what this API is "for". BGContinuedProcessingTask is specifically about completing “long-term" work for the user, not small discrete tasks— work that takes "minutes", not "seconds". As the most straightforward example, there’s no reason to use BGContinuedProcessingTask for anything that takes less than ~30s, as you could just use beginBackgroundTask(...) to keep your app awake instead.

In my experience, larger work like that generally doesn't "neatly" fit with an individual task architecture, so the natural object to own your BGContinuedProcessingTask is the same object that's managing your tasks in the first place.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

First. Thanks for the detailed response. I've filed a bug about the progress reporting ( FB19925898 )

About the other matter, maybe I misunderstood the purpose of BGContinuedProcessingTask, but I think the fact that the user is the one starting the task is important... I don't see it as a way to finish some work in the background that was started in the foreground in a "normal" way, because when the work is started it has to be started from the BGContinuedProcessingTask itself.

The actual work is done by whatever part of the code has to do it, yes, but it is started from within the block of code that is registered when creating the task. I think it makes sense to have a two way communication with the code doing the work so the UI can be updated in whatever way it is necessary...

In my experience, larger work like that generally doesn't "neatly" fit with an individual task architecture, so the natural object to own your BGContinuedProcessingTask is the same object that's managing your tasks in the first place.

In my case I first tried this approach but the object doing the work is an Actor and I couldn't find a way to start a BGContinuedProcessingTask from inside the actor. I had to force the actor to use a specific DispatchQueue but then when the task was active it blocked the actor.

In the end I start the task from outside the actor (in a MainActor class) and then start the work calling an asynchronous function on the actor object.

Thanks again!

  • Jorge

About the other matter, maybe I misunderstood the purpose of BGContinuedProcessingTask, but I think the fact that the user is the one starting the task is important... I don't see it as a way to finish some work in the background that was started in the foreground in a "normal" way, because when the work is started it has to be started from the BGContinuedProcessingTask itself.

So, one thing to keep in mind here is that the normal behavior of "BGContinuedProcessingTask" is that it's start time is NOT tied to your app lifecycle. In other words, when you call "submit(_:)" what will normally happen is that the task will immediately be run. The only reason it wouldn't run is that it's been deferred to other tasks being active.

That leads to this point:

because when the work is started it has to be started from the BGContinuedProcessingTask itself.

We didn't cover this in the WWDC session but one of the "oddities" of this API is that there are lots of cases where an app should just "do the work", even if the BGContinuedProcessingTask doesn't start. Taking one example from our own documentation:

"- Applying visual filters (HDR, etc) or compressing images for social media posts"

Assuming your app is doing that as part of an operation the user has requested ("process images and post...") nothing requires your app to delay that work waiting for the processing task to start. You're the foreground app and your job is to do what the user told you to do.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

How is BGContinuedProcessingTask intended to be used?
 
 
Q