From d0e2d78f0580811a4b1c0ebf0d214aaf4e31cad4 Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Wed, 7 Aug 2024 13:55:14 +0200 Subject: [PATCH] Setup tracing and document tracing usage --- .gitignore | 8 + Cargo.lock | 22 +- Cargo.toml | 3 +- crates/red_knot/Cargo.toml | 8 +- crates/red_knot/docs/tracing-flamegraph.png | Bin 0 -> 41274 bytes crates/red_knot/docs/tracing.md | 89 +++++++ crates/red_knot/src/logging.rs | 251 ++++++++++++++++++++ crates/red_knot/src/main.rs | 82 +------ crates/red_knot/src/verbosity.rs | 33 --- crates/ruff_db/src/parsed.rs | 2 +- 10 files changed, 383 insertions(+), 115 deletions(-) create mode 100644 crates/red_knot/docs/tracing-flamegraph.png create mode 100644 crates/red_knot/docs/tracing.md create mode 100644 crates/red_knot/src/logging.rs diff --git a/.gitignore b/.gitignore index 4302ff30a762a4..ad7de8ce76cffb 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,14 @@ flamegraph.svg # `CARGO_TARGET_DIR=target-llvm-lines RUSTFLAGS="-Csymbol-mangling-version=v0" cargo llvm-lines -p ruff --lib` /target* +# samply profiles +profile.json + +# tracing-flame traces +tracing.folded +tracing-flamechart.svg +tracing-flamegraph.svg + ### # Rust.gitignore ### diff --git a/Cargo.lock b/Cargo.lock index 3880a9982a5e1a..fb5fbcd0b812c7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1482,11 +1482,11 @@ dependencies = [ [[package]] name = "nu-ansi-term" -version = "0.50.0" +version = "0.50.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd2800e1520bdc966782168a627aa5d1ad92e33b984bf7c7615d31280c83ff14" +checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -1860,7 +1860,9 @@ name = "red_knot" version = "0.0.0" dependencies = [ "anyhow", + "chrono", "clap", + "colored", "countme", "crossbeam", "ctrlc", @@ -1873,6 +1875,7 @@ dependencies = [ "salsa", "tempfile", "tracing", + "tracing-flame", "tracing-subscriber", "tracing-tree", ] @@ -3225,6 +3228,17 @@ dependencies = [ "valuable", ] +[[package]] +name = "tracing-flame" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bae117ee14789185e129aaee5d93750abe67fdc5a9a62650452bfe4e122a3a9" +dependencies = [ + "lazy_static", + "tracing", + "tracing-subscriber", +] + [[package]] name = "tracing-indicatif" version = "0.3.6" @@ -3272,7 +3286,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f459ca79f1b0d5f71c54ddfde6debfc59c8b6eeb46808ae492077f739dc7b49c" dependencies = [ - "nu-ansi-term 0.50.0", + "nu-ansi-term 0.50.1", "tracing-core", "tracing-log", "tracing-subscriber", diff --git a/Cargo.toml b/Cargo.toml index b9e4e0d625f4ba..7ff4b380c89909 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -132,8 +132,9 @@ thiserror = { version = "1.0.58" } tikv-jemallocator = { version = "0.6.0" } toml = { version = "0.8.11" } tracing = { version = "0.1.40" } +tracing-flame = { version = "0.2.0" } tracing-indicatif = { version = "0.3.6" } -tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } +tracing-subscriber = { version = "0.3.18", default-features = false, features = ["env-filter", "fmt"] } tracing-tree = { version = "0.4.0" } typed-arena = { version = "2.0.2" } unic-ucd-category = { version = "0.9" } diff --git a/crates/red_knot/Cargo.toml b/crates/red_knot/Cargo.toml index 50781acfd123b9..5fc0fcf4926ece 100644 --- a/crates/red_knot/Cargo.toml +++ b/crates/red_knot/Cargo.toml @@ -19,20 +19,22 @@ red_knot_server = { workspace = true } ruff_db = { workspace = true, features = ["os", "cache"] } anyhow = { workspace = true } +chrono = { workspace = true } clap = { workspace = true, features = ["wrap_help"] } +colored = { workspace = true } countme = { workspace = true, features = ["enable"] } crossbeam = { workspace = true } ctrlc = { version = "3.4.4" } rayon = { workspace = true } salsa = { workspace = true } -tracing = { workspace = true } -tracing-subscriber = { workspace = true } +tracing = { workspace = true, features = ["release_max_level_debug"] } +tracing-subscriber = { workspace = true, features = ["env-filter", "fmt"] } +tracing-flame = { workspace = true } tracing-tree = { workspace = true } [dev-dependencies] filetime = { workspace = true } tempfile = { workspace = true } - [lints] workspace = true diff --git a/crates/red_knot/docs/tracing-flamegraph.png b/crates/red_knot/docs/tracing-flamegraph.png new file mode 100644 index 0000000000000000000000000000000000000000..6a350c6b551ef92e0f683bd4fdf103e6e27859a2 GIT binary patch literal 41274 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 z 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 literal 0 HcmV?d00001 diff --git a/crates/red_knot/docs/tracing.md b/crates/red_knot/docs/tracing.md new file mode 100644 index 00000000000000..275c08c245ffd6 --- /dev/null +++ b/crates/red_knot/docs/tracing.md @@ -0,0 +1,89 @@ +# Tracing + +Traces are a useful tool to narrow down the location of a bug or, at least, to understand why the compiler is doing a particular thing. +Note, tracing messages with severity `debug`..`error` are user-facing. They should be phrased accordingly. +Tracing spans are only shown when using `-vvv`. + +## Verbosity levels + +The CLI supports different verbosity levels. + +- default: Only show errors and warnings. +- `-v` activates `info!`: Show generally useful information such as paths of configuration files, detected platform, etc., but it's not a lot of messages, it's something you'll activate in CI by default. cargo build e.g. shows you which packages are fresh. +- `-vv` activates `debug!` and timestamps: This should be enough information to get to the bottom of bug reports. When you're processing many packages or files, you'll get pages and pages of output, but each line is link to a specific action or state change. +- `-vvv` activates `trace!` (only in debug builds) and shows tracing-spans: At this level, you're logging everything. Most of this is wasted, it's really slow, we dump e.g. the entire resolution graph. Only useful to developers, and you almost certainly want to use `RED_KNOT_LOG` to filter it down to the area your investigating. + +## `RED_KNOT_LOG` + +By default, the CLI shows messages from the `ruff` and `red_knot` crates. Tracing messages from other crates are not shown. +The `RED_KNOT_LOG` environment variable allows you to customize which messages are shown by specifying one +or more [filter directives](https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#directives). + +### Examples + +#### Show all debug messages + +Shows debug messages from all crates. + +```bash +RED_KNOT_LOG=debug +``` + +#### Show salsa query execution messages + +Show the salsa `execute: my_query` messages in addition to all red knot messages. + +```bash +RED_KNOT_LOG=ruff=trace,red_knot=trace,salsa=info +``` + +#### Show typing traces + +Only show traces for the `red_knot_python_semantic::types` module. + +```bash +RED_KNOT_LOG="red_knot_python_semantic::types" +``` + +Note: Ensure that you use `-vvv` to see tracing spans. + +## Tracing and Salsa + +Be mindful about using `tracing` in Salsa queries, especially when using `warn` or `error` because it isn't guaranteed +that the query will execute after restoring from a persistent cache. In which case the user won't see the message. + +For example, don't use `tracing` to show the user a message when generating a lint violation failed +because the message would only be shown when linting the file the first time, but not on subsequent analysis +runs or when restoring from a persistent cache. This can be confusing for users because they +don't understand why a specific lint violation isn't raised. Instead, change your +query to return the failure as part of the query's result or use a Salsa accumulator. + +## Release builds + +`trace!` events are removed in release builds. + +## Profiling + +Red Knot generates a folded stack trace to the current directory named `tracing.folded` when setting the environment variable `RED_KNOT_LOG_PROFILE` to `1` or `true`. + +```bash +RED_KNOT_LOG_PROFILE=1 red_knot -- --current-directory=../test -vvv +``` + +You can convert the textual representation into a visual one using `inferno`. + +```shell +cargo install inferno +``` + +```shell +# flamegraph +cat tracing.folded | inferno-flamegraph > tracing-flamegraph.svg + +# flamechart +cat tracing.folded | inferno-flamegraph --flamechart > tracing-flamechart.svg +``` + +![Example flamegraph](./tracing-flamegraph.png) + +See [`tracing-flame`](https://crates.io/crates/tracing-flame) for more details. diff --git a/crates/red_knot/src/logging.rs b/crates/red_knot/src/logging.rs new file mode 100644 index 00000000000000..7a9c796b5e107b --- /dev/null +++ b/crates/red_knot/src/logging.rs @@ -0,0 +1,251 @@ +//! Sets up logging for Red Knot + +use anyhow::Context; +use colored::Colorize; +use std::fmt; +use std::fs::File; +use std::io::BufWriter; +use tracing::log::LevelFilter; +use tracing::{Event, Subscriber}; +use tracing_subscriber::fmt::format::Writer; +use tracing_subscriber::fmt::{FmtContext, FormatEvent, FormatFields}; +use tracing_subscriber::registry::LookupSpan; +use tracing_subscriber::EnvFilter; + +/// Logging flags to `#[command(flatten)]` into your CLI +#[derive(clap::Args, Debug, Clone, Default)] +#[command(about = None, long_about = None)] +pub(crate) struct Verbosity { + #[arg( + long, + short = 'v', + help = "Use verbose output (or `-vv` and `-vvv` for more verbose output)", + action = clap::ArgAction::Count, + global = true, + )] + verbose: u8, +} + +impl Verbosity { + /// Returns the verbosity level based on the number of `-v` flags. + /// + /// Returns `None` if the user did not specify any verbosity flags. + pub(crate) fn level(&self) -> VerbosityLevel { + match self.verbose { + 0 => VerbosityLevel::Default, + 1 => VerbosityLevel::Verbose, + 2 => VerbosityLevel::ExtraVerbose, + _ => VerbosityLevel::Trace, + } + } +} + +#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)] +pub(crate) enum VerbosityLevel { + /// Default output level. Only shows ruff and red knot events up to the [`LogLevel::Warn`]. + Default, + + /// Enables verbose output. Emits ruff and red knot events up to the [`LogLevel::Info`] + Verbose, + + /// Enables a more verbose tracing format and emits ruff and red_knot events up to [`LogLevel::Debug`] Corresponds to `-vv` + ExtraVerbose, + + /// Enables all tracing events and uses a tree like output format. Corresponds to `-vvv`. + Trace, +} + +impl VerbosityLevel { + const fn level_filter(self) -> LevelFilter { + match self { + VerbosityLevel::Default => LevelFilter::Warn, + VerbosityLevel::Verbose => LevelFilter::Info, + VerbosityLevel::ExtraVerbose => LevelFilter::Debug, + VerbosityLevel::Trace => LevelFilter::Trace, + } + } + + pub(crate) const fn is_trace(self) -> bool { + matches!(self, VerbosityLevel::Trace) + } + + pub(crate) const fn is_extra_verbose(self) -> bool { + matches!(self, VerbosityLevel::ExtraVerbose) + } +} + +pub(crate) fn setup_tracing(level: VerbosityLevel) -> anyhow::Result { + use tracing_subscriber::prelude::*; + + // The `RED_KNOT_LOG` environment variable overrides the default log level. + let filter = if let Ok(log_env_variable) = std::env::var("RED_KNOT_LOG") { + EnvFilter::builder() + .parse(log_env_variable) + .context("Failed to parse directives specified in RED_KNOT_LOG environment variable.")? + } else { + match level { + VerbosityLevel::Default => { + // Show warning traces + EnvFilter::default().add_directive(tracing::level_filters::LevelFilter::WARN.into()) + } + level => { + let level_filter = level.level_filter(); + + // Show info|debug|trace events, but allow `RED_KNOT_LOG` to override + let filter = EnvFilter::default().add_directive( + format!("red_knot={level_filter}") + .parse() + .expect("Hardcoded directive to be valid"), + ); + + filter.add_directive( + format!("ruff={level_filter}") + .parse() + .expect("Hardcoded directive to be valid"), + ) + } + } + }; + + let (profiling_layer, guard) = setup_profile(); + + let registry = tracing_subscriber::registry() + .with(filter) + .with(profiling_layer); + + if level.is_trace() { + let subscriber = registry.with( + tracing_tree::HierarchicalLayer::default() + .with_indent_lines(true) + .with_indent_amount(2) + .with_bracketed_fields(true) + .with_thread_ids(true) + .with_targets(true) + .with_writer(std::io::stderr) + .with_timer(tracing_tree::time::Uptime::default()), + ); + + subscriber.init(); + } else { + let subscriber = registry.with( + tracing_subscriber::fmt::layer() + .event_format(RedKnotFormat { + display_level: true, + display_timestamp: level.is_extra_verbose(), + show_spans: false, + }) + .with_writer(std::io::stderr), + ); + + subscriber.init(); + } + + Ok(TracingGuard { + _flame_guard: guard, + }) +} + +fn setup_profile() -> ( + Option>>, + Option>>, +) +where + S: Subscriber + for<'span> LookupSpan<'span>, +{ + if let Ok("1" | "true") = std::env::var("RED_KNOT_LOG_PROFILE").as_deref() { + let (layer, guard) = tracing_flame::FlameLayer::with_file("tracing.folded") + .expect("Flame layer to be created"); + (Some(layer), Some(guard)) + } else { + (None, None) + } +} + +pub(crate) struct TracingGuard { + _flame_guard: Option>>, +} + +struct RedKnotFormat { + display_timestamp: bool, + display_level: bool, + show_spans: bool, +} + +/// See +impl FormatEvent for RedKnotFormat +where + S: Subscriber + for<'a> LookupSpan<'a>, + N: for<'a> FormatFields<'a> + 'static, +{ + fn format_event( + &self, + ctx: &FmtContext<'_, S, N>, + mut writer: Writer<'_>, + event: &Event<'_>, + ) -> fmt::Result { + let meta = event.metadata(); + let ansi = writer.has_ansi_escapes(); + + if self.display_timestamp { + let timestamp = chrono::Local::now() + .format("%Y-%m-%d %H:%M:%S.%f") + .to_string(); + if ansi { + write!(writer, "{} ", timestamp.dimmed())?; + } else { + write!( + writer, + "{} ", + chrono::Local::now().format("%Y-%m-%d %H:%M:%S.%f") + )?; + } + } + + if self.display_level { + let level = meta.level(); + // Same colors as tracing + if ansi { + let formatted_level = level.to_string(); + match *level { + tracing::Level::TRACE => { + write!(writer, "{} ", formatted_level.purple().bold())? + } + tracing::Level::DEBUG => write!(writer, "{} ", formatted_level.blue().bold())?, + tracing::Level::INFO => write!(writer, "{} ", formatted_level.green().bold())?, + tracing::Level::WARN => write!(writer, "{} ", formatted_level.yellow().bold())?, + tracing::Level::ERROR => write!(writer, "{} ", level.to_string().red().bold())?, + } + } else { + write!(writer, "{level} ")?; + } + } + + if self.show_spans { + let span = event.parent(); + let mut seen = false; + + let span = span + .and_then(|id| ctx.span(id)) + .or_else(|| ctx.lookup_current()); + + let scope = span.into_iter().flat_map(|span| span.scope().from_root()); + + for span in scope { + seen = true; + if ansi { + write!(writer, "{}:", span.metadata().name().bold())?; + } else { + write!(writer, "{}:", span.metadata().name())?; + } + } + + if seen { + writer.write_char(' ')?; + } + } + + ctx.field_format().format_fields(writer.by_ref(), event)?; + + writeln!(writer) + } +} diff --git a/crates/red_knot/src/main.rs b/crates/red_knot/src/main.rs index 1a6a555be2767a..208306416449fb 100644 --- a/crates/red_knot/src/main.rs +++ b/crates/red_knot/src/main.rs @@ -4,12 +4,6 @@ use std::sync::Mutex; use clap::Parser; use crossbeam::channel as crossbeam_channel; use red_knot_workspace::site_packages::site_packages_dirs_of_venv; -use tracing::subscriber::Interest; -use tracing::{Level, Metadata}; -use tracing_subscriber::filter::LevelFilter; -use tracing_subscriber::layer::{Context, Filter, SubscriberExt}; -use tracing_subscriber::{Layer, Registry}; -use tracing_tree::time::Uptime; use red_knot_workspace::db::RootDatabase; use red_knot_workspace::watch; @@ -18,8 +12,10 @@ use red_knot_workspace::workspace::WorkspaceMetadata; use ruff_db::program::{ProgramSettings, SearchPathSettings}; use ruff_db::system::{OsSystem, System, SystemPathBuf}; use target_version::TargetVersion; -use verbosity::{Verbosity, VerbosityLevel}; +use crate::logging::{setup_tracing, Verbosity, VerbosityLevel}; + +mod logging; mod target_version; mod verbosity; @@ -106,7 +102,7 @@ pub fn main() -> anyhow::Result<()> { } = Args::parse_from(std::env::args().collect::>()); let verbosity = verbosity.level(); - countme::enable(verbosity == Some(VerbosityLevel::Trace)); + countme::enable(verbosity.is_trace()); if matches!(command, Some(Command::Server)) { let four = NonZeroUsize::new(4).unwrap(); @@ -119,7 +115,7 @@ pub fn main() -> anyhow::Result<()> { return red_knot_server::Server::new(worker_threads)?.run(); } - setup_tracing(verbosity); + let _guard = setup_tracing(verbosity)?; let cwd = if let Some(cwd) = current_directory { let canonicalized = cwd.as_utf8_path().canonicalize_utf8().unwrap(); @@ -192,11 +188,11 @@ struct MainLoop { /// The file system watcher, if running in watch mode. watcher: Option, - verbosity: Option, + verbosity: VerbosityLevel, } impl MainLoop { - fn new(verbosity: Option) -> (Self, MainLoopCancellationToken) { + fn new(verbosity: VerbosityLevel) -> (Self, MainLoopCancellationToken) { let (sender, receiver) = crossbeam_channel::bounded(10); ( @@ -254,7 +250,7 @@ impl MainLoop { if check_revision == revision { eprintln!("{}", result.join("\n")); - if self.verbosity == Some(VerbosityLevel::Trace) { + if self.verbosity.is_trace() { eprintln!("{}", countme::get_all()); } } @@ -284,7 +280,7 @@ impl MainLoop { #[allow(clippy::print_stderr, clippy::unused_self)] fn exit(self) { - if self.verbosity == Some(VerbosityLevel::Trace) { + if self.verbosity.is_trace() { eprintln!("Exit"); eprintln!("{}", countme::get_all()); } @@ -313,63 +309,3 @@ enum MainLoopMessage { ApplyChanges(Vec), Exit, } - -fn setup_tracing(verbosity: Option) { - let trace_level = match verbosity { - None => Level::WARN, - Some(VerbosityLevel::Info) => Level::INFO, - Some(VerbosityLevel::Debug) => Level::DEBUG, - Some(VerbosityLevel::Trace) => Level::TRACE, - }; - - let subscriber = Registry::default().with( - tracing_tree::HierarchicalLayer::default() - .with_indent_lines(true) - .with_indent_amount(2) - .with_bracketed_fields(true) - .with_thread_ids(true) - .with_targets(true) - .with_writer(|| Box::new(std::io::stderr())) - .with_timer(Uptime::default()) - .with_filter(LoggingFilter { trace_level }), - ); - - tracing::subscriber::set_global_default(subscriber).unwrap(); -} - -struct LoggingFilter { - trace_level: Level, -} - -impl LoggingFilter { - fn is_enabled(&self, meta: &Metadata<'_>) -> bool { - let filter = if meta.target().starts_with("red_knot") || meta.target().starts_with("ruff") { - self.trace_level - } else if meta.target().starts_with("salsa") && self.trace_level <= Level::INFO { - // Salsa emits very verbose query traces with level info. Let's not show these to the user. - Level::WARN - } else { - Level::INFO - }; - - meta.level() <= &filter - } -} - -impl Filter for LoggingFilter { - fn enabled(&self, meta: &Metadata<'_>, _cx: &Context<'_, S>) -> bool { - self.is_enabled(meta) - } - - fn callsite_enabled(&self, meta: &'static Metadata<'static>) -> Interest { - if self.is_enabled(meta) { - Interest::always() - } else { - Interest::never() - } - } - - fn max_level_hint(&self) -> Option { - Some(LevelFilter::from_level(self.trace_level)) - } -} diff --git a/crates/red_knot/src/verbosity.rs b/crates/red_knot/src/verbosity.rs index 692553bcd93e20..8b137891791fe9 100644 --- a/crates/red_knot/src/verbosity.rs +++ b/crates/red_knot/src/verbosity.rs @@ -1,34 +1 @@ -#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)] -pub(crate) enum VerbosityLevel { - Info, - Debug, - Trace, -} -/// Logging flags to `#[command(flatten)]` into your CLI -#[derive(clap::Args, Debug, Clone, Default)] -#[command(about = None, long_about = None)] -pub(crate) struct Verbosity { - #[arg( - long, - short = 'v', - help = "Use verbose output (or `-vv` and `-vvv` for more verbose output)", - action = clap::ArgAction::Count, - global = true, - )] - verbose: u8, -} - -impl Verbosity { - /// Returns the verbosity level based on the number of `-v` flags. - /// - /// Returns `None` if the user did not specify any verbosity flags. - pub(crate) fn level(&self) -> Option { - match self.verbose { - 0 => None, - 1 => Some(VerbosityLevel::Info), - 2 => Some(VerbosityLevel::Debug), - _ => Some(VerbosityLevel::Trace), - } - } -} diff --git a/crates/ruff_db/src/parsed.rs b/crates/ruff_db/src/parsed.rs index 90afb1fa7ba36a..3b032fef018c4c 100644 --- a/crates/ruff_db/src/parsed.rs +++ b/crates/ruff_db/src/parsed.rs @@ -22,7 +22,7 @@ use crate::Db; /// for determining if a query result is unchanged. #[salsa::tracked(return_ref, no_eq)] pub fn parsed_module(db: &dyn Db, file: File) -> ParsedModule { - let _span = tracing::trace_span!("parse_module", file = ?file.path(db)).entered(); + let _span = tracing::trace_span!("parsed_module", file = ?file.path(db)).entered(); let source = source_text(db, file); let path = file.path(db);