From 7c5874c2b9e80ceb051303f48e0e543b479c7416 Mon Sep 17 00:00:00 2001 From: Stephen Date: Fri, 15 May 2026 11:45:15 -0400 Subject: [PATCH 1/6] Add ast and hir stats to print-codegen-stats-json flag --- compiler/rustc_interface/src/passes.rs | 7 ++++ compiler/rustc_interface/src/queries.rs | 26 +++++++++++++-- compiler/rustc_passes/src/input_stats.rs | 41 ++++++++++++++++++++++++ compiler/rustc_session/src/session.rs | 4 +++ 4 files changed, 75 insertions(+), 3 deletions(-) diff --git a/compiler/rustc_interface/src/passes.rs b/compiler/rustc_interface/src/passes.rs index bcd1a52ce9dcd..ec8cbceeefe2c 100644 --- a/compiler/rustc_interface/src/passes.rs +++ b/compiler/rustc_interface/src/passes.rs @@ -406,6 +406,10 @@ fn early_lint_checks(tcx: TyCtxt<'_>, (): ()) { input_stats::print_ast_stats(tcx, krate); } + if sess.print_llvm_stats_json().is_some() { + input_stats::collect_ast_stats(tcx, krate); + } + // Needs to go *after* expansion to be able to check the results of macro expansion. sess.time("complete_gated_feature_checking", || { rustc_ast_passes::feature_gate::check_crate(krate, sess, tcx.features()); @@ -1074,6 +1078,9 @@ fn run_required_analyses(tcx: TyCtxt<'_>) { if tcx.sess.opts.unstable_opts.input_stats { rustc_passes::input_stats::print_hir_stats(tcx); } + if tcx.sess.print_llvm_stats_json().is_some() { + rustc_passes::input_stats::collect_hir_stats(tcx); + } // When using rustdoc's "jump to def" feature, it enters this code and `check_crate` // is not defined. So we need to cfg it out. #[cfg(all(not(doc), debug_assertions))] diff --git a/compiler/rustc_interface/src/queries.rs b/compiler/rustc_interface/src/queries.rs index 20ac6e258ed16..c0f9bf2221bc2 100644 --- a/compiler/rustc_interface/src/queries.rs +++ b/compiler/rustc_interface/src/queries.rs @@ -72,15 +72,35 @@ impl Linker { if let Some(out_path) = sess.print_llvm_stats_json() { let llvm_stats_json = codegen_backend.print_statistics_json(); + let frontend_stats = sess.frontend_stats.lock(); + let mut json_entries = Vec::new(); if !llvm_stats_json.is_empty() { - if let Err(e) = std::fs::write(&out_path, llvm_stats_json) { + if let Some(start) = llvm_stats_json.find('{') { + if let Some(end) = llvm_stats_json.rfind('}') { + for line in llvm_stats_json[start + 1..end].lines() { + let line = line.trim().strip_suffix(',').unwrap_or(line.trim()); + if !line.is_empty() { + json_entries.push(line.to_string()); + } + } + } + } + } + + // Append frontend stats + for (key, value) in frontend_stats.iter() { + json_entries.push(format!("{:?}: {}", key, value)); + } + + if !json_entries.is_empty() { + let final_json = format!("{{\n {}\n}}", json_entries.join(",\n ")); + if let Err(e) = std::fs::write(&out_path, final_json) { sess.dcx().err(format!("failed to write stats to {}: {}", out_path, e)); } } else { sess.dcx().warn(format!( - "requested to print LLVM statistics to JSON file {}, but the codegen backend \ - did not provide any statistics", + "requested to print statistics to JSON file {}, but no statistics were collected", out_path, )); } diff --git a/compiler/rustc_passes/src/input_stats.rs b/compiler/rustc_passes/src/input_stats.rs index 9127e4936803d..6b172cdbff4fe 100644 --- a/compiler/rustc_passes/src/input_stats.rs +++ b/compiler/rustc_passes/src/input_stats.rs @@ -197,6 +197,47 @@ impl<'k> StatCollector<'k> { _ = writeln!(s, "{prefix} {}", "=".repeat(banner_w)); eprint!("{s}"); } + fn to_json_entries(&self, prefix: &str) -> Vec<(String, usize)> { + let mut out = Vec::new(); + let mut keys: Vec<_> = self.nodes.keys().collect(); + keys.sort(); + for label in keys { + let node = &self.nodes[label]; + out.push((format!("{prefix}.{label}.count"), node.stats.count)); + let mut sub_keys: Vec<_> = node.subnodes.keys().collect(); + sub_keys.sort(); + for sub_label in sub_keys { + let subnode = &node.subnodes[sub_label]; + out.push((format!("{prefix}.{label}.{sub_label}.count"), subnode.count)); + out.push((format!("{prefix}.{label}.{sub_label}.size"), subnode.size)); + } + } + out + } +} + +pub fn collect_hir_stats(tcx: TyCtxt<'_>) { + if tcx.sess.print_llvm_stats_json().is_none() { + return; + } + let mut collector = + StatCollector { tcx: Some(tcx), nodes: FxHashMap::default(), seen: FxHashSet::default() }; + tcx.hir_walk_toplevel_module(&mut collector); + tcx.hir_walk_attributes(&mut collector); + let mut stats = tcx.sess.frontend_stats.lock(); + stats.extend(collector.to_json_entries("hir-stats")); +} + +pub fn collect_ast_stats(tcx: TyCtxt<'_>, krate: &ast::Crate) { + if tcx.sess.print_llvm_stats_json().is_none() { + return; + } + use rustc_ast::visit::Visitor; + let mut collector = + StatCollector { tcx: None, nodes: FxHashMap::default(), seen: FxHashSet::default() }; + collector.visit_crate(krate); + let mut stats = tcx.sess.frontend_stats.lock(); + stats.extend(collector.to_json_entries("ast-stats")); } // Used to avoid boilerplate for types with many variants. diff --git a/compiler/rustc_session/src/session.rs b/compiler/rustc_session/src/session.rs index 003164e8f9054..e2e12150345ed 100644 --- a/compiler/rustc_session/src/session.rs +++ b/compiler/rustc_session/src/session.rs @@ -178,6 +178,9 @@ pub struct Session { /// /// The value is the `DepNodeIndex` of the node encodes the used feature. pub used_features: Lock>, + + /// Frontend stats collected for JSON output. + pub frontend_stats: Lock>, } #[derive(Clone, Copy)] @@ -1127,6 +1130,7 @@ pub fn build_session( thin_lto_supported: true, // filled by `run_compiler` mir_opt_bisect_eval_count: AtomicUsize::new(0), used_features: Lock::default(), + frontend_stats: Lock::default(), }; validate_commandline_args_with_session_available(&sess); From 1f23106c5d75b5cfa1b3db0c5d75f6c210728003 Mon Sep 17 00:00:00 2001 From: Stephen Date: Fri, 15 May 2026 12:06:24 -0400 Subject: [PATCH 2/6] Allow to use keys as initial order doesnt matter --- compiler/rustc_passes/src/input_stats.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/compiler/rustc_passes/src/input_stats.rs b/compiler/rustc_passes/src/input_stats.rs index 6b172cdbff4fe..eebfd4bf4f8fb 100644 --- a/compiler/rustc_passes/src/input_stats.rs +++ b/compiler/rustc_passes/src/input_stats.rs @@ -199,11 +199,15 @@ impl<'k> StatCollector<'k> { } fn to_json_entries(&self, prefix: &str) -> Vec<(String, usize)> { let mut out = Vec::new(); + // We will soon sort, so the initial order does not matter. + #[allow(rustc::potential_query_instability)] let mut keys: Vec<_> = self.nodes.keys().collect(); keys.sort(); for label in keys { let node = &self.nodes[label]; out.push((format!("{prefix}.{label}.count"), node.stats.count)); + // We will soon sort, so the initial order does not matter. + #[allow(rustc::potential_query_instability)] let mut sub_keys: Vec<_> = node.subnodes.keys().collect(); sub_keys.sort(); for sub_label in sub_keys { From bdfa2b4d3ee771122ef9cccc5b726f8b8c875dc2 Mon Sep 17 00:00:00 2001 From: Stephen Date: Fri, 29 May 2026 15:33:21 -0400 Subject: [PATCH 3/6] Add more input-stats stats, use serde to parse, reserve old warnings when cannot collect backend stats --- compiler/rustc_interface/Cargo.toml | 1 + compiler/rustc_interface/src/queries.rs | 43 +++++++++++++++--------- compiler/rustc_passes/src/input_stats.rs | 10 ++++++ 3 files changed, 39 insertions(+), 15 deletions(-) diff --git a/compiler/rustc_interface/Cargo.toml b/compiler/rustc_interface/Cargo.toml index 9c115736a3d4f..50eacdd89ddf8 100644 --- a/compiler/rustc_interface/Cargo.toml +++ b/compiler/rustc_interface/Cargo.toml @@ -45,6 +45,7 @@ rustc_thread_pool = { path = "../rustc_thread_pool" } rustc_trait_selection = { path = "../rustc_trait_selection" } rustc_traits = { path = "../rustc_traits" } rustc_ty_utils = { path = "../rustc_ty_utils" } +serde_json = "1.0.59" tracing = "0.1" # tidy-alphabetical-end diff --git a/compiler/rustc_interface/src/queries.rs b/compiler/rustc_interface/src/queries.rs index c0f9bf2221bc2..8470234244574 100644 --- a/compiler/rustc_interface/src/queries.rs +++ b/compiler/rustc_interface/src/queries.rs @@ -72,31 +72,44 @@ impl Linker { if let Some(out_path) = sess.print_llvm_stats_json() { let llvm_stats_json = codegen_backend.print_statistics_json(); - let frontend_stats = sess.frontend_stats.lock(); - let mut json_entries = Vec::new(); + if llvm_stats_json.is_empty() { + sess.dcx().warn(format!( + "requested to print LLVM statistics to JSON file {}, but the codegen backend did not provide any statistics", + out_path, + )); + } + + let mut merged_stats = serde_json::Map::new(); + + // Parse LLVM stats if present if !llvm_stats_json.is_empty() { - if let Some(start) = llvm_stats_json.find('{') { - if let Some(end) = llvm_stats_json.rfind('}') { - for line in llvm_stats_json[start + 1..end].lines() { - let line = line.trim().strip_suffix(',').unwrap_or(line.trim()); - if !line.is_empty() { - json_entries.push(line.to_string()); - } - } + match serde_json::from_str::(&llvm_stats_json) { + Ok(serde_json::Value::Object(map)) => { + merged_stats = map; + } + Ok(_) => { + sess.dcx().warn("LLVM statistics JSON was not a valid JSON object"); + } + Err(e) => { + sess.dcx().warn(format!("failed to parse LLVM statistics JSON: {}", e)); } } } // Append frontend stats + let frontend_stats = sess.frontend_stats.lock(); for (key, value) in frontend_stats.iter() { - json_entries.push(format!("{:?}: {}", key, value)); + merged_stats.insert(key.clone(), serde_json::Value::Number((*value).into())); } - if !json_entries.is_empty() { - let final_json = format!("{{\n {}\n}}", json_entries.join(",\n ")); - if let Err(e) = std::fs::write(&out_path, final_json) { - sess.dcx().err(format!("failed to write stats to {}: {}", out_path, e)); + if !merged_stats.is_empty() { + if let Ok(final_json) = + serde_json::to_string_pretty(&serde_json::Value::Object(merged_stats)) + { + if let Err(e) = std::fs::write(&out_path, final_json) { + sess.dcx().err(format!("failed to write stats to {}: {}", out_path, e)); + } } } else { sess.dcx().warn(format!( diff --git a/compiler/rustc_passes/src/input_stats.rs b/compiler/rustc_passes/src/input_stats.rs index eebfd4bf4f8fb..d6abb463c94cd 100644 --- a/compiler/rustc_passes/src/input_stats.rs +++ b/compiler/rustc_passes/src/input_stats.rs @@ -206,6 +206,8 @@ impl<'k> StatCollector<'k> { for label in keys { let node = &self.nodes[label]; out.push((format!("{prefix}.{label}.count"), node.stats.count)); + out.push((format!("{prefix}.{label}.size"), node.stats.size)); + out.push((format!("{prefix}.{label}.cumulative_size"), node.stats.accum_size())); // We will soon sort, so the initial order does not matter. #[allow(rustc::potential_query_instability)] let mut sub_keys: Vec<_> = node.subnodes.keys().collect(); @@ -214,8 +216,16 @@ impl<'k> StatCollector<'k> { let subnode = &node.subnodes[sub_label]; out.push((format!("{prefix}.{label}.{sub_label}.count"), subnode.count)); out.push((format!("{prefix}.{label}.{sub_label}.size"), subnode.size)); + out.push(( + format!("{prefix}.{label}.{sub_label}.cumulative_size"), + subnode.accum_size(), + )); } } + let total_size = self.nodes.values().map(|node| node.stats.accum_size()).sum(); + let total_count = self.nodes.values().map(|node| node.stats.count).sum(); + out.push((format!("{prefix}.total.count"), total_count)); + out.push((format!("{prefix}.total.size"), total_size)); out } } From f0d5958cdc2e75c4cbeee49cc8d02948db2f261b Mon Sep 17 00:00:00 2001 From: Stephen Date: Fri, 29 May 2026 15:55:25 -0400 Subject: [PATCH 4/6] Add change to Cargo.lock --- Cargo.lock | 1 + 1 file changed, 1 insertion(+) diff --git a/Cargo.lock b/Cargo.lock index 0f6368e4497cb..0dc520e30ffcd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4233,6 +4233,7 @@ dependencies = [ "rustc_trait_selection", "rustc_traits", "rustc_ty_utils", + "serde_json", "tracing", ] From 0ba88b22aa09ba5ba442e218b424eee77c7795b2 Mon Sep 17 00:00:00 2001 From: Stephen Date: Fri, 29 May 2026 16:18:40 -0400 Subject: [PATCH 5/6] Refactor to_json_entries to not using values() --- compiler/rustc_passes/src/input_stats.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/compiler/rustc_passes/src/input_stats.rs b/compiler/rustc_passes/src/input_stats.rs index d6abb463c94cd..fe1a95e941c0b 100644 --- a/compiler/rustc_passes/src/input_stats.rs +++ b/compiler/rustc_passes/src/input_stats.rs @@ -202,9 +202,13 @@ impl<'k> StatCollector<'k> { // We will soon sort, so the initial order does not matter. #[allow(rustc::potential_query_instability)] let mut keys: Vec<_> = self.nodes.keys().collect(); - keys.sort(); + keys.sort(); // Ensure deterministic output + let mut total_size = 0; + let mut total_count = 0; for label in keys { let node = &self.nodes[label]; + total_size += node.stats.accum_size(); + total_count += node.stats.count; out.push((format!("{prefix}.{label}.count"), node.stats.count)); out.push((format!("{prefix}.{label}.size"), node.stats.size)); out.push((format!("{prefix}.{label}.cumulative_size"), node.stats.accum_size())); @@ -222,8 +226,6 @@ impl<'k> StatCollector<'k> { )); } } - let total_size = self.nodes.values().map(|node| node.stats.accum_size()).sum(); - let total_count = self.nodes.values().map(|node| node.stats.count).sum(); out.push((format!("{prefix}.total.count"), total_count)); out.push((format!("{prefix}.total.size"), total_size)); out From 54b3c5825ac1991f432fecb89d5d6e8b1a4f00a8 Mon Sep 17 00:00:00 2001 From: Stephen Date: Fri, 29 May 2026 16:18:40 -0400 Subject: [PATCH 6/6] Refactor to_json_entries to not using values() --- Cargo.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 0dc520e30ffcd..185f33ae23d14 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4233,7 +4233,7 @@ dependencies = [ "rustc_trait_selection", "rustc_traits", "rustc_ty_utils", - "serde_json", + "serde_json", "tracing", ]