-
Notifications
You must be signed in to change notification settings - Fork 734
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
core: add faster fast path for level filtering #853
Conversation
Oh, cool update: looks like the reason the We should probably either skip these tests when any |
Ugh...changes to the CI config will only actually change anything when merged to The tests currently only pass with |
It turns out that the test run with `--all-features` should never be able to pass, due to the `static_max_level` features. Setting `--all-features` will enable `static_max_level_off`, which disables all tracing. Therefore, all the integration tests will fail. However, due to a bug in `tracing`'s `LevelFilter::OFF` implementation, `static_max_level_off` actually _enables_ everything. This makes the tests pass accidentally. If we fix this bug, though, everything will be disabled, as it should be, causing the tests to fail. Therefore, we can't merge a PR that fixes this, like #853, without first resolving the issues with the tests. Unfortunately, these are integration tests, so we cannot just put ```rust #[cfg(not(any(feature = "static_max_level_off", feature = ...))] ``` on the tests to skip them when the feature is set --- features set on the _crate_ being tested are not visible to integration tests. Similarly, we cannot use ```rust #[cfg(test)] ``` to not set the max level when running the tests, because the test cfg is not set for the crate when running integration tests, only unit tests. We could change all these tests to be integration tests, I suppose. However, we are already running a `cargo hack` job that tests every feature combination for `tracing` _except_ for the `static_max_level` ones, so I think it's fine to just remove this. Signed-off-by: Eliza Weisman <[email protected]>
@@ -33,7 +33,7 @@ impl Subscriber for TestSubscriber { | |||
fn exit(&self, _span: &Id) {} | |||
} | |||
|
|||
#[cfg(test)] | |||
#[test] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this test was never actually running 🙃
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
RIP
tracing-core/src/metadata.rs
Outdated
/// Returns a `LevelFilter` that matches the most verbose [`Level`] that any | ||
/// currently active [`Subscriber`] will enable. | ||
#[inline(always)] | ||
pub fn max() -> Self { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Interesting fact: log
has a LevelFilter::max
with the same signature that returns a totally different thing. Maybe we should rename this to avoid confusing users familiar with the log
API...
I am not really sure if there's all that much value in having a method for returning the highest possible level filter, when users can just write TRACE
...but maybe people are doing that. Also, I would have probably called that function all()
, but, whatever...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@davidbarsky @LucioFranco any thoughts on the API naming before I merge this PR?
/// determine the most verbose level that will be enabled. This will allow | ||
/// spans and events which are more verbose than that level to be skipped | ||
/// more efficiently. Subscribers which perform filtering are strongly |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I never liked "verbosity" when applied to levels, but... I guess that ship sailed. I was always confused by what it meant.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If there's another wording you think is clearer, I'm all ears! If we're going to change it, we should change it throughout the docs.
@@ -33,7 +33,7 @@ impl Subscriber for TestSubscriber { | |||
fn exit(&self, _span: &Id) {} | |||
} | |||
|
|||
#[cfg(test)] | |||
#[test] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
RIP
Signed-off-by: Eliza Weisman <[email protected]>
Signed-off-by: Eliza Weisman <[email protected]>
Signed-off-by: Eliza Weisman <[email protected]>
Signed-off-by: Eliza Weisman <[email protected]>
Signed-off-by: Eliza Weisman <[email protected]>
Signed-off-by: Eliza Weisman <[email protected]>
Signed-off-by: Eliza Weisman <[email protected]>
Signed-off-by: Eliza Weisman <[email protected]>
Co-authored-by: bjorn3 <[email protected]>
Signed-off-by: Eliza Weisman <[email protected]>
Signed-off-by: Eliza Weisman <[email protected]>
Signed-off-by: Eliza Weisman <[email protected]>
Signed-off-by: Eliza Weisman <[email protected]>
FWIW, it looks like this change results in a pretty significant performance impact for |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM!
Signed-off-by: Eliza Weisman <[email protected]>
Signed-off-by: Eliza Weisman <[email protected]>
Signed-off-by: Eliza Weisman <[email protected]>
Added - `LevelFilter` type and `LevelFilter::current()` for returning the highest level that any subscriber will enable (#853) - `Subscriber::max_level_hint` optional trait method, for setting the value returned by `LevelFilter::current()` (#853) Fixed - **docs**: Removed outdated reference to a Tokio API that no longer exists (#857) Thanks to new contributor @dignati for contributing to this release! Signed-off-by: Eliza Weisman <[email protected]>
Fixed - **docs**: Incorrect formatting (#862) Changed - **filter**: `LevelFilter` is now a re-export of the `tracing_core::LevelFilter` type, it can now be used interchangably with the versions in `tracing` and `tracing-core` (#853) - **filter**: Significant performance improvements when comparing `LevelFilter`s and `Level`s (#853) - Updated the minimum `tracing-core` dependency to 0.1.12 (#853) Added - **filter**: `LevelFilter` and `EnvFilter` now participate in `tracing-core`'s max level hinting, improving performance significantly in some use cases where levels are disabled globally (#853) Signed-off-by: Eliza Weisman <[email protected]>
### Added - `LevelFilter` type and `LevelFilter::current()` for returning the highest level that any subscriber will enable (#853) - `Subscriber::max_level_hint` optional trait method, for setting the value returned by `LevelFilter::current()` (#853) ### Fixed - **docs**: Removed outdated reference to a Tokio API that no longer exists (#857) Thanks to new contributor @dignati for contributing to this release! Signed-off-by: Eliza Weisman <[email protected]>
Fixed - Fixed a bug where `LevelFilter::OFF` (and thus also the `static_max_level_off` feature flag) would enable *all* traces, rather than *none* (#853) - **log**: Fixed `tracing` macros and `Span`s not checking `log::max_level` before emitting `log` records (#870) Changed - **macros**: Macros now check the global max level (`LevelFilter::current`) before the per-callsite cache when determining if a span or event is enabled. This significantly improves performance in some use cases (#853) - **macros**: Simplified the code generated by macro expansion significantly, which may improve compile times and/or `rustc` optimizatation of surrounding code (#869, #869) - **macros**: Macros now check the static max level before checking any runtime filtering, improving performance when a span or event is disabled by a `static_max_level_XXX` feature flag (#868) - `LevelFilter` is now a re-export of the `tracing_core::LevelFilter` type, it can now be used interchangably with the versions in `tracing-core` and `tracing-subscriber` (#853) - Significant performance improvements when comparing `LevelFilter`s and `Level`s (#853) - Updated the minimum `tracing-core` dependency to 0.1.12 (#853) Added - **macros**: Quoted string literals may now be used as field names, to allow fields whose names are not valid Rust identifiers (#790) - **docs**: Several documentation improvements (#850, #857, #841) - `LevelFilter::current()` function, which returns the highest level that any subscriber will enable (#853) - `Subscriber::max_level_hint` optional trait method, for setting the value returned by `LevelFilter::current()` (#853) Thanks to new contributors @cuviper, @ethanboxx, @ben0x539, @dignati, @colelawrence, and @rbtcollins for helping out with this release! Signed-off-by: Eliza Weisman <[email protected]>
### Fixed - **docs**: Incorrect formatting (#862) ### Changed - **filter**: `LevelFilter` is now a re-export of the `tracing_core::LevelFilter` type, it can now be used interchangably with the versions in `tracing` and `tracing-core` (#853) - **filter**: Significant performance improvements when comparing `LevelFilter`s and `Level`s (#853) - Updated the minimum `tracing-core` dependency to 0.1.12 (#853) ### Added - **filter**: `LevelFilter` and `EnvFilter` now participate in `tracing-core`'s max level hinting, improving performance significantly in some use cases where levels are disabled globally (#853) Signed-off-by: Eliza Weisman <[email protected]>
### Fixed - Fixed a bug where `LevelFilter::OFF` (and thus also the `static_max_level_off` feature flag) would enable *all* traces, rather than *none* (#853) - **log**: Fixed `tracing` macros and `Span`s not checking `log::max_level` before emitting `log` records (#870) ### Changed - **macros**: Macros now check the global max level (`LevelFilter::current`) before the per-callsite cache when determining if a span or event is enabled. This significantly improves performance in some use cases (#853) - **macros**: Simplified the code generated by macro expansion significantly, which may improve compile times and/or `rustc` optimizatation of surrounding code (#869, #869) - **macros**: Macros now check the static max level before checking any runtime filtering, improving performance when a span or event is disabled by a `static_max_level_XXX` feature flag (#868) - `LevelFilter` is now a re-export of the `tracing_core::LevelFilter` type, it can now be used interchangably with the versions in `tracing-core` and `tracing-subscriber` (#853) - Significant performance improvements when comparing `LevelFilter`s and `Level`s (#853) - Updated the minimum `tracing-core` dependency to 0.1.12 (#853) ### Added - **macros**: Quoted string literals may now be used as field names, to allow fields whose names are not valid Rust identifiers (#790) - **docs**: Several documentation improvements (#850, #857, #841) - `LevelFilter::current()` function, which returns the highest level that any subscriber will enable (#853) - `Subscriber::max_level_hint` optional trait method, for setting the value returned by `LevelFilter::current()` (#853) Thanks to new contributors @cuviper, @ethanboxx, @ben0x539, @dignati, @colelawrence, and @rbtcollins for helping out with this release! Signed-off-by: Eliza Weisman <[email protected]>
This picks up upstream changes tokio-rs/tracing#853, tokio-rs/tracing#868, and tokio-rs/tracing#869 which improve performance in some use cases. The overhead removed by these changes may already be amortized enough in the proxy that it's not a problem, but it seems worth picking up regardless.
This picks up upstream changes tokio-rs/tracing#853, tokio-rs/tracing#868, and tokio-rs/tracing#869 which improve performance in some use cases. The overhead removed by these changes may already be amortized enough in the proxy that it's not a problem, but it seems worth picking up regardless. Signed-off-by: Eliza Weisman <[email protected]>
Motivation
Tracing currently stores cached filter evaluations in a per-callsite
cache. Once filters have been evaluated, this allows us to skip disabled
callsites with a single load and compare, which is a nice fast path for
skipping. However, determining the value to store in the callsite cache
is expensive: it requires acquiring a lock to register the callsite, and
asking the subscriber for an
Interest
value to cache.In long-running applications, such as servers, daemons, and interactive
applications that are used for a long period of time, this cost is not
very visible; it's amortized over the rest of the application's
lifetime. However, in batch-oriented programs and CLI tools that run to
completion, the cost of building the initial cached value has a much
bigger impact. If a given callsite is hit twice over the thirty seconds
that a CLI tool runs for (for example), the cost of the initial
evaluation is much more meaningful than if the same callsite is hit
hundreds of thousands of times by a server that runs for several weeks
before its restarted. Even in a long running application, though, it
could lead to a surprising latency hit if a given callsite is only hit
after a long period of operation.
Per-callsite caching allows high performance for more granular filters,
since it tracks the state of every callsite; we still get a fast path
for skipping a
trace
event in thefoo::bar
module if thetrace
level is only enabled for the
baz
module. Because of this, we didn'tinitially believe that a global maximum level fast path was necessary.
However, in
rustc
, it turns out that the cost of just building thecallsite cache for the first time has a noticeable impact on
performance. See here for details:
rust-lang/rust#74726 (comment)
Solution
This branch adds a faster fast path for skipping callsites whose level
is higher than the highest level enabled for any target/metadata
combination, similar to the
log
crate'smax_level
.Unlike the
log
crate, which only ever has a single global logger,tracing
allows multipleSubscriber
s to coexist in the same process.Therefore, our implementation is somewhat different than
log
's. Wecan't simply provide a
set_max_level
function call, because anothersubscriber may have already set a lower max level, and just overwriting
the max level with ours may result in diagnostics being skipped that
another subscriber wants to collect.
Instead, we've added a
max_level_hint
method to theSubscriber
trait, returning an
Option<LevelFilter>
. By default, this returnsNone
, for backwards compatibility. However, if it returnsSome
, theresulting
LevelFilter
is stored in a globalMAX_LEVEL
atomic. Thismethod is called by the
register_dispatch
function, which handles thecreation of a new
Dispatch
. This allows us to compare the max levelfrom a new subscriber with those of all other currently active
subscribers. Similarly, when cached interests are invalidated (such as
by changing a filter, or when a
Subscriber
is dropped), we canreevaluate the max level.
The path for checking the max level (e.g. in the macros) should be
very efficient. We load the atomic, convert the
usize
value into aLevelFilter
, and compare it to the callsite's level. With help from@eddyb, I was able to implement the conversion such that it should
hopefully be an identity conversion in release mode, rather than
requiring rustc to generate a LUT. The macros have been updated to check
the max level before anything requiring hitting the callsite cache or
the subscriber TLS var.
Also, I've updated
tracing
andtracing-subscriber
to reexport theLevelFilter
type fromtracing-core
rather than defining their own.The API should be a superset of those crates' versions.
Thanks to @oli-obk for uncovering this issue when adding
tracing
torustc
, and for doing a lot of the preliminary analysis of the cause!So far, there doesn't appear to be a significant change in any of
tracing
'sinternal benchmarks on this branch. However, my guess is that this is
because of
criterion
's outlier elimination: since the cache is constructedonly on the first iteration, it's pretty likely that
criterion
considers this anoutlier and throws it out entirely, so the benchmark results on master don't
include the cost of evaluating the filter for the initial time.
Incidentally, this PR also fixes a bug in the
tracing
crate's versionof
LevelFilter
, where theOFF
level actually enabled everything,rather than nothing. Oopsie!