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

tracing: add tokio::task::Builder #3881

Merged
merged 7 commits into from
Jun 29, 2021
Merged

Conversation

jbr
Copy link
Contributor

@jbr jbr commented Jun 21, 2021

Motivation

Task name annotations are helpful in order to better understand what is happening in an async application

Solution

This PR adds a cfg_trace-gated unstable builder tokio::task::Builder, which allows authors to annotate a task at runtime with an appropriate name. It includes task.name instrumentation for Builder::spawn, Builder::spawn_local, and Builder::spawn_blocking

Copy link
Member

@carllerche carllerche left a comment

Choose a reason for hiding this comment

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

Looks good to me. Some basic tests using the Builder would be nice. We don't have any testing infrastructure to validate tracing spaces yet, so that doesn't matter so much. However, we could duplicate some of the existing "spawn" tests to go via the builder.

///
/// See [`task::spawn_local`][crate::task::spawn_local]
/// for more details
#[cfg_attr(tokio_track_caller, track_caller)]
Copy link
Member

Choose a reason for hiding this comment

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

For the caller location to be tracked, do we need to call Location::caller() in the method proper or is the location passed to inner calls?

Copy link
Contributor Author

@jbr jbr Jun 22, 2021

Choose a reason for hiding this comment

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

My read of this is that it just makes the annotated function "transparent" to the walked Location, so inner calls don't report this function, but its caller. I'm new to this interface though — did I misunderstand it?

Copy link
Member

Choose a reason for hiding this comment

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

Not sure what you mean exactly, but here: https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=9fe91c5452a8add4f3b9a45e688ec3c7

Without #[track_caller] on fn two(), Location::caller() isn't what we want. If fn two() does have #[track_caller] then it is passed through one() and reports the line within fn main().

Copy link
Member

Choose a reason for hiding this comment

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

So it looks like you are correct.

@carllerche
Copy link
Member

Once this lands, it would be nice to see it piped through the console. I think it would be fine to switch the console git repo to point to the latest tokio git.

@jbr
Copy link
Contributor Author

jbr commented Jun 22, 2021

Currently the console doesn't do anything special with task.name, but I have some locally stashed changes that adds a column on the task view

In general, I wasn't quite sure how to test this. Does CI run tracing tests? Would I register a subscriber, spawn a task, and then check that the right task.name was registered?

Another note: I wasn't sure how to handle tracing spawn_blocking, since task::spawn_blocking hasn't been instrumented yet, but it didn't seem strictly necessary for this PR. So as of this PR: Builder::new().spawn_blocking(||) is instrumented but task::spawn_blocking(||) isn't

@hawkw
Copy link
Member

hawkw commented Jun 22, 2021

Another note: I wasn't sure how to handle tracing spawn_blocking, since task::spawn_blocking hasn't been instrumented yet, but it didn't seem strictly necessary for this PR. So as of this PR: Builder::new().spawn_blocking(||) is instrumented but task::spawn_blocking(||) isn't

spawn_blocking is instrumented on the Handle::spawn_blocking function, which task::spawn_blocking calls internally:

#[cfg(all(tokio_unstable, feature = "tracing"))]
let fut = {
use tracing::Instrument;
#[cfg(tokio_track_caller)]
let location = std::panic::Location::caller();
#[cfg(tokio_track_caller)]
let span = tracing::trace_span!(
target: "tokio::task",
"task",
kind = %"blocking",
function = %std::any::type_name::<F>(),
spawn.location = %format_args!("{}:{}:{}", location.file(), location.line(), location.column()),
);
#[cfg(not(tokio_track_caller))]
let span = tracing::trace_span!(
target: "tokio::task",
"task",
kind = %"blocking",
function = %std::any::type_name::<F>(),
);
fut.instrument(span)
};

So, AFAICT, I think using the builder will currently wrap spawn_blocking calls with a duplicate span, since it calls runtime::spawn_blocking, which calls Handle::spawn_blocking, which also adds a span. I think we need to fix that by adding a spawn_blocking_inner and threading the task name through to that, just like we do for the other spawn functions, rather than constructing the span in Builder::spawn_blocking.

@hawkw
Copy link
Member

hawkw commented Jun 22, 2021

In general, I wasn't quite sure how to test this. Does CI run tracing tests? Would I register a subscriber, spawn a task, and then check that the right task.name was registered?

I wouldn't worry too much about writing tests for this; you're right that we don't currently have any way of writing tests for trace output, and I'm not sure if it's really worth spending a lot of time on. Instead, I would just run some of the examples with RUSTFLAGS="--cfg tokio_unstable" and tracing_subscriber::fmt added, and confirm manually that the logged traces look correct.

