The review of SE-0469: Task Naming begins now and runs through March 27th, 2025.
Reviews are an important part of the Swift evolution process. All review feedback should be either on this forum thread or, if you would like to keep your feedback private, directly to me as the review manager by email or DM. When contacting the review manager directly, please put "SE-0469" in the subject line.
Trying it out
If you'd like to try this proposal out, you can download a toolchain supporting it for Linux, Windows, or macOS using the main development snapshot from March 9th or later at Swift.org - Download Swift.
What goes into a review?
The goal of the review process is to improve the proposal under review through constructive criticism and, eventually, determine the direction of Swift. When writing your review, here are some questions you might want to answer in your review:
What is your evaluation of the proposal?
Is the problem being addressed significant enough to warrant a change to Swift?
Does this proposal fit well with the feel and direction of Swift?
If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?
How much effort did you put into your review? A glance, a quick reading, or an in-depth study?
More information about the Swift evolution process is available at:
I don't really see a reason to accept StringProtocol; in the end we'd like to return a String anyway from task.name and we won't be able to avoid copying in most cases either.
If someone has good reasons for doing so we could think about it but so far I don't see a reason to do so.
I don't see it in the alternatives considered for async let task naming—what about providing a compiler-builtin @TaskName() macro that can be attached to the async let?
@TaskName("Get profile picture")
async let profileImg = getProfilePicture()
@TaskName("Get header picture")
async let headerImg = getHeaderPicture()
This seems like it's compatible as an additive change in the future, wondering if it's worth mentioning in the future directions / alongside the alternatives considered.
Thanks for mentioning that, seems we didn't list it as an alternative but it did pass our minds at some point.
You're right that that's something that maybe would be possible. We can't do such thing with macros today AFAIK but we could add it to alternatives considered.
For what it's worth if we were to do such thing I'd probably want it to be:
@Task(name: ..., preferredExcutor: ...)
async let ...
etc so we could solve all of the problems that these async let tasks have in one go -- that is, the inability to configure them at all, and having to reach for a task group immediately...
I don't think we can do this in this proposal but it'd be interesting to get a read from the LSG if that could be a future direction that would be considered. The async let = Task(...) is quite problematic, and the @Task could be quite nice.
I'm in favour. I see no reason not to support this functionality.
In terms of back-deployment, would it be possible to make the API available on newer OSes but simply make it a no-op if the runtime support isn't present?
And! I realize it's an implementation detail, but will setting the name of a task cause the runtime to transiently set the current thread's name as tasks move onto/off of them? Presumably that would be necessary for non-Swift-aware debugging tools to be able to report the names of running tasks.
If so: different platforms have different limits on thread name length, with Linux having a particularly short limit of only 15 bytes. Will the runtime make any sort of guarantees here around truncation, UTF-8 preservation, etc.?
I don't think we really do such "make the API be there but do nothing" moves in the standard library tbh. It could be pretty confusing, so I's say no.
As currently proposed it does not. For what it's worth this is part of an effort to make debugging tools aware of these names though.
It would be quite a lot of setting those names since suspensions and task runs can be quite frequent; I'm not sure changing thread names every cycles is really a good way to use the thread name APIs
Having that said, you can always from any code get to the task with UnsafeCurrentTask and query the name, so in Swift code it's always easy. In tools I think we just need to continue making them more aware of Swift.
Another point here is that Task APIs never really interact with threads (one notable API which does is setting a TaskLocal when there is no task... this does actually set a thread local), and threads are really left up to executors. You could make a custom executor that names the thread as it is about to run the task... because in another because we're propsing ths job -> task conversion here, so you could query the name, name the thread, run the job in your executor. I do think this should be left to executor implementations though, and no, ours will not do that.
This seems nice and is great functionality to offer!
A question about expected usage -- would you expect providing a name for every task in a codebase to be considered be a good practice / best practice, or is this primarily a debugging aide to be used more sparingly?
I ask because I imagine it will be tempting for code authors to think of this as a best practice and I can imagine people taking the time to write names for every tasks just because the API is there (I'm imaging a lot of unnecessary reverse DNS task names )
If we don't expect or want this to be a thing people do by default, maybe we pick a different name like debugName: instead.
Good question, it is not intended to name "all" your tasks. It is for some tasks that are "interesting enough" for you to want to see their names in some debugging tools.
Adding a task name also may (currently does) incur a (task local allocator) allocation because the name needs to be copied into the task's storage. This allocation is relatively cheap (does not necessarily result in a malloc, but it may), but yeah you should not go around naming all your tasks.
I'd probably say "name tasks you'll be actively looking for using some tool that inspects the swift runtime", such as swift-inspect or instruments / lldb if and when they'd get support for it. Maybe we can add a sentence of documentation about that.
Initial revisions had called this debugName: actually, and we should have captured that in the alternatives considered I just noticed -- let me add that. The reason "debugName" isn't good, we believe, because
--
This also does not replace GitHub - apple/swift-distributed-tracing: Instrumentation library for Swift server applications which is a way more powerful way to annotate execution and pieces of code. I would still recommend annotating your code with traces if you want deep detailed execution analysis in servers, which is not something you can attach instruments to -- and there you already have a place to name things (span names).
Big +1 on this feature. While it might look simple on the surface it will enable us to improve our debugging and testing experience tremendously.
For me the biggest reason why we should do this is that it allows us to write deterministic tests for our code. Something that has come up in those forums previously here and here. Right now it is impossible to test certain scenarios where we need specific job ordering to hit certain branches of our code.
I have been experimenting with various ways how to do this in the past and I think this proposal is the missing piece. Below is a snippet showing how we could write a deterministic task executor using task names.
DeterministicTestExecutor
import Dispatch
final class DeterministicTestExecutor: TaskExecutor, @unchecked Sendable {
private let queue = DispatchSerialQueue(label: "queue")
private var order = [String]()
private var jobs = [String: UnownedJob]()
init(taskNameOrder: [String]) {
self.order = Array(taskNameOrder.reversed())
}
func enqueue(_ job: consuming ExecutorJob) {
let unownedJob = UnownedJob(job)
guard let taskName = job?.unsafeCurrentTask?.name else {
fatalError("All jobs need to be name for deterministic scheduling")
}
queue.async {
if self.order.last == taskName {
self.order.removeLast()
unownedJob.runSynchronously(on: self.asUnownedTaskExecutor())
while let nextTaskName = self.order.last, let nextJob = self.jobs[taskName] {
self.order.removeLast()
self.jobs.removeValue(forKey: nextTaskName)
nextJob.runSynchronously(on: self.asUnownedTaskExecutor())
}
} else {
self.jobs[taskName] = unownedJob
}
}
}
}
I personally think that we will end up with naming a lot of tasks to make testing and debugging easier. There are so many scenarios where it would have been helpful understanding which one of my child tasks finished or which one got cancellation triggered on them.
I would assume it is due to the possible memory allocation @ktoso mentioned to not incur extra overhead for something that might be just a short running task that you likely never care about the naming for.
I think it is the right tradeoff, most of the time we know the tasks we may be interested in debugging/troubleshooting and will name them (with #function or by hand...).
I agree it’s the right trade-off if we need to allocate. However if we could avoid the allocation and use a static string for this instead that would be amazing. In general having all tasks have names would be great as long as it is cheap.
I guess if we can do a best effort to use a default function name but guarantee no allow will be done, but be able to explicitly set an arbitrary longer name - we could have the best of two worlds.
Basically cut the name so it fits into the small string optimization window (15?) by default.
I don't think it's confusing as long it's documented. That's a great API for debugging and you can debug in a newer OS while support older ones. Without this convenience people will end up creating they own overload anyway.
Maybe not relevant for the proposal at hand, but I often wish for better ways to communicate a task's state/progress to the holder of a Task.
Could we e.g. consider something like these userInfo dicts we see all over Foundation? Or some state with reducers that communicate state between children and parents tasks, similar to SwiftUI's "preferences"? Maybe something akin to Foundation's Progress? Perhaps some more modern "swifty" way of achieving the same goal?
I realize that this is probably well outside the scope for the proposal at hand, but I'd like to understand if this idea has merit, and if it is compatible with this.
As for something like user info… you can do this today with task locals, stuff a mutable but sendable bag of things into a task local and there you go, a way to share things with the whole task tree.
This is interesting. The proposal's Motivation section specifies debugging & profiling tools as the primary use cases for this new API, though Franz's example above shows Task.name used as a pseudo-Identifiable conformance, which one might imagine developers using in tandem with a convention like
The proposal draws a comparison to the label of a dispatch queue, which feels like the right analogy for a task operating as e.g. the consumer of an async sequence, performing periodic operations over a period of time. But when used with a "one-shot" task, like one created to update a piece of @MainActor state like a UI, it's almost like applying a label onto the work item passed to a dispatch queue, rather than the queue itself. And in Franz's example, that more granular unit of identification is used to affect program behavior.
I'd like to see the proposal acknowledge this flavor of usage of the name property, whether that is:
explicit support as an intended use case, or
documented discouragement if this is truly intended as debug-only information.
I can imagine that structured concurrency can’t be built on embedded systems where String type might not be available? Correct? That was the only use I could think of to make it StringProtocol.