diff --git a/crates/ty/Cargo.toml b/crates/ty/Cargo.toml index 129012b6e1941..ce07cb71d0631 100644 --- a/crates/ty/Cargo.toml +++ b/crates/ty/Cargo.toml @@ -57,6 +57,7 @@ toml = { workspace = true } [features] default = [] +tdd-stats = ["ty_python_semantic/tdd-stats"] [lints] workspace = true diff --git a/crates/ty/docs/environment.md b/crates/ty/docs/environment.md index 9a0b1f9d13918..0291e0d225ab8 100644 --- a/crates/ty/docs/environment.md +++ b/crates/ty/docs/environment.md @@ -30,6 +30,79 @@ If set to `"1"` or `"true"`, ty will enable flamegraph profiling. This creates a `tracing.folded` file that can be used to generate flame graphs for performance analysis. +### `TY_TDD_STATS_REPORT` + +Controls reporting of TDD (ternary decision diagram) size statistics after `ty check`. + +This is a developer-focused diagnostic mode and is only available when ty is built +with the `tdd-stats` cargo feature. +Without this feature, no TDD stats collection code is compiled into the binary. + +Accepted values: + +- `0`: Disable TDD stats output (default when unset). +- `1` or `short`: Emit summary and per-file counts through tracing target `ty.tdd_stats`. + Includes both `reachability_*` and `narrowing_*` counters. +- `2` or `full`: Emit `1` output plus per-scope summaries (including histograms) and + hot-node diagnostics through tracing target `ty.tdd_stats`. + +Values greater than `2` are treated as `2` (`full`). + +Example: + +```bash +TY_TDD_STATS_REPORT=1 TY_LOG=ty.tdd_stats=info cargo run -p ty --features tdd-stats -- check path/to/project +``` + +```bash +TY_TDD_STATS_REPORT=2 TY_LOG=ty.tdd_stats=info cargo run -p ty --features tdd-stats -- check path/to/project +``` + +For tracing filter syntax and logging tips, see [Tracing](./tracing.md). + +#### How to read `tdd_stats_summary` and `tdd_stats_file` + +`short` and `full` both emit project-level and per-file summary lines on the `ty.tdd_stats` target: + +```text +INFO tdd_stats_summary verbose=... files=... max_root_nodes=... reachability_roots=... reachability_nodes=... reachability_max_depth=... narrowing_roots=... narrowing_nodes=... narrowing_max_depth=... +INFO tdd_stats_file file=... max_root_nodes=... reachability_roots=... reachability_nodes=... reachability_max_depth=... narrowing_roots=... narrowing_nodes=... narrowing_max_depth=... +``` + +Field meanings: + +- `verbose`: Effective verbosity level (`1` for short, `2` for full). +- `files`: Number of analyzed files with non-empty stats (summary line only). +- `max_root_nodes`: Largest interior-node count among single roots in scope. +- `reachability_roots` / `narrowing_roots`: Unique root-constraint ID counts split by family. +- `reachability_nodes` / `narrowing_nodes`: Interior-node visits split by family. +- `reachability_max_depth` / `narrowing_max_depth`: Maximum TDD depth observed in each family. + +#### How to read `tdd_stats_hot_node` (full mode) + +In `full` mode, ty emits `tdd_stats_hot_node` lines on the `ty.tdd_stats` target: + +```text +INFO tdd_stats_hot_node file=... scope_id=... kind=... subtree_nodes=... root_uses=... score=... roots=... +``` + +Field meanings: + +- `kind`: Which root family this hotspot is attributed to (`reachability` or `narrowing`). +- `subtree_nodes`: Number of interior nodes reachable from `constraint` (subtree size). +- `root_uses`: Number of root constraints whose TDD includes this interior node. +- `score`: Hotness score, computed as `subtree_nodes * root_uses`. +- `roots`: Up to five sample roots that include this node. + - `line:column` means source location was resolved from an AST node. + - `unknown` is fallback when source location could not be resolved. + +Practical interpretation: + +- Higher `score` means a larger subtree reused by many roots, hence a likely hotspot. +- If multiple top rows share very similar `roots`, they are often one clustered hotspot, not unrelated issues. +- Use `subtree_nodes` to spot deep/large structures and `root_uses` to spot broad fanout; both can dominate runtime. +- In `short` mode, compare `reachability_*` vs `narrowing_*` first to decide which family to investigate in `full` mode. + ### `TY_MAX_PARALLELISM` Specifies an upper limit for the number of tasks ty is allowed to run in parallel. @@ -84,4 +157,3 @@ Path to user-level configuration directory on Unix systems. ### `_CONDA_ROOT` Used to determine the root install path of Conda. - diff --git a/crates/ty/docs/mypy_primer.md b/crates/ty/docs/mypy_primer.md index 26cc17c23874c..4894e9951a0a3 100644 --- a/crates/ty/docs/mypy_primer.md +++ b/crates/ty/docs/mypy_primer.md @@ -33,6 +33,13 @@ mypy_primer \ This will show the diagnostics diff for the `black` project between the `main` branch and your `my/feature` branch. To run the diff for all projects we currently enable in CI, use `--project-selector "/($(paste -s -d'|' crates/ty_python_semantic/resources/primer/good.txt))\$"`. +If you're investigating performance regressions, you can also enable TDD stats while running `mypy_primer` +by setting `TY_TDD_STATS_REPORT` and `TY_LOG=ty.tdd_stats=info` in the environment (for `tdd-stats` builds). +For baseline comparisons, `TY_TDD_STATS_REPORT=1` is usually easiest to diff. +Switch to `TY_TDD_STATS_REPORT=2` when you need scope-level histograms and hot-node details. +(`short`/`full` are aliases for `1`/`2`.) +See [`TY_TDD_STATS_REPORT`](./environment.md#ty_tdd_stats_report) and [Tracing](./tracing.md) for details. + You can also take a look at the [full list of ecosystem projects]. Note that some of them might still need a `ty_paths` configuration option to work correctly. diff --git a/crates/ty/docs/tracing.md b/crates/ty/docs/tracing.md index 898b8271d1f23..59638cb40fff2 100644 --- a/crates/ty/docs/tracing.md +++ b/crates/ty/docs/tracing.md @@ -75,6 +75,27 @@ whether one if its children has the file `x.py`. **Note**: Salsa currently logs the entire memoized values. In our case, the source text and parsed AST. This very quickly leads to extremely long outputs. +#### Show TDD stats traces + +`tdd-stats` builds can emit TDD-size diagnostics on the `ty.tdd_stats` tracing target. +This is useful for analyzing TDD blowups and hot nodes. + +```bash +TY_TDD_STATS_REPORT=2 TY_LOG=ty.tdd_stats=info cargo run -p ty --features tdd-stats -- check path/to/project +``` + +For quick regressions checks, start with: + +```bash +TY_TDD_STATS_REPORT=1 TY_LOG=ty.tdd_stats=info cargo run -p ty --features tdd-stats -- check path/to/project +``` + +`short` mode reports `reachability_*` and `narrowing_*` counters, which makes old/new diffs easier to scan. +Use `full` when you need per-scope histograms and `tdd_stats_hot_node` output. +(`short`/`full` are aliases for `1`/`2`.) + +See [`TY_TDD_STATS_REPORT`](./environment.md#ty_tdd_stats_report) for modes and output interpretation. + ## Tracing and Salsa Be mindful about using `tracing` in Salsa queries, especially when using `warn` or `error` because it isn't guaranteed diff --git a/crates/ty/src/lib.rs b/crates/ty/src/lib.rs index c90a8dee5bb2c..1cccf1291b5ec 100644 --- a/crates/ty/src/lib.rs +++ b/crates/ty/src/lib.rs @@ -27,6 +27,8 @@ use ty_project::metadata::settings::TerminalSettings; use ty_project::watch::ProjectWatcher; use ty_project::{CollectReporter, Db, suppress_all_diagnostics, watch}; use ty_project::{ProjectDatabase, ProjectMetadata}; +#[cfg(feature = "tdd-stats")] +use ty_python_semantic::semantic_index::tdd_stats_for_file; use ty_server::run_server; use ty_static::EnvVars; @@ -179,6 +181,10 @@ fn run_check(args: CheckCommand) -> anyhow::Result { } Err(_) => {} } + drop(stdout); + + #[cfg(feature = "tdd-stats")] + write_tdd_stats_report(&db, printer); std::mem::forget(db); @@ -189,6 +195,230 @@ fn run_check(args: CheckCommand) -> anyhow::Result { } } +#[cfg(feature = "tdd-stats")] +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +enum TddStatsReportMode { + Off = 0, + Short, + Full, +} + +#[cfg(feature = "tdd-stats")] +impl TddStatsReportMode { + const MAX_LEVEL: u8 = TddStatsReportMode::Full as u8; + + const fn from_verbose(verbose: u8) -> Self { + match verbose { + 0 => TddStatsReportMode::Off, + 1 => TddStatsReportMode::Short, + _ => TddStatsReportMode::Full, + } + } + + const fn verbose(self) -> u8 { + self as u8 + } +} + +#[cfg(feature = "tdd-stats")] +fn tdd_stats_report_mode() -> TddStatsReportMode { + match std::env::var(EnvVars::TY_TDD_STATS_REPORT) { + Ok(raw) => { + let raw = raw.trim(); + if raw.eq_ignore_ascii_case("short") { + return TddStatsReportMode::Short; + } + + if raw.eq_ignore_ascii_case("full") { + return TddStatsReportMode::Full; + } + + match raw.parse::() { + Ok(verbose) if verbose <= TddStatsReportMode::MAX_LEVEL => { + TddStatsReportMode::from_verbose(verbose) + } + Ok(verbose) => { + tracing::warn!( + "Value for `TY_TDD_STATS_REPORT` is capped at {} (full), got `{verbose}`.", + TddStatsReportMode::MAX_LEVEL + ); + TddStatsReportMode::Full + } + Err(_) => { + tracing::warn!( + "Unknown value for `TY_TDD_STATS_REPORT`: `{raw}`. Valid values are `0`, `1`, `2`, `short`, and `full`." + ); + TddStatsReportMode::Off + } + } + } + Err(_) => TddStatsReportMode::Off, + } +} + +#[cfg(feature = "tdd-stats")] +fn write_tdd_stats_report(db: &ProjectDatabase, _printer: Printer) { + use ty_python_semantic::semantic_index::tdd_stats::FileTddStatsSummary; + + enum FileSummaryIter<'a> { + Full(std::slice::Iter<'a, FileTddStatsSummary>), + Short(std::iter::Take>), + } + + impl<'a> Iterator for FileSummaryIter<'a> { + type Item = &'a FileTddStatsSummary; + + fn next(&mut self) -> Option { + match self { + FileSummaryIter::Full(iter) => iter.next(), + FileSummaryIter::Short(iter) => iter.next(), + } + } + } + + let mode = tdd_stats_report_mode(); + if matches!(mode, TddStatsReportMode::Off) { + return; + } + + let project = db.project(); + let files = project.files(db); + + let mut summaries = Vec::new(); + for file in &files { + if !project.should_check_file(db, file) { + continue; + } + let summary = tdd_stats_for_file(db, file); + if summary.total_roots > 0 { + summaries.push(summary); + } + } + + if summaries.is_empty() { + return; + } + + summaries.sort(); + let max_root_nodes = summaries + .iter() + .map(|summary| summary.max_interior_nodes) + .max() + .unwrap_or(0); + let tdd_pool_nodes: usize = summaries.iter().map(|summary| summary.tdd_pool_nodes).sum(); + let tdd_pool_roots: usize = summaries.iter().map(|summary| summary.tdd_pool_roots).sum(); + let reachability_roots: usize = summaries + .iter() + .map(|summary| summary.reachability_roots) + .sum(); + let reachability_interior_nodes: usize = summaries + .iter() + .map(|summary| summary.reachability_interior_nodes) + .sum(); + let reachability_max_depth = summaries + .iter() + .map(|summary| summary.reachability_max_depth) + .max() + .unwrap_or(0); + let narrowing_roots: usize = summaries + .iter() + .map(|summary| summary.narrowing_roots) + .sum(); + let narrowing_interior_nodes: usize = summaries + .iter() + .map(|summary| summary.narrowing_interior_nodes) + .sum(); + let narrowing_max_depth = summaries + .iter() + .map(|summary| summary.narrowing_max_depth) + .max() + .unwrap_or(0); + + tracing::info!( + target: "ty.tdd_stats", + verbose = mode.verbose(), + files = summaries.len(), + max_root_nodes, + tdd_pool_roots, + tdd_pool_nodes, + reachability_roots, + reachability_nodes = reachability_interior_nodes, + reachability_max_depth, + narrowing_roots, + narrowing_nodes = narrowing_interior_nodes, + narrowing_max_depth, + "tdd_stats_summary" + ); + + let is_full = mode.verbose() >= TddStatsReportMode::Full.verbose(); + let file_summaries = if is_full { + FileSummaryIter::Full(summaries.iter()) + } else { + FileSummaryIter::Short(summaries.iter().take(20)) + }; + + for summary in file_summaries { + tracing::info!( + target: "ty.tdd_stats", + file = %summary.file_path, + max_root_nodes = summary.max_interior_nodes, + tdd_pool_roots = summary.tdd_pool_roots, + tdd_pool_nodes = summary.tdd_pool_nodes, + reachability_roots = summary.reachability_roots, + reachability_nodes = summary.reachability_interior_nodes, + reachability_max_depth = summary.reachability_max_depth, + narrowing_roots = summary.narrowing_roots, + narrowing_nodes = summary.narrowing_interior_nodes, + narrowing_max_depth = summary.narrowing_max_depth, + "tdd_stats_file" + ); + + if is_full { + let mut scopes = summary.scopes.clone(); + scopes.sort(); + for scope in &scopes { + let mut histogram = String::new(); + for bin in &scope.histogram { + if !histogram.is_empty() { + histogram.push(' '); + } + let _ = write!(&mut histogram, "{}=>{}", bin.interior_nodes, bin.count); + } + tracing::info!( + target: "ty.tdd_stats", + file = %summary.file_path, + scope_id = scope.scope_id.as_u32(), + root_count = scope.root_count, + total_nodes = scope.total_interior_nodes, + max_root_nodes = scope.max_interior_nodes, + tdd_pool_roots = scope.tdd_pool_roots, + tdd_pool_nodes = scope.tdd_pool_nodes, + reachability_max_depth = scope.reachability_max_depth, + narrowing_max_depth = scope.narrowing_max_depth, + node_histogram = %histogram, + "tdd_stats_scope" + ); + + let mut hot_nodes = scope.hot_nodes.clone(); + hot_nodes.sort(); + for hot in hot_nodes.iter().take(20) { + tracing::info!( + target: "ty.tdd_stats", + file = %summary.file_path, + scope_id = scope.scope_id.as_u32(), + kind = hot.kind, + subtree_nodes = hot.subtree_interior_nodes, + root_uses = hot.root_uses, + score = hot.score, + roots = %hot.sample_roots.join(" | "), + "tdd_stats_hot_node" + ); + } + } + } + } +} + #[derive(Copy, Clone)] pub enum ExitStatus { /// Checking was successful and there were no errors. diff --git a/crates/ty_python_semantic/Cargo.toml b/crates/ty_python_semantic/Cargo.toml index 08e1f7546d646..e3219b5fe1ebd 100644 --- a/crates/ty_python_semantic/Cargo.toml +++ b/crates/ty_python_semantic/Cargo.toml @@ -73,6 +73,7 @@ quickcheck_macros = { workspace = true } schemars = ["dep:schemars", "dep:serde_json"] serde = ["ruff_db/serde", "dep:serde", "ruff_python_ast/serde"] testing = [] +tdd-stats = [] [[test]] name = "mdtest" diff --git a/crates/ty_python_semantic/src/node_key.rs b/crates/ty_python_semantic/src/node_key.rs index a93931294b228..787ffa50e855f 100644 --- a/crates/ty_python_semantic/src/node_key.rs +++ b/crates/ty_python_semantic/src/node_key.rs @@ -3,7 +3,7 @@ use ruff_python_ast::{HasNodeIndex, NodeIndex}; use crate::ast_node_ref::AstNodeRef; /// Compact key for a node for use in a hash map. -#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, get_size2::GetSize)] +#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, get_size2::GetSize)] pub(super) struct NodeKey(NodeIndex); impl NodeKey { @@ -17,4 +17,9 @@ impl NodeKey { pub(super) fn from_node_ref(node_ref: &AstNodeRef) -> Self { NodeKey(node_ref.index()) } + + #[cfg(feature = "tdd-stats")] + pub(super) fn node_index(self) -> NodeIndex { + self.0 + } } diff --git a/crates/ty_python_semantic/src/semantic_index.rs b/crates/ty_python_semantic/src/semantic_index.rs index fefa9be917410..d3c08965aa89d 100644 --- a/crates/ty_python_semantic/src/semantic_index.rs +++ b/crates/ty_python_semantic/src/semantic_index.rs @@ -40,6 +40,8 @@ mod re_exports; mod reachability_constraints; pub(crate) mod scope; pub(crate) mod symbol; +#[cfg(feature = "tdd-stats")] +pub mod tdd_stats; mod use_def; pub(crate) use self::use_def::{ @@ -95,6 +97,175 @@ pub(crate) fn use_def_map<'db>(db: &'db dyn Db, scope: ScopeId<'db>) -> Arc crate::semantic_index::tdd_stats::FileTddStatsSummary { + use ruff_db::parsed::ParsedModuleRef; + use ruff_db::source::{line_index, source_text}; + use ruff_text_size::Ranged; + + use crate::semantic_index::tdd_stats::{ + FileTddStatsSummary, ScopeTddStatsSummary, TddHotNodeSummary, TddRootKind, TddRootRef, + TddRootSource, + }; + + fn format_root_ref( + db: &dyn Db, + file: File, + parsed: &ParsedModuleRef, + ast_ids: &crate::semantic_index::ast_ids::AstIds, + root: TddRootRef, + ) -> String { + let format_node = |node: crate::node_key::NodeKey| -> Option { + let node_index = node.node_index(); + node_index.as_u32()?; + let node = parsed.get_by_index(node_index); + let range = node.range(); + let line_column = + line_index(db, file).line_column(range.start(), &source_text(db, file)); + Some(format!("{}:{}", line_column.line, line_column.column)) + }; + + match root.source { + TddRootSource::Node(node) => { + format_node(node).unwrap_or_else(|| format!("unknown(node={node:?})")) + } + TddRootSource::Use { use_id, binding } => ast_ids + .node_key_for_use(use_id) + .and_then(format_node) + .unwrap_or_else(|| format!("unknown(binding={binding})")), + TddRootSource::EnclosingSnapshot { + snapshot_id, + binding, + } => { + let binding = binding + .map(|binding| format!(", binding={binding}")) + .unwrap_or_default(); + format!("unknown(snapshot={}{binding})", snapshot_id.as_u32(),) + } + } + } + + let index = semantic_index(db, file); + let parsed = parsed_module(db, file).load(db); + let mut scopes = Vec::new(); + for (scope_id, use_def) in index.use_def_maps.iter_enumerated() { + let ast_ids = index.ast_ids(scope_id); + let report = use_def.tdd_stats_report(); + if report.roots.is_empty() { + continue; + } + let root_count = report.roots.len(); + let mut total_interior_nodes = 0; + let mut max_interior_nodes = 0; + let reachability_roots = report.reachability_roots; + let mut reachability_interior_nodes = 0; + let mut reachability_max_depth = 0; + let narrowing_roots = report.narrowing_roots; + let mut narrowing_interior_nodes = 0; + let mut narrowing_max_depth = 0; + for root in &report.roots { + total_interior_nodes += root.interior_nodes; + max_interior_nodes = max_interior_nodes.max(root.interior_nodes); + match root.root.kind { + TddRootKind::NodeReachability => { + reachability_interior_nodes += root.interior_nodes; + reachability_max_depth = reachability_max_depth.max(root.max_depth); + } + TddRootKind::NarrowingConstraint => { + narrowing_interior_nodes += root.interior_nodes; + narrowing_max_depth = narrowing_max_depth.max(root.max_depth); + } + } + } + let mut hot_nodes = Vec::with_capacity(report.hot_nodes.len()); + for hot in report.hot_nodes { + let mut sample_roots = hot + .sample_roots + .into_iter() + .map(|root| format_root_ref(db, file, &parsed, ast_ids, root)) + .collect::>(); + sample_roots.sort(); + sample_roots.dedup(); + hot_nodes.push(TddHotNodeSummary { + kind: hot.kind.as_str(), + constraint_id: hot.constraint, + predicate_id: hot.predicate, + subtree_interior_nodes: hot.subtree_interior_nodes, + root_uses: hot.root_uses, + score: hot.score, + sample_roots, + }); + } + hot_nodes.sort(); + scopes.push(ScopeTddStatsSummary { + scope_id, + root_count, + total_interior_nodes, + max_interior_nodes, + tdd_pool_nodes: report.tdd_pool_nodes, + tdd_pool_roots: report.tdd_pool_roots, + reachability_roots, + reachability_interior_nodes, + reachability_max_depth, + narrowing_roots, + narrowing_interior_nodes, + narrowing_max_depth, + histogram: report.histogram, + hot_nodes, + }); + } + scopes.sort_unstable_by_key(|scope| scope.scope_id.as_u32()); + + let total_roots = scopes.iter().map(|scope| scope.root_count).sum(); + let total_interior_nodes = scopes.iter().map(|scope| scope.total_interior_nodes).sum(); + let reachability_roots = scopes.iter().map(|scope| scope.reachability_roots).sum(); + let reachability_interior_nodes = scopes + .iter() + .map(|scope| scope.reachability_interior_nodes) + .sum(); + let narrowing_roots = scopes.iter().map(|scope| scope.narrowing_roots).sum(); + let narrowing_interior_nodes = scopes + .iter() + .map(|scope| scope.narrowing_interior_nodes) + .sum(); + let tdd_pool_nodes = scopes.iter().map(|scope| scope.tdd_pool_nodes).sum(); + let tdd_pool_roots = scopes.iter().map(|scope| scope.tdd_pool_roots).sum(); + let reachability_max_depth = scopes + .iter() + .map(|scope| scope.reachability_max_depth) + .max() + .unwrap_or(0); + let narrowing_max_depth = scopes + .iter() + .map(|scope| scope.narrowing_max_depth) + .max() + .unwrap_or(0); + let max_interior_nodes = scopes + .iter() + .map(|scope| scope.max_interior_nodes) + .max() + .unwrap_or(0); + + FileTddStatsSummary { + file_path: file.path(db).clone(), + scopes, + total_roots, + total_interior_nodes, + max_interior_nodes, + tdd_pool_nodes, + tdd_pool_roots, + reachability_roots, + reachability_interior_nodes, + reachability_max_depth, + narrowing_roots, + narrowing_interior_nodes, + narrowing_max_depth, + } +} + /// Returns all attribute assignments (and their method scope IDs) with a symbol name matching /// the one given for a specific class body scope. /// diff --git a/crates/ty_python_semantic/src/semantic_index/ast_ids.rs b/crates/ty_python_semantic/src/semantic_index/ast_ids.rs index 5b8e83a2b0e4d..9e8c722d3f425 100644 --- a/crates/ty_python_semantic/src/semantic_index/ast_ids.rs +++ b/crates/ty_python_semantic/src/semantic_index/ast_ids.rs @@ -5,6 +5,8 @@ use ruff_python_ast as ast; use ruff_python_ast::ExprRef; use crate::Db; +#[cfg(feature = "tdd-stats")] +use crate::node_key::NodeKey; use crate::semantic_index::ast_ids::node_key::ExpressionNodeKey; use crate::semantic_index::scope::ScopeId; use crate::semantic_index::semantic_index; @@ -28,12 +30,22 @@ use crate::semantic_index::semantic_index; pub(crate) struct AstIds { /// Maps expressions which "use" a place (that is, [`ast::ExprName`], [`ast::ExprAttribute`] or [`ast::ExprSubscript`]) to a use id. uses_map: FxHashMap, + #[cfg(feature = "tdd-stats")] + use_nodes: Vec, } impl AstIds { fn use_id(&self, key: impl Into) -> ScopedUseId { self.uses_map[&key.into()] } + + #[cfg(feature = "tdd-stats")] + pub(super) fn node_key_for_use(&self, use_id: ScopedUseId) -> Option { + self.use_nodes + .get(use_id.as_u32() as usize) + .copied() + .map(ExpressionNodeKey::node_key) + } } fn ast_ids<'db>(db: &'db dyn Db, scope: ScopeId) -> &'db AstIds { @@ -42,7 +54,7 @@ fn ast_ids<'db>(db: &'db dyn Db, scope: ScopeId) -> &'db AstIds { /// Uniquely identifies a use of a name in a [`crate::semantic_index::FileScopeId`]. #[newtype_index] -#[derive(get_size2::GetSize)] +#[derive(get_size2::GetSize, PartialOrd, Ord)] pub struct ScopedUseId; pub trait HasScopedUseId { @@ -88,23 +100,32 @@ impl HasScopedUseId for ast::ExprRef<'_> { #[derive(Debug, Default)] pub(super) struct AstIdsBuilder { uses_map: FxHashMap, + #[cfg(feature = "tdd-stats")] + use_nodes: Vec, } impl AstIdsBuilder { /// Adds `expr` to the use ids map and returns its id. pub(super) fn record_use(&mut self, expr: impl Into) -> ScopedUseId { let use_id = self.uses_map.len().into(); + let expr = expr.into(); - self.uses_map.insert(expr.into(), use_id); + self.uses_map.insert(expr, use_id); + #[cfg(feature = "tdd-stats")] + self.use_nodes.push(expr); use_id } pub(super) fn finish(mut self) -> AstIds { self.uses_map.shrink_to_fit(); + #[cfg(feature = "tdd-stats")] + self.use_nodes.shrink_to_fit(); AstIds { uses_map: self.uses_map, + #[cfg(feature = "tdd-stats")] + use_nodes: self.use_nodes, } } } @@ -118,6 +139,13 @@ pub(crate) mod node_key { #[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, salsa::Update, get_size2::GetSize)] pub(crate) struct ExpressionNodeKey(NodeKey); + impl ExpressionNodeKey { + #[cfg(feature = "tdd-stats")] + pub(super) fn node_key(self) -> NodeKey { + self.0 + } + } + impl From> for ExpressionNodeKey { fn from(value: ast::ExprRef<'_>) -> Self { Self(NodeKey::from_node(value)) @@ -147,4 +175,10 @@ pub(crate) mod node_key { Self(NodeKey::from_node(value)) } } + + impl From for ExpressionNodeKey { + fn from(value: NodeKey) -> Self { + Self(value) + } + } } diff --git a/crates/ty_python_semantic/src/semantic_index/predicate.rs b/crates/ty_python_semantic/src/semantic_index/predicate.rs index cb0519e6ca674..26fddad6063ce 100644 --- a/crates/ty_python_semantic/src/semantic_index/predicate.rs +++ b/crates/ty_python_semantic/src/semantic_index/predicate.rs @@ -33,6 +33,11 @@ impl ScopedPredicateId { fn is_terminal(self) -> bool { self >= Self::SMALLEST_TERMINAL } + + #[cfg(feature = "tdd-stats")] + pub(crate) fn as_u32(self) -> u32 { + self.0 + } } impl Idx for ScopedPredicateId { diff --git a/crates/ty_python_semantic/src/semantic_index/reachability_constraints.rs b/crates/ty_python_semantic/src/semantic_index/reachability_constraints.rs index 5761e95249be1..18dcf8dc29b3c 100644 --- a/crates/ty_python_semantic/src/semantic_index/reachability_constraints.rs +++ b/crates/ty_python_semantic/src/semantic_index/reachability_constraints.rs @@ -197,6 +197,8 @@ use std::cmp::Ordering; use ruff_index::{Idx, IndexVec}; use rustc_hash::FxHashMap; +#[cfg(feature = "tdd-stats")] +use rustc_hash::FxHashSet; use crate::Db; use crate::dunder_all::dunder_all_names; @@ -288,7 +290,7 @@ impl ScopedReachabilityConstraintId { self.0 >= SMALLEST_TERMINAL.0 } - fn as_u32(self) -> u32 { + pub(crate) fn as_u32(self) -> u32 { self.0 } } @@ -750,6 +752,12 @@ fn accumulate_constraint<'db>( } impl ReachabilityConstraints { + /// Returns the number of interior nodes in this scope's reduced TDD pool. + #[cfg(feature = "tdd-stats")] + pub(crate) fn pool_interior_node_count(&self) -> usize { + self.used_interiors.len() + } + /// Look up an interior node by its constraint ID. fn get_interior_node(&self, id: ScopedReachabilityConstraintId) -> InteriorNode { debug_assert!(!id.is_terminal()); @@ -762,6 +770,90 @@ impl ReachabilityConstraints { self.used_interiors[index] } + /// Returns the number of unique interior TDD nodes reachable from `root`. + #[cfg(feature = "tdd-stats")] + pub(crate) fn interior_node_count(&self, root: ScopedReachabilityConstraintId) -> usize { + if root.is_terminal() { + return 0; + } + + let mut seen = FxHashSet::default(); + let mut stack = vec![root]; + while let Some(id) = stack.pop() { + if id.is_terminal() || !seen.insert(id) { + continue; + } + let node = self.get_interior_node(id); + stack.push(node.if_true); + stack.push(node.if_ambiguous); + stack.push(node.if_false); + } + + seen.len() + } + + /// Returns all unique interior TDD nodes reachable from `root`. + #[cfg(feature = "tdd-stats")] + pub(crate) fn interior_nodes( + &self, + root: ScopedReachabilityConstraintId, + ) -> Vec { + if root.is_terminal() { + return Vec::new(); + } + + let mut seen = FxHashSet::default(); + let mut stack = vec![root]; + let mut nodes = Vec::new(); + while let Some(id) = stack.pop() { + if id.is_terminal() || !seen.insert(id) { + continue; + } + nodes.push(id); + let node = self.get_interior_node(id); + stack.push(node.if_true); + stack.push(node.if_ambiguous); + stack.push(node.if_false); + } + + nodes.sort_unstable_by_key(|id| id.as_u32()); + nodes + } + + /// Returns the maximum interior depth reachable from `root`. + /// + /// Terminal constraints have depth `0`. An interior node has depth + /// `1 + max(depth(if_true), depth(if_ambiguous), depth(if_false))`. + #[cfg(feature = "tdd-stats")] + pub(crate) fn max_depth(&self, root: ScopedReachabilityConstraintId) -> usize { + fn max_depth_inner( + constraints: &ReachabilityConstraints, + id: ScopedReachabilityConstraintId, + memo: &mut FxHashMap, + ) -> usize { + if id.is_terminal() { + return 0; + } + if let Some(depth) = memo.get(&id) { + return *depth; + } + let node = constraints.get_interior_node(id); + let depth = 1 + max_depth_inner(constraints, node.if_true, memo) + .max(max_depth_inner(constraints, node.if_ambiguous, memo)) + .max(max_depth_inner(constraints, node.if_false, memo)); + memo.insert(id, depth); + depth + } + + let mut memo = FxHashMap::default(); + max_depth_inner(self, root, &mut memo) + } + + #[cfg(feature = "tdd-stats")] + pub(crate) fn interior_atom(&self, id: ScopedReachabilityConstraintId) -> ScopedPredicateId { + self.get_interior_node(id).atom + } + /// Narrow a type by walking a TDD narrowing constraint. /// /// The TDD represents a ternary formula over predicates that encodes which predicates @@ -927,21 +1019,7 @@ impl ReachabilityConstraints { ALWAYS_TRUE => return Truthiness::AlwaysTrue, AMBIGUOUS => return Truthiness::Ambiguous, ALWAYS_FALSE => return Truthiness::AlwaysFalse, - _ => { - // `id` gives us the index of this node in the IndexVec that we used when - // constructing this BDD. When finalizing the builder, we threw away any - // interior nodes that weren't marked as used. The `used_indices` bit vector - // lets us verify that this node was marked as used, and the rank of that bit - // in the bit vector tells us where this node lives in the "condensed" - // `used_interiors` vector. - let raw_index = id.as_u32() as usize; - debug_assert!( - self.used_indices.get_bit(raw_index).unwrap_or(false), - "all used reachability constraints should have been marked as used", - ); - let index = self.used_indices.rank(raw_index) as usize; - self.used_interiors[index] - } + _ => self.get_interior_node(id), }; let predicate = &predicates[node.atom]; match Self::analyze_single(db, predicate) { diff --git a/crates/ty_python_semantic/src/semantic_index/tdd_stats.rs b/crates/ty_python_semantic/src/semantic_index/tdd_stats.rs new file mode 100644 index 0000000000000..65b4707df6a3b --- /dev/null +++ b/crates/ty_python_semantic/src/semantic_index/tdd_stats.rs @@ -0,0 +1,328 @@ +use std::cmp::Ordering; +use std::collections::BTreeMap; + +use ruff_db::files::FilePath; + +use crate::node_key::NodeKey; +use crate::semantic_index::ast_ids::ScopedUseId; +use crate::semantic_index::predicate::ScopedPredicateId; +use crate::semantic_index::reachability_constraints::ScopedReachabilityConstraintId; +use crate::semantic_index::{FileScopeId, ScopedEnclosingSnapshotId}; + +/// TDD root that we want to measure. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(crate) enum TddRootKind { + NodeReachability, + NarrowingConstraint, +} + +impl TddRootKind { + pub(crate) const fn as_str(self) -> &'static str { + match self { + Self::NodeReachability => "reachability", + Self::NarrowingConstraint => "narrowing", + } + } +} + +/// Source location for a measured root. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(crate) enum TddRootSource { + Node(NodeKey), + Use { + use_id: ScopedUseId, + binding: u32, + }, + EnclosingSnapshot { + snapshot_id: ScopedEnclosingSnapshotId, + binding: Option, + }, +} + +/// Debug reference to a measured root. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub(crate) struct TddRootRef { + pub(crate) kind: TddRootKind, + pub(crate) source: TddRootSource, + pub(crate) constraint: ScopedReachabilityConstraintId, +} + +/// Size stats for one root constraint. +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct TddRootStat { + pub(crate) root: TddRootRef, + pub(crate) interior_nodes: usize, + pub(crate) max_depth: usize, +} + +/// Hot interior node aggregated across multiple roots. +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct TddHotNodeStat { + pub(crate) kind: TddRootKind, + pub(crate) constraint: ScopedReachabilityConstraintId, + pub(crate) predicate: ScopedPredicateId, + pub(crate) subtree_interior_nodes: usize, + pub(crate) root_uses: usize, + pub(crate) score: usize, + pub(crate) sample_roots: Vec, +} + +impl Ord for TddRootRef { + fn cmp(&self, other: &Self) -> Ordering { + self.kind + .cmp(&other.kind) + .then_with(|| self.constraint.as_u32().cmp(&other.constraint.as_u32())) + .then_with(|| self.source.cmp(&other.source)) + } +} + +impl PartialOrd for TddRootRef { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for TddRootStat { + fn cmp(&self, other: &Self) -> Ordering { + other + .interior_nodes + .cmp(&self.interior_nodes) + .then_with(|| other.max_depth.cmp(&self.max_depth)) + .then_with(|| self.root.cmp(&other.root)) + } +} + +impl PartialOrd for TddRootStat { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for TddHotNodeStat { + fn cmp(&self, other: &Self) -> Ordering { + other + .score + .cmp(&self.score) + .then_with(|| { + other + .subtree_interior_nodes + .cmp(&self.subtree_interior_nodes) + }) + .then_with(|| other.root_uses.cmp(&self.root_uses)) + .then_with(|| self.kind.cmp(&other.kind)) + .then_with(|| self.constraint.as_u32().cmp(&other.constraint.as_u32())) + .then_with(|| self.predicate.as_u32().cmp(&other.predicate.as_u32())) + .then_with(|| self.sample_roots.cmp(&other.sample_roots)) + } +} + +impl PartialOrd for TddHotNodeStat { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +/// Histogram bucket keyed by exact interior-node count. +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct TddHistogramBin { + pub interior_nodes: usize, + pub count: usize, +} + +/// Aggregate report for TDD size debugging. +#[derive(Debug, Default, Clone, PartialEq, Eq)] +pub(crate) struct TddStatsReport { + pub(crate) roots: Vec, + pub(crate) histogram: Vec, + pub(crate) hot_nodes: Vec, + pub(crate) tdd_pool_nodes: usize, + pub(crate) tdd_pool_roots: usize, + pub(crate) reachability_roots: usize, + pub(crate) narrowing_roots: usize, +} + +impl TddStatsReport { + pub(crate) fn from_roots( + roots: Vec, + hot_nodes: Vec, + tdd_pool_nodes: usize, + tdd_pool_roots: usize, + reachability_roots: usize, + narrowing_roots: usize, + ) -> Self { + let mut by_size: BTreeMap = BTreeMap::new(); + for stat in &roots { + *by_size.entry(stat.interior_nodes).or_default() += 1; + } + let histogram = by_size + .into_iter() + .map(|(interior_nodes, count)| TddHistogramBin { + interior_nodes, + count, + }) + .collect(); + Self { + roots, + histogram, + hot_nodes, + tdd_pool_nodes, + tdd_pool_roots, + reachability_roots, + narrowing_roots, + } + } +} + +/// Public hot-node summary used by `ty` for reporting. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct TddHotNodeSummary { + pub kind: &'static str, + pub(crate) constraint_id: ScopedReachabilityConstraintId, + pub(crate) predicate_id: ScopedPredicateId, + pub subtree_interior_nodes: usize, + pub root_uses: usize, + pub score: usize, + pub sample_roots: Vec, +} + +/// Public, file-level summary used by `ty` for reporting. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct FileTddStatsSummary { + pub file_path: FilePath, + pub scopes: Vec, + pub total_roots: usize, + pub total_interior_nodes: usize, + pub max_interior_nodes: usize, + pub tdd_pool_nodes: usize, + pub tdd_pool_roots: usize, + pub reachability_roots: usize, + pub reachability_interior_nodes: usize, + pub reachability_max_depth: usize, + pub narrowing_roots: usize, + pub narrowing_interior_nodes: usize, + pub narrowing_max_depth: usize, +} + +/// Public, scope-level summary used by `ty` for reporting. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ScopeTddStatsSummary { + pub scope_id: FileScopeId, + pub root_count: usize, + pub total_interior_nodes: usize, + pub max_interior_nodes: usize, + pub tdd_pool_nodes: usize, + pub tdd_pool_roots: usize, + pub reachability_roots: usize, + pub reachability_interior_nodes: usize, + pub reachability_max_depth: usize, + pub narrowing_roots: usize, + pub narrowing_interior_nodes: usize, + pub narrowing_max_depth: usize, + pub histogram: Vec, + pub hot_nodes: Vec, +} + +impl Ord for TddHotNodeSummary { + fn cmp(&self, other: &Self) -> Ordering { + other + .score + .cmp(&self.score) + .then_with(|| { + other + .subtree_interior_nodes + .cmp(&self.subtree_interior_nodes) + }) + .then_with(|| other.root_uses.cmp(&self.root_uses)) + .then_with(|| self.kind.cmp(other.kind)) + .then_with(|| { + self.constraint_id + .as_u32() + .cmp(&other.constraint_id.as_u32()) + }) + .then_with(|| self.predicate_id.cmp(&other.predicate_id)) + .then_with(|| self.sample_roots.cmp(&other.sample_roots)) + } +} + +impl PartialOrd for TddHotNodeSummary { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for ScopeTddStatsSummary { + fn cmp(&self, other: &Self) -> Ordering { + other + .total_interior_nodes + .cmp(&self.total_interior_nodes) + .then_with(|| other.max_interior_nodes.cmp(&self.max_interior_nodes)) + .then_with(|| other.tdd_pool_nodes.cmp(&self.tdd_pool_nodes)) + .then_with(|| other.tdd_pool_roots.cmp(&self.tdd_pool_roots)) + .then_with(|| other.root_count.cmp(&self.root_count)) + .then_with(|| { + other + .reachability_interior_nodes + .cmp(&self.reachability_interior_nodes) + }) + .then_with(|| { + other + .reachability_max_depth + .cmp(&self.reachability_max_depth) + }) + .then_with(|| { + other + .narrowing_interior_nodes + .cmp(&self.narrowing_interior_nodes) + }) + .then_with(|| other.narrowing_max_depth.cmp(&self.narrowing_max_depth)) + .then_with(|| other.reachability_roots.cmp(&self.reachability_roots)) + .then_with(|| other.narrowing_roots.cmp(&self.narrowing_roots)) + .then_with(|| self.scope_id.as_u32().cmp(&other.scope_id.as_u32())) + .then_with(|| self.histogram.cmp(&other.histogram)) + .then_with(|| self.hot_nodes.cmp(&other.hot_nodes)) + } +} + +impl PartialOrd for ScopeTddStatsSummary { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for FileTddStatsSummary { + fn cmp(&self, other: &Self) -> Ordering { + other + .total_interior_nodes + .cmp(&self.total_interior_nodes) + .then_with(|| other.max_interior_nodes.cmp(&self.max_interior_nodes)) + .then_with(|| other.tdd_pool_nodes.cmp(&self.tdd_pool_nodes)) + .then_with(|| other.tdd_pool_roots.cmp(&self.tdd_pool_roots)) + .then_with(|| other.total_roots.cmp(&self.total_roots)) + .then_with(|| { + other + .reachability_interior_nodes + .cmp(&self.reachability_interior_nodes) + }) + .then_with(|| { + other + .reachability_max_depth + .cmp(&self.reachability_max_depth) + }) + .then_with(|| { + other + .narrowing_interior_nodes + .cmp(&self.narrowing_interior_nodes) + }) + .then_with(|| other.narrowing_max_depth.cmp(&self.narrowing_max_depth)) + .then_with(|| other.reachability_roots.cmp(&self.reachability_roots)) + .then_with(|| other.narrowing_roots.cmp(&self.narrowing_roots)) + .then_with(|| self.file_path.as_str().cmp(other.file_path.as_str())) + .then_with(|| self.scopes.cmp(&other.scopes)) + } +} + +impl PartialOrd for FileTddStatsSummary { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} diff --git a/crates/ty_python_semantic/src/semantic_index/use_def.rs b/crates/ty_python_semantic/src/semantic_index/use_def.rs index 4a32870bafa4a..6a7fed8c67015 100644 --- a/crates/ty_python_semantic/src/semantic_index/use_def.rs +++ b/crates/ty_python_semantic/src/semantic_index/use_def.rs @@ -418,6 +418,186 @@ impl<'db> UseDefMap<'db> { .may_be_true() } + /// Returns a debug report describing TDD sizes for recorded node reachability roots. + #[cfg(feature = "tdd-stats")] + pub(crate) fn tdd_stats_report(&self) -> crate::semantic_index::tdd_stats::TddStatsReport { + use crate::semantic_index::tdd_stats::{ + TddHotNodeStat, TddRootKind, TddRootRef, TddRootSource, TddRootStat, TddStatsReport, + }; + use rustc_hash::FxHashSet; + + let estimated_narrowing_roots: usize = self + .bindings_by_use + .iter() + .map(|bindings| bindings.iter().len()) + .sum(); + let estimated_snapshot_roots: usize = self + .enclosing_snapshots + .iter() + .map(|snapshot| match snapshot { + EnclosingSnapshot::Constraint(_) => 1, + EnclosingSnapshot::Bindings(bindings) => bindings.iter().len(), + }) + .sum(); + let mut roots = Vec::with_capacity( + self.node_reachability.len() + estimated_narrowing_roots + estimated_snapshot_roots, + ); + let mut seen_roots: FxHashSet = FxHashSet::default(); + let mut tdd_pool_roots: FxHashSet = FxHashSet::default(); + let mut reachability_roots: FxHashSet = + FxHashSet::default(); + let mut narrowing_roots: FxHashSet = FxHashSet::default(); + if self.end_of_scope_reachability != ScopedReachabilityConstraintId::ALWAYS_TRUE + && self.end_of_scope_reachability != ScopedReachabilityConstraintId::ALWAYS_FALSE + && self.end_of_scope_reachability != ScopedReachabilityConstraintId::AMBIGUOUS + { + tdd_pool_roots.insert(self.end_of_scope_reachability); + } + + let mut push_root = |root: TddRootRef| { + if !seen_roots.insert(root) { + return; + } + if root.constraint != ScopedReachabilityConstraintId::ALWAYS_TRUE + && root.constraint != ScopedReachabilityConstraintId::ALWAYS_FALSE + && root.constraint != ScopedReachabilityConstraintId::AMBIGUOUS + { + tdd_pool_roots.insert(root.constraint); + } + match root.kind { + TddRootKind::NodeReachability => { + reachability_roots.insert(root.constraint); + } + TddRootKind::NarrowingConstraint => { + narrowing_roots.insert(root.constraint); + } + } + roots.push(TddRootStat { + root, + interior_nodes: self + .reachability_constraints + .interior_node_count(root.constraint), + max_depth: self.reachability_constraints.max_depth(root.constraint), + }); + }; + + for (&node, &constraint) in &self.node_reachability { + push_root(TddRootRef { + kind: TddRootKind::NodeReachability, + source: TddRootSource::Node(node), + constraint, + }); + } + for (use_id, bindings) in self.bindings_by_use.iter_enumerated() { + for binding in bindings.iter() { + let constraint = binding.narrowing_constraint; + if constraint == ScopedReachabilityConstraintId::ALWAYS_TRUE { + continue; + } + push_root(TddRootRef { + kind: TddRootKind::NarrowingConstraint, + source: TddRootSource::Use { + use_id, + binding: binding.binding.as_u32(), + }, + constraint, + }); + } + } + for (snapshot_id, snapshot) in self.enclosing_snapshots.iter_enumerated() { + match snapshot { + EnclosingSnapshot::Constraint(constraint) => { + if *constraint == ScopedReachabilityConstraintId::ALWAYS_TRUE { + continue; + } + push_root(TddRootRef { + kind: TddRootKind::NarrowingConstraint, + source: TddRootSource::EnclosingSnapshot { + snapshot_id, + binding: None, + }, + constraint: *constraint, + }); + } + EnclosingSnapshot::Bindings(bindings) => { + for binding in bindings.iter() { + let constraint = binding.narrowing_constraint; + if constraint == ScopedReachabilityConstraintId::ALWAYS_TRUE { + continue; + } + push_root(TddRootRef { + kind: TddRootKind::NarrowingConstraint, + source: TddRootSource::EnclosingSnapshot { + snapshot_id, + binding: Some(binding.binding.as_u32()), + }, + constraint, + }); + } + } + } + } + roots.shrink_to_fit(); + roots.sort(); + + let mut root_uses_by_node: FxHashMap<(TddRootKind, ScopedReachabilityConstraintId), usize> = + FxHashMap::default(); + let mut sample_roots_by_node: FxHashMap< + (TddRootKind, ScopedReachabilityConstraintId), + Vec, + > = FxHashMap::default(); + let mut subtree_size_memo: FxHashMap = + FxHashMap::default(); + + for root in &roots { + let interior_nodes = self + .reachability_constraints + .interior_nodes(root.root.constraint); + for interior in interior_nodes { + let hot_key = (root.root.kind, interior); + *root_uses_by_node.entry(hot_key).or_default() += 1; + let sample_roots = sample_roots_by_node.entry(hot_key).or_default(); + if sample_roots.len() < 5 { + sample_roots.push(root.root); + } + + subtree_size_memo + .entry(interior) + .or_insert_with(|| self.reachability_constraints.interior_node_count(interior)); + } + } + + let mut hot_nodes = Vec::with_capacity(root_uses_by_node.len()); + for ((kind, constraint), root_uses) in root_uses_by_node { + let subtree_interior_nodes = subtree_size_memo[&constraint]; + let score = subtree_interior_nodes.saturating_mul(root_uses); + let predicate = self.reachability_constraints.interior_atom(constraint); + let sample_roots = sample_roots_by_node + .remove(&(kind, constraint)) + .unwrap_or_default(); + hot_nodes.push(TddHotNodeStat { + kind, + constraint, + predicate, + subtree_interior_nodes, + root_uses, + score, + sample_roots, + }); + } + hot_nodes.shrink_to_fit(); + hot_nodes.sort(); + + TddStatsReport::from_roots( + roots, + hot_nodes, + self.reachability_constraints.pool_interior_node_count(), + tdd_pool_roots.len(), + reachability_roots.len(), + narrowing_roots.len(), + ) + } + pub(crate) fn end_of_scope_bindings( &self, place: ScopedPlaceId, @@ -667,7 +847,7 @@ impl<'db> UseDefMap<'db> { /// /// There is a unique ID for each distinct [`EnclosingSnapshotKey`] in the file. #[newtype_index] -#[derive(get_size2::GetSize)] +#[derive(get_size2::GetSize, PartialOrd, Ord)] pub(crate) struct ScopedEnclosingSnapshotId; #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, get_size2::GetSize)] diff --git a/crates/ty_static/src/env_vars.rs b/crates/ty_static/src/env_vars.rs index 8ffcb73fc5fa2..23590dfd93ddc 100644 --- a/crates/ty_static/src/env_vars.rs +++ b/crates/ty_static/src/env_vars.rs @@ -32,6 +32,16 @@ impl EnvVars { #[attr_hidden] pub const TY_MEMORY_REPORT: &'static str = "TY_MEMORY_REPORT"; + /// Control TDD stats reporting format after ty execution (requires `tdd-stats` feature). + /// + /// Accepted values: + /// + /// * `short` - emit a per-file summary through tracing target `ty.tdd_stats` + /// * `full` - emit per-file and per-scope summaries (including histograms) + /// through tracing target `ty.tdd_stats` + #[attr_hidden] + pub const TY_TDD_STATS_REPORT: &'static str = "TY_TDD_STATS_REPORT"; + /// Specifies an upper limit for the number of tasks ty is allowed to run in parallel. /// /// For example, how many files should be checked in parallel.