SE-0469: Task Naming

I think maybe the rest of the sentence got lost in editing. Would be curious to hear the reason!

2 Likes

Ah, I was doing too many things at once again and lost that sentence :sweat_smile:

The reason "debugName" isn't good, we believe, because it implies like something "only available in debug configurations" which is very untrue and misleading. This is very much available in any build configuration and calling it "debug" would make it seem like something only used for debugging -- but it's not, it can be equally useful just for logging or instrumentation purposes. So we'd like to steer away from calling it a "debug" thing. It's just a name, and it's up to developers what they do with it. Tools will certainly print it when inspecting a task as well.

2 Likes

I'm not totally sure about this argument -- CustomDebugStringConvertible's debugDescription also provides additional context for debugging and logging purposes. I don't think the "debug" here is taken to imply that it's only available in debug builds, and it doesn't seem surprising or unexpected for the debugDescription used for logging / instrumentation purposes or accessed by other tools.

1 Like

Though with debugDescription the reason that exists with a prefix is to differentiate from description -- in tasks there is no "name" and "debugName", it's just name.

Are we talking about a copy of Strings 2 word inline storage or the whole backing storage? If it is the later, why do we need to do that?

1 Like

I'd rather not dive too deep into implementation details in an SE review to be honest, but since I did mention it I'll give a quick summary.

Today the Swift runtime has no good way to interact with a Swift String to "retain it" (that's not a thing basically). Since a string may be small, bridged, or the usual "big" string. We'll have to reimplement all this logic in the runtime to be able to prevent the copy/allocation. Today to get the API in shape in time the implementation does copy the entire string.

Optimizing this copy is possible, but will take some more time. Note that this can be done in any release, while landing new APIs is more restricted when it can happen, so I wanted to get the APIs in place first. For a small string we do need to copy it anyway, for larger strings we could refer to the String's storage I think...

Having that said, this is a performance optimizations that we can do at any point in time, and the review I think we should focus on the API and functionality in general.

6 Likes

In the debugger side, we've seen a lot of interest in being able to surface the name of the task to the user.

To accomplish this, the debugger needs to be able to access the name of the Task through simple memory reads (i.e. it cannot evaluate any expressions or execute swift code). For example, today the debugger surfaces the Task ID by reading the memory at a fixed offset in the Job data structure.

Under the current proposal, would it be possible to both detect whether a name is present, and if so find the name through simple memory reads in an ABI guaranteed way?

1 Like

Runtime details follow:

We don't strictly guarantee the location in ABI, however yes it is nicely accessible because it is the "initial task record" that a task creates during the tasks creation. AFAIK this is sufficient for tools / lldb integration, since the runtime may have version differences anyway. Discussing how to signal and test changes has been a topic that has come up in the past, but regardless of that, this is pretty "stable" as far as task records go and you could get to it without runtime calls.

The initial task records are immediately after the task storage. If there is an executor preference as well, it's after it, but otherwise it's the first record (see here). You'd be able to calculate the offset where the name is fairly easily by checking flags (e.g. hasInitialTaskNameRecord) and adjusting the offsets from the task pointer.

I'd love lldb, crash reports etc. to be able to include those names, so if there's something we should collaborate on here you know where to reach me :slight_smile:

1 Like

What is your evaluation of the proposal?
+1.

Is the problem being addressed significant enough to warrant a change to Swift?
Yes.

Does this proposal fit well with the feel and direction of Swift?
Yes.

If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?
A DispatchQueue can have a name, and it's useful.

How much effort did you put into your review? A glance, a quick reading, or an in-depth study?
A quick reading.

I'm just curious to know if it would be beneficial to add support for naming a TaskGroup as well? Something a long the line of withTaskGroup(name: String?, of: .., body: ...) async -> ... or would that be totally out of scope for this proposal?

3 Likes

Iโ€™m thinking no because itโ€™s the tasks you should be naming and a task group isnโ€™t really a resource that shows up much either in traces or debugging, itโ€™s more about the tasks it creates I think :thinking: I donโ€™t think we have any existing tools that โ€œprint task groupsโ€ we just print tasks etc hm. Maybe itโ€™s a future direction if we discover those might after all be useful?

2 Likes

I can easily imagine cases where Task name is created from static string, like #function. But static string doesn't conform to StringProtocol, so we must convert it to String anyway.
There is another open question about embedded swift, where some String alternative is needed, like ascii-string.
While there are reasons use some abstraction over String, the StringProtocol itself seems to be not suitable for now.

Thanks for your swift reply ;-) I know that NSOperation has a name property and kind of a parent/child relationship (via the dependency methods). Maybe worth exploring as the TaskGroup is kind of a parent/child relationship. In Instruments it would also be nice to be able to group tasks per TaskGroup.

2 Likes

Agree on the statement, but I am not sure if the usage of taskGroup naming is common.

It is an advantage when you are finding errors between multiple task groups, when debugging concurrency-related errors. But from my usage, I mainly debugging on tasks instead of their parent, TaskGroup is just an async group-caller for me.

I agree what you and ktoso said, it could be worth discovering, but a more concrete example maybe needed.

This proposal is a big +1 for me, as it can uplift the efficiency of debugging tasks, and I am expecting there might be a heavy usage of this feature (like adding to many different tasks :rofl:)

It is also a big +1 from my side - I just think it would be nice to raise the bar :slight_smile:

1 Like

Sorry for the late-breaking feedback here. In reading this proposal I also questioned why it's useful to make the name optional, instead of just using the empty string.

On 64-bit platforms, if anyone is concerned about performance consequences of allocating, Swift has a capacious small string optimization of up to 15 UTF8 code units.

On all platforms, even 32-bit where the SSO is way smaller (or not even implemented in some platforms), the empty string should always be free. If we're needlessly micro-optimizing, it ought even to be cheaper in some uses since there's no need to first check for optionality and then for emptiness.

If you create a String from a string literal, that literal ought to make it into the text section and the String instance should just hold a pointer to it. It's only dynamically generated strings beyond the SSO size that will allocate.

6 Likes

Let me make the case for this. It'll be a real pain to have to conditionalize both setting and retrieving task names on what OS version you're running on. Given this is primarily a debug tool, I think the risks from "I set it, but now I'm getting the empty string back because I'm running on an older OS" are minimal compared to the big ergonomic win of not having to add an if #available check every single time you create a task you want to name.

15 Likes

I really like @grynspan's suggestion here to always have ability to name the task on creation, but drop the name on older runtimes. Creating a new Task usually means putting a bunch of code in the closure:

Task {
  // code code code
}

If we have to put a if #available around each task creation, we have to duplicate the closure code:

if #available(...) {
  Task(name: "Task name") {
    // code code code
  }
} else {
  Task {
    // SAME code code code
  }
}

Yes, someone could write their own Task initializer that does this dance, but it seems unnecessary to force them to do that. The name property would still have next-version-of-Swift availability, so you wouldn't be able to observe the lack of names from Swift code. Only in developer tools running against an older platform would you note the lack of names, but that seems like a much smaller inconvenience than the above. The documentation for the initializer can call out the runtime dependency.

  • Doug
16 Likes

Color me positively surprised but if we're okey with that then absolutely fine by me!

We can easily adjust the new methods to have lower availability and just pass the name if-we-can :slight_smile:

3 Likes

Thinking about this more, another very reasonable option would be making setting it back deploy, but not getting it. That way, setting the name doesn't need conditionalizing, whereas getting it back out, which is where the "huh, I set it but it's blank" confusion could kick in, might deserve an availability guard, defining away that problem.

4 Likes