Copy link
Member

@hawkw hawkw left a comment

Choose a reason for hiding this comment

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

overall, this definitely looks like the right direction to go in --- i think this is close to ready! the main things that i noticed are

  1. i think using the builder's spawn_blocking will currently produce duplicate spans --- i left a comment explaining that in more detail
  2. it would be nice to have a way to spawn named tasks using a Handle. i left a comment on that as well.

tokio/src/task/builder.rs Outdated Show resolved Hide resolved
tokio/src/task/builder.rs Outdated Show resolved Hide resolved
tokio/src/task/builder.rs Outdated Show resolved Hide resolved
tokio/src/task/builder.rs Outdated Show resolved Hide resolved
tokio/src/task/builder.rs Outdated Show resolved Hide resolved
tokio/src/task/builder.rs Show resolved Hide resolved
tokio/src/task/builder.rs Outdated Show resolved Hide resolved
tokio/src/task/builder.rs Outdated Show resolved Hide resolved
@jbr jbr marked this pull request as ready for review June 24, 2021 23:42
@jbr jbr changed the title initial sketch of tokio::task::Builder tracing: add tokio::task::Builder Jun 24, 2021
function = %std::any::type_name::<F>(),
);
fut.instrument(span)
};

#[cfg(not(all(tokio_unstable, feature = "tracing")))]
let _ = name;
Copy link
Member

Choose a reason for hiding this comment

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

clever

Copy link
Member

@carllerche carllerche left a comment

Choose a reason for hiding this comment

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

Anyway, this looks good to me. Because it is an unstable API, I won't block on a test.

Copy link
Member

@hawkw hawkw left a comment

Choose a reason for hiding this comment

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

looks good to me!

@davidbarsky
Copy link
Member

Running CI since it needed permissions to do so.

@Darksonn Darksonn added A-tokio Area: The main tokio crate M-task Module: tokio/task M-tracing Tracing support in Tokio labels Jun 28, 2021
Comment on lines 49 to 63
#[derive(Default, Debug)]
pub struct Builder<'a> {
name: Option<&'a str>,
}

impl Builder<'_> {
/// Creates a new task builder.
pub fn new() -> Self {
Self::default()
}

/// Assigns a name to the task which will be spawned.
pub fn name<'b>(&self, name: &'b str) -> Builder<'b> {
Builder { name: Some(name) }
}
Copy link
Contributor

Choose a reason for hiding this comment

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

This seems dangerous. If we add another option that also has a lifetime in the future, then this signature will not work because that other option's lifetime will continue to be 'a, not 'b. To handle that, we would need two lifetime parameters, but afaik adding a new lifetime parameter is a breaking change.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Wouldn't adding a second lifetime to a public struct already be a breaking change?

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes. My point is that you also can't reuse the same lifetime for both fields.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

What other lifetime are we discussing here, and what is the proposed change? Only accepting &'static str (or Into<Cow<'static, str>> in order to not lose support for dynamic names, which we established we didn't want to enforce against)? I'm not sure what you're proposing

Copy link
Contributor Author

@jbr jbr Jun 28, 2021

Choose a reason for hiding this comment

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

We couldn't add another lifetime once this is stable so I don't understand the concern. Are you proposing that we design for hypothetical future borrows to necessarily have the same lifetime as the str?

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 more that this introduces a convention for having borrowed options for the builder. If we add any other string arguments, then we would be in trouble if the convention is to store them as a reference.

Copy link
Contributor

@Darksonn Darksonn Jun 28, 2021

Choose a reason for hiding this comment

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

If the signature is changed to this, then reusing the 'a for all parameters that are by reference would not be a problem.

impl<'a> Builder<'a> {
    /// Assigns a name to the task which will be spawned.
    pub fn name(&self, name: &'a str) -> Builder<'a> {
        Builder { name: Some(name) }
    }
}

Copy link
Contributor Author

@jbr jbr Jun 28, 2021

Choose a reason for hiding this comment

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

[deleted comment]

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks for the suggestion, addressed in 183d556

hawkw added a commit to tokio-rs/console that referenced this pull request Aug 4, 2021
Now that Tokio supports optional user-provided task names
(in tokio-rs/tokio#3881), the task spans it generates may contain empty
fields (if the user does not provide a name for a spawned task).
However, the console always displays the `task.name` field, even when
they are empty. This wastes space, and looks kind of ugly.

This branch updates the console to skip displaying fields whose value is
the empty string.

### Before:
![image](https://user-images.githubusercontent.com/2796466/128227118-7ee3b4d7-634f-410f-a1d0-4e5afd423566.png)

### After:
![image](https://user-images.githubusercontent.com/2796466/128227203-248bd059-839a-4254-8470-2761f726c885.png)

Signed-off-by: Eliza Weisman <eliza@buoyant.io>
hawkw added a commit to tokio-rs/console that referenced this pull request Aug 4, 2021
Depends on #81 (since it touches the same code and would cause merge conflicts).

* feat(console): don't display fields with "" values

Now that Tokio supports optional user-provided task names
(in tokio-rs/tokio#3881), the task spans it generates may contain empty
fields (if the user does not provide a name for a spawned task).
However, the console always displays the `task.name` field, even when
they are empty. This wastes space, and looks kind of ugly.

This branch updates the console to skip displaying fields whose value is
the empty string.

### Before:
![image](https://user-images.githubusercontent.com/2796466/128227118-7ee3b4d7-634f-410f-a1d0-4e5afd423566.png)

### After:
![image](https://user-images.githubusercontent.com/2796466/128227203-248bd059-839a-4254-8470-2761f726c885.png)

Signed-off-by: Eliza Weisman <eliza@buoyant.io>

* refac(console): factor out field from pb conversion

This factors out the protobuf `Field` -> internal `Field` conversion
into a method, and cleans it up a bit. No functional change, except that
we now take the proto `Field` type by value, so we don't have to clone
strings out of it like we did previously (due to taking it by
reference). I also added the metadata ID to the `Metadata` type, so that
we can continue asserting that the metadata's ID matches the expected
one for the field, without having to pass that in separately.

Signed-off-by: Eliza Weisman <eliza@buoyant.io>
hawkw added a commit to tokio-rs/console that referenced this pull request Aug 4, 2021
Depends on #81 (since it touches the same code and would cause merge conflicts).

* feat(console): don't display fields with "" values

Now that Tokio supports optional user-provided task names
(in tokio-rs/tokio#3881), the task spans it generates may contain empty
fields (if the user does not provide a name for a spawned task).
However, the console always displays the `task.name` field, even when
they are empty. This wastes space, and looks kind of ugly.

This branch updates the console to skip displaying fields whose value is
the empty string.

### Before:
![image](https://user-images.githubusercontent.com/2796466/128227118-7ee3b4d7-634f-410f-a1d0-4e5afd423566.png)

### After:
![image](https://user-images.githubusercontent.com/2796466/128227203-248bd059-839a-4254-8470-2761f726c885.png)

Signed-off-by: Eliza Weisman <eliza@buoyant.io>

* refac(console): factor out field from pb conversion

This factors out the protobuf `Field` -> internal `Field` conversion
into a method, and cleans it up a bit. No functional change, except that
we now take the proto `Field` type by value, so we don't have to clone
strings out of it like we did previously (due to taking it by
reference). I also added the metadata ID to the `Metadata` type, so that
we can continue asserting that the metadata's ID matches the expected
one for the field, without having to pass that in separately.

Signed-off-by: Eliza Weisman <eliza@buoyant.io>
hawkw added a commit that referenced this pull request Feb 9, 2022
PR #3881 factored out the spawning of local tasks on a `LocalSet` into a
function `spawn_local_inner`, so that the implementation could be shared
with the `task::Builder` API. But, that PR neglected to add a
`#[track_caller]` attribute to `spawn_local_inner`, so the `tracing`
spans for local tasks are all generated with `spawn_local_inner` as
their spawn location, rather than forwarding the actual spawn location
from the calling function.

This causes pretty useless results when using `tokio-console` with code
that spawns a number of local tasks, such as Actix
(https://old.reddit.com/r/rust/comments/snt5fq/can_tokioconsole_profile_actixrt/)

This commit fixes the issue by adding the missing `#[track_caller]`
attribute.
Darksonn pushed a commit that referenced this pull request Feb 9, 2022
PR #3881 factored out the spawning of local tasks on a `LocalSet` into a
function `spawn_local_inner`, so that the implementation could be shared
with the `task::Builder` API. But, that PR neglected to add a
`#[track_caller]` attribute to `spawn_local_inner`, so the `tracing`
spans for local tasks are all generated with `spawn_local_inner` as
their spawn location, rather than forwarding the actual spawn location
from the calling function.

This causes pretty useless results when using `tokio-console` with code
that spawns a number of local tasks, such as Actix
(https://reddit.com/r/rust/comments/snt5fq/can_tokioconsole_profile_actixrt/)

This commit fixes the issue by adding the missing `#[track_caller]`
attribute.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-tokio Area: The main tokio crate M-task Module: tokio/task M-tracing Tracing support in Tokio
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants