about summary refs log tree commit diff
diff options
context:
space:
mode:
authorNicholas Nethercote <n.nethercote@gmail.com>2025-06-02 19:39:38 +1000
committerNicholas Nethercote <n.nethercote@gmail.com>2025-06-12 21:17:17 +1000
commit376cbc3787d2312b6b3b5db84dd1734fed1ebda6 (patch)
tree1f93fafea489fc66c36fbacd70b45931952418f2
parentbdfe1b9fb0c3f98df2a99ab96c6190542c234060 (diff)
downloadrust-376cbc3787d2312b6b3b5db84dd1734fed1ebda6.tar.gz
rust-376cbc3787d2312b6b3b5db84dd1734fed1ebda6.zip
Introduce `-Zmacro-stats`.
It collects data about macro expansions and prints them in a table after
expansion finishes. It's very useful for detecting macro bloat,
especially for proc macros.

Details:
- It measures code snippets by pretty-printing them and then measuring
  lines and bytes. This required a bunch of additional pretty-printing
  plumbing, in `rustc_ast_pretty` and `rustc_expand`.
- The measurement is done in `MacroExpander::expand_invoc`.
- The measurements are stored in `ExtCtxt::macro_stats`.
-rw-r--r--compiler/rustc_ast_pretty/src/pprust/mod.rs12
-rw-r--r--compiler/rustc_ast_pretty/src/pprust/state.rs8
-rw-r--r--compiler/rustc_ast_pretty/src/pprust/state/item.rs4
-rw-r--r--compiler/rustc_expand/src/base.rs6
-rw-r--r--compiler/rustc_expand/src/expand.rs190
-rw-r--r--compiler/rustc_expand/src/lib.rs1
-rw-r--r--compiler/rustc_expand/src/stats.rs171
-rw-r--r--compiler/rustc_interface/src/passes.rs81
-rw-r--r--compiler/rustc_interface/src/tests.rs1
-rw-r--r--compiler/rustc_session/src/options.rs2
-rw-r--r--compiler/rustc_span/src/hygiene.rs2
-rw-r--r--compiler/rustc_span/src/symbol.rs1
-rw-r--r--src/doc/unstable-book/src/compiler-flags/macro-stats.md24
-rw-r--r--tests/ui/stats/auxiliary/include.rs3
-rw-r--r--tests/ui/stats/macro-stats.rs130
-rw-r--r--tests/ui/stats/macro-stats.stderr26
16 files changed, 610 insertions, 52 deletions
diff --git a/compiler/rustc_ast_pretty/src/pprust/mod.rs b/compiler/rustc_ast_pretty/src/pprust/mod.rs
index a05e2bd6a5d..a766e2006e5 100644
--- a/compiler/rustc_ast_pretty/src/pprust/mod.rs
+++ b/compiler/rustc_ast_pretty/src/pprust/mod.rs
@@ -53,6 +53,18 @@ pub fn item_to_string(i: &ast::Item) -> String {
     State::new().item_to_string(i)
 }
 
+pub fn assoc_item_to_string(i: &ast::AssocItem) -> String {
+    State::new().assoc_item_to_string(i)
+}
+
+pub fn foreign_item_to_string(i: &ast::ForeignItem) -> String {
+    State::new().foreign_item_to_string(i)
+}
+
+pub fn stmt_to_string(s: &ast::Stmt) -> String {
+    State::new().stmt_to_string(s)
+}
+
 pub fn path_to_string(p: &ast::Path) -> String {
     State::new().path_to_string(p)
 }
diff --git a/compiler/rustc_ast_pretty/src/pprust/state.rs b/compiler/rustc_ast_pretty/src/pprust/state.rs
index 0990c9b27eb..3d738fa31f2 100644
--- a/compiler/rustc_ast_pretty/src/pprust/state.rs
+++ b/compiler/rustc_ast_pretty/src/pprust/state.rs
@@ -1063,6 +1063,14 @@ pub trait PrintState<'a>: std::ops::Deref<Target = pp::Printer> + std::ops::Dere
         Self::to_string(|s| s.print_item(i))
     }
 
+    fn assoc_item_to_string(&self, i: &ast::AssocItem) -> String {
+        Self::to_string(|s| s.print_assoc_item(i))
+    }
+
+    fn foreign_item_to_string(&self, i: &ast::ForeignItem) -> String {
+        Self::to_string(|s| s.print_foreign_item(i))
+    }
+
     fn path_to_string(&self, p: &ast::Path) -> String {
         Self::to_string(|s| s.print_path(p, false, 0))
     }
diff --git a/compiler/rustc_ast_pretty/src/pprust/state/item.rs b/compiler/rustc_ast_pretty/src/pprust/state/item.rs
index 3638eb31c61..6c442553976 100644
--- a/compiler/rustc_ast_pretty/src/pprust/state/item.rs
+++ b/compiler/rustc_ast_pretty/src/pprust/state/item.rs
@@ -28,7 +28,7 @@ impl<'a> State<'a> {
         }
     }
 
-    fn print_foreign_item(&mut self, item: &ast::ForeignItem) {
+    pub(crate) fn print_foreign_item(&mut self, item: &ast::ForeignItem) {
         let ast::Item { id, span, ref attrs, ref kind, ref vis, tokens: _ } = *item;
         self.ann.pre(self, AnnNode::SubItem(id));
         self.hardbreak_if_not_bol();
@@ -548,7 +548,7 @@ impl<'a> State<'a> {
         }
     }
 
-    fn print_assoc_item(&mut self, item: &ast::AssocItem) {
+    pub(crate) fn print_assoc_item(&mut self, item: &ast::AssocItem) {
         let ast::Item { id, span, ref attrs, ref kind, ref vis, tokens: _ } = *item;
         self.ann.pre(self, AnnNode::SubItem(id));
         self.hardbreak_if_not_bol();
diff --git a/compiler/rustc_expand/src/base.rs b/compiler/rustc_expand/src/base.rs
index c7b975d8f3e..311dadb53c4 100644
--- a/compiler/rustc_expand/src/base.rs
+++ b/compiler/rustc_expand/src/base.rs
@@ -12,7 +12,7 @@ use rustc_ast::tokenstream::TokenStream;
 use rustc_ast::visit::{AssocCtxt, Visitor};
 use rustc_ast::{self as ast, AttrVec, Attribute, HasAttrs, Item, NodeId, PatKind};
 use rustc_attr_data_structures::{AttributeKind, Deprecation, Stability, find_attr};
-use rustc_data_structures::fx::FxIndexMap;
+use rustc_data_structures::fx::{FxHashMap, FxIndexMap};
 use rustc_data_structures::sync;
 use rustc_errors::{DiagCtxtHandle, ErrorGuaranteed, PResult};
 use rustc_feature::Features;
@@ -727,6 +727,7 @@ pub enum SyntaxExtensionKind {
     /// A trivial attribute "macro" that does nothing,
     /// only keeps the attribute and marks it as inert,
     /// thus making it ineligible for further expansion.
+    /// E.g. `#[default]`, `#[rustfmt::skip]`.
     NonMacroAttr,
 
     /// A token-based derive macro.
@@ -1189,6 +1190,8 @@ pub struct ExtCtxt<'a> {
     /// in the AST, but insert it here so that we know
     /// not to expand it again.
     pub(super) expanded_inert_attrs: MarkedAttrs,
+    /// `-Zmacro-stats` data.
+    pub macro_stats: FxHashMap<(Symbol, MacroKind), crate::stats::MacroStat>, // njn: quals
 }
 
 impl<'a> ExtCtxt<'a> {
@@ -1218,6 +1221,7 @@ impl<'a> ExtCtxt<'a> {
             expansions: FxIndexMap::default(),
             expanded_inert_attrs: MarkedAttrs::new(),
             buffered_early_lint: vec![],
+            macro_stats: Default::default(),
         }
     }
 
diff --git a/compiler/rustc_expand/src/expand.rs b/compiler/rustc_expand/src/expand.rs
index 569165a64e5..3cfeb01ea47 100644
--- a/compiler/rustc_expand/src/expand.rs
+++ b/compiler/rustc_expand/src/expand.rs
@@ -42,13 +42,22 @@ use crate::module::{
     DirOwnership, ParsedExternalMod, mod_dir_path, mod_file_path_from_attr, parse_external_mod,
 };
 use crate::placeholders::{PlaceholderExpander, placeholder};
+use crate::stats::*;
 
 macro_rules! ast_fragments {
     (
         $($Kind:ident($AstTy:ty) {
             $kind_name:expr;
-            $(one fn $mut_visit_ast:ident; fn $visit_ast:ident;)?
-            $(many fn $flat_map_ast_elt:ident; fn $visit_ast_elt:ident($($args:tt)*);)?
+            $(one
+                fn $mut_visit_ast:ident;
+                fn $visit_ast:ident;
+                fn $ast_to_string:path;
+            )?
+            $(many
+                fn $flat_map_ast_elt:ident;
+                fn $visit_ast_elt:ident($($args:tt)*);
+                fn $ast_to_string_elt:path;
+            )?
             fn $make_ast:ident;
         })*
     ) => {
@@ -61,7 +70,7 @@ macro_rules! ast_fragments {
         }
 
         /// "Discriminant" of an AST fragment.
-        #[derive(Copy, Clone, PartialEq, Eq)]
+        #[derive(Copy, Clone, Debug, PartialEq, Eq)]
         pub enum AstFragmentKind {
             OptExpr,
             MethodReceiverExpr,
@@ -151,6 +160,21 @@ macro_rules! ast_fragments {
                 }
                 V::Result::output()
             }
+
+            pub(crate) fn to_string(&self) -> String {
+                match self {
+                    AstFragment::OptExpr(Some(expr)) => pprust::expr_to_string(expr),
+                    AstFragment::OptExpr(None) => unreachable!(),
+                    AstFragment::MethodReceiverExpr(expr) => pprust::expr_to_string(expr),
+                    $($(AstFragment::$Kind(ast) => $ast_to_string(ast),)?)*
+                    $($(
+                        AstFragment::$Kind(ast) => {
+                            // The closure unwraps a `P` if present, or does nothing otherwise.
+                            elems_to_string(&*ast, |ast| $ast_to_string_elt(&*ast))
+                        }
+                    )?)*
+                }
+            }
         }
 
         impl<'a> MacResult for crate::mbe::macro_rules::ParserAnyMacro<'a> {
@@ -163,76 +187,98 @@ macro_rules! ast_fragments {
 }
 
 ast_fragments! {
-    Expr(P<ast::Expr>) { "expression"; one fn visit_expr; fn visit_expr; fn make_expr; }
-    Pat(P<ast::Pat>) { "pattern"; one fn visit_pat; fn visit_pat; fn make_pat; }
-    Ty(P<ast::Ty>) { "type"; one fn visit_ty; fn visit_ty; fn make_ty; }
+    Expr(P<ast::Expr>) {
+        "expression";
+        one fn visit_expr; fn visit_expr; fn pprust::expr_to_string;
+        fn make_expr;
+    }
+    Pat(P<ast::Pat>) {
+        "pattern";
+        one fn visit_pat; fn visit_pat; fn pprust::pat_to_string;
+        fn make_pat;
+    }
+    Ty(P<ast::Ty>) {
+        "type";
+        one fn visit_ty; fn visit_ty; fn pprust::ty_to_string;
+        fn make_ty;
+    }
     Stmts(SmallVec<[ast::Stmt; 1]>) {
-        "statement"; many fn flat_map_stmt; fn visit_stmt(); fn make_stmts;
+        "statement";
+        many fn flat_map_stmt; fn visit_stmt(); fn pprust::stmt_to_string;
+        fn make_stmts;
     }
     Items(SmallVec<[P<ast::Item>; 1]>) {
-        "item"; many fn flat_map_item; fn visit_item(); fn make_items;
+        "item";
+        many fn flat_map_item; fn visit_item(); fn pprust::item_to_string;
+        fn make_items;
     }
     TraitItems(SmallVec<[P<ast::AssocItem>; 1]>) {
         "trait item";
-        many fn flat_map_assoc_item;
-        fn visit_assoc_item(AssocCtxt::Trait);
+        many fn flat_map_assoc_item; fn visit_assoc_item(AssocCtxt::Trait);
+            fn pprust::assoc_item_to_string;
         fn make_trait_items;
     }
     ImplItems(SmallVec<[P<ast::AssocItem>; 1]>) {
         "impl item";
-        many fn flat_map_assoc_item;
-        fn visit_assoc_item(AssocCtxt::Impl { of_trait: false });
+        many fn flat_map_assoc_item; fn visit_assoc_item(AssocCtxt::Impl { of_trait: false });
+            fn pprust::assoc_item_to_string;
         fn make_impl_items;
     }
     TraitImplItems(SmallVec<[P<ast::AssocItem>; 1]>) {
         "impl item";
-        many fn flat_map_assoc_item;
-        fn visit_assoc_item(AssocCtxt::Impl { of_trait: true });
+        many fn flat_map_assoc_item; fn visit_assoc_item(AssocCtxt::Impl { of_trait: true });
+            fn pprust::assoc_item_to_string;
         fn make_trait_impl_items;
     }
     ForeignItems(SmallVec<[P<ast::ForeignItem>; 1]>) {
         "foreign item";
-        many fn flat_map_foreign_item;
-        fn visit_foreign_item();
+        many fn flat_map_foreign_item; fn visit_foreign_item(); fn pprust::foreign_item_to_string;
         fn make_foreign_items;
     }
     Arms(SmallVec<[ast::Arm; 1]>) {
-        "match arm"; many fn flat_map_arm; fn visit_arm(); fn make_arms;
+        "match arm";
+        many fn flat_map_arm; fn visit_arm(); fn unreachable_to_string;
+        fn make_arms;
     }
     ExprFields(SmallVec<[ast::ExprField; 1]>) {
-        "field expression"; many fn flat_map_expr_field; fn visit_expr_field(); fn make_expr_fields;
+        "field expression";
+        many fn flat_map_expr_field; fn visit_expr_field(); fn unreachable_to_string;
+        fn make_expr_fields;
     }
     PatFields(SmallVec<[ast::PatField; 1]>) {
         "field pattern";
-        many fn flat_map_pat_field;
-        fn visit_pat_field();
+        many fn flat_map_pat_field; fn visit_pat_field(); fn unreachable_to_string;
         fn make_pat_fields;
     }
     GenericParams(SmallVec<[ast::GenericParam; 1]>) {
         "generic parameter";
-        many fn flat_map_generic_param;
-        fn visit_generic_param();
+        many fn flat_map_generic_param; fn visit_generic_param(); fn unreachable_to_string;
         fn make_generic_params;
     }
     Params(SmallVec<[ast::Param; 1]>) {
-        "function parameter"; many fn flat_map_param; fn visit_param(); fn make_params;
+        "function parameter";
+        many fn flat_map_param; fn visit_param(); fn unreachable_to_string;
+        fn make_params;
     }
     FieldDefs(SmallVec<[ast::FieldDef; 1]>) {
         "field";
-        many fn flat_map_field_def;
-        fn visit_field_def();
+        many fn flat_map_field_def; fn visit_field_def(); fn unreachable_to_string;
         fn make_field_defs;
     }
     Variants(SmallVec<[ast::Variant; 1]>) {
-        "variant"; many fn flat_map_variant; fn visit_variant(); fn make_variants;
+        "variant"; many fn flat_map_variant; fn visit_variant(); fn unreachable_to_string;
+        fn make_variants;
     }
     WherePredicates(SmallVec<[ast::WherePredicate; 1]>) {
         "where predicate";
-        many fn flat_map_where_predicate;
-        fn visit_where_predicate();
+        many fn flat_map_where_predicate; fn visit_where_predicate(); fn unreachable_to_string;
         fn make_where_predicates;
-     }
-    Crate(ast::Crate) { "crate"; one fn visit_crate; fn visit_crate; fn make_crate; }
+    }
+    Crate(ast::Crate) {
+        "crate";
+        one fn visit_crate; fn visit_crate; fn unreachable_to_string;
+        fn make_crate;
+    }
 }
 
 pub enum SupportsMacroExpansion {
@@ -270,7 +316,7 @@ impl AstFragmentKind {
         }
     }
 
-    fn expect_from_annotatables<I: IntoIterator<Item = Annotatable>>(
+    pub(crate) fn expect_from_annotatables<I: IntoIterator<Item = Annotatable>>(
         self,
         items: I,
     ) -> AstFragment {
@@ -686,13 +732,26 @@ impl<'a, 'b> MacroExpander<'a, 'b> {
             return ExpandResult::Ready(invoc.fragment_kind.dummy(invoc.span(), guar));
         }
 
+        let macro_stats = self.cx.sess.opts.unstable_opts.macro_stats;
+
         let (fragment_kind, span) = (invoc.fragment_kind, invoc.span());
         ExpandResult::Ready(match invoc.kind {
             InvocationKind::Bang { mac, span } => match ext {
                 SyntaxExtensionKind::Bang(expander) => {
                     match expander.expand(self.cx, span, mac.args.tokens.clone()) {
                         Ok(tok_result) => {
-                            self.parse_ast_fragment(tok_result, fragment_kind, &mac.path, span)
+                            let fragment =
+                                self.parse_ast_fragment(tok_result, fragment_kind, &mac.path, span);
+                            if macro_stats {
+                                update_bang_macro_stats(
+                                    self.cx,
+                                    fragment_kind,
+                                    span,
+                                    mac,
+                                    &fragment,
+                                );
+                            }
+                            fragment
                         }
                         Err(guar) => return ExpandResult::Ready(fragment_kind.dummy(span, guar)),
                     }
@@ -708,13 +767,15 @@ impl<'a, 'b> MacroExpander<'a, 'b> {
                             });
                         }
                     };
-                    let result = if let Some(result) = fragment_kind.make_from(tok_result) {
-                        result
+                    if let Some(fragment) = fragment_kind.make_from(tok_result) {
+                        if macro_stats {
+                            update_bang_macro_stats(self.cx, fragment_kind, span, mac, &fragment);
+                        }
+                        fragment
                     } else {
                         let guar = self.error_wrong_fragment_kind(fragment_kind, &mac, span);
                         fragment_kind.dummy(span, guar)
-                    };
-                    result
+                    }
                 }
                 _ => unreachable!(),
             },
@@ -746,24 +807,39 @@ impl<'a, 'b> MacroExpander<'a, 'b> {
                         }
                         _ => item.to_tokens(),
                     };
-                    let attr_item = attr.unwrap_normal_item();
+                    let attr_item = attr.get_normal_item();
                     if let AttrArgs::Eq { .. } = attr_item.args {
                         self.cx.dcx().emit_err(UnsupportedKeyValue { span });
                     }
                     let inner_tokens = attr_item.args.inner_tokens();
                     match expander.expand(self.cx, span, inner_tokens, tokens) {
-                        Ok(tok_result) => self.parse_ast_fragment(
-                            tok_result,
-                            fragment_kind,
-                            &attr_item.path,
-                            span,
-                        ),
+                        Ok(tok_result) => {
+                            let fragment = self.parse_ast_fragment(
+                                tok_result,
+                                fragment_kind,
+                                &attr_item.path,
+                                span,
+                            );
+                            if macro_stats {
+                                update_attr_macro_stats(
+                                    self.cx,
+                                    fragment_kind,
+                                    span,
+                                    &attr_item.path,
+                                    &attr,
+                                    item,
+                                    &fragment,
+                                );
+                            }
+                            fragment
+                        }
                         Err(guar) => return ExpandResult::Ready(fragment_kind.dummy(span, guar)),
                     }
                 }
                 SyntaxExtensionKind::LegacyAttr(expander) => {
                     match validate_attr::parse_meta(&self.cx.sess.psess, &attr) {
                         Ok(meta) => {
+                            let item_clone = macro_stats.then(|| item.clone());
                             let items = match expander.expand(self.cx, span, &meta, item, false) {
                                 ExpandResult::Ready(items) => items,
                                 ExpandResult::Retry(item) => {
@@ -782,7 +858,19 @@ impl<'a, 'b> MacroExpander<'a, 'b> {
                                 let guar = self.cx.dcx().emit_err(RemoveExprNotSupported { span });
                                 fragment_kind.dummy(span, guar)
                             } else {
-                                fragment_kind.expect_from_annotatables(items)
+                                let fragment = fragment_kind.expect_from_annotatables(items);
+                                if macro_stats {
+                                    update_attr_macro_stats(
+                                        self.cx,
+                                        fragment_kind,
+                                        span,
+                                        &meta.path,
+                                        &attr,
+                                        item_clone.unwrap(),
+                                        &fragment,
+                                    );
+                                }
+                                fragment
                             }
                         }
                         Err(err) => {
@@ -792,6 +880,7 @@ impl<'a, 'b> MacroExpander<'a, 'b> {
                     }
                 }
                 SyntaxExtensionKind::NonMacroAttr => {
+                    // `-Zmacro-stats` ignores these because they don't do any real expansion.
                     self.cx.expanded_inert_attrs.mark(&attr);
                     item.visit_attrs(|attrs| attrs.insert(pos, attr));
                     fragment_kind.expect_from_annotatables(iter::once(item))
@@ -822,7 +911,17 @@ impl<'a, 'b> MacroExpander<'a, 'b> {
                             });
                         }
                     };
-                    fragment_kind.expect_from_annotatables(items)
+                    let fragment = fragment_kind.expect_from_annotatables(items);
+                    if macro_stats {
+                        update_derive_macro_stats(
+                            self.cx,
+                            fragment_kind,
+                            span,
+                            &meta.path,
+                            &fragment,
+                        );
+                    }
+                    fragment
                 }
                 _ => unreachable!(),
             },
@@ -852,6 +951,7 @@ impl<'a, 'b> MacroExpander<'a, 'b> {
                 let single_delegations = build_single_delegations::<Node>(
                     self.cx, deleg, &item, &suffixes, item.span, true,
                 );
+                // `-Zmacro-stats` ignores these because they don't seem important.
                 fragment_kind.expect_from_annotatables(
                     single_delegations
                         .map(|item| Annotatable::AssocItem(P(item), AssocCtxt::Impl { of_trait })),
diff --git a/compiler/rustc_expand/src/lib.rs b/compiler/rustc_expand/src/lib.rs
index 515d82296ca..64be7649775 100644
--- a/compiler/rustc_expand/src/lib.rs
+++ b/compiler/rustc_expand/src/lib.rs
@@ -20,6 +20,7 @@ mod errors;
 mod mbe;
 mod placeholders;
 mod proc_macro_server;
+mod stats;
 
 pub use mbe::macro_rules::compile_declarative_macro;
 pub mod base;
diff --git a/compiler/rustc_expand/src/stats.rs b/compiler/rustc_expand/src/stats.rs
new file mode 100644
index 00000000000..6b2ad30dffd
--- /dev/null
+++ b/compiler/rustc_expand/src/stats.rs
@@ -0,0 +1,171 @@
+use std::iter;
+
+use rustc_ast::ptr::P;
+use rustc_ast::{self as ast, DUMMY_NODE_ID, Expr, ExprKind};
+use rustc_ast_pretty::pprust;
+use rustc_span::hygiene::{ExpnKind, MacroKind};
+use rustc_span::{Span, Symbol, kw, sym};
+use smallvec::SmallVec;
+
+use crate::base::{Annotatable, ExtCtxt};
+use crate::expand::{AstFragment, AstFragmentKind};
+
+#[derive(Default)]
+pub struct MacroStat {
+    /// Number of uses of the macro.
+    pub uses: usize,
+
+    /// Net increase in number of lines of code (when pretty-printed), i.e.
+    /// `lines(output) - lines(invocation)`. Can be negative because a macro
+    /// output may be smaller than the invocation.
+    pub lines: isize,
+
+    /// Net increase in number of lines of code (when pretty-printed), i.e.
+    /// `bytes(output) - bytes(invocation)`. Can be negative because a macro
+    /// output may be smaller than the invocation.
+    pub bytes: isize,
+}
+
+pub(crate) fn elems_to_string<T>(elems: &SmallVec<[T; 1]>, f: impl Fn(&T) -> String) -> String {
+    let mut s = String::new();
+    for (i, elem) in elems.iter().enumerate() {
+        if i > 0 {
+            s.push('\n');
+        }
+        s.push_str(&f(elem));
+    }
+    s
+}
+
+pub(crate) fn unreachable_to_string<T>(_: &T) -> String {
+    unreachable!()
+}
+
+pub(crate) fn update_bang_macro_stats(
+    ecx: &mut ExtCtxt<'_>,
+    fragment_kind: AstFragmentKind,
+    span: Span,
+    mac: P<ast::MacCall>,
+    fragment: &AstFragment,
+) {
+    // Does this path match any of the include macros, e.g. `include!`?
+    // Ignore them. They would have large numbers but are entirely
+    // unsurprising and uninteresting.
+    let is_include_path = mac.path == sym::include
+        || mac.path == sym::include_bytes
+        || mac.path == sym::include_str
+        || mac.path == [sym::std, sym::include].as_slice() // std::include
+        || mac.path == [sym::std, sym::include_bytes].as_slice() // std::include_bytes
+        || mac.path == [sym::std, sym::include_str].as_slice(); // std::include_str
+    if is_include_path {
+        return;
+    }
+
+    // The call itself (e.g. `println!("hi")`) is the input. Need to wrap
+    // `mac` in something printable; `ast::Expr` is as good as anything
+    // else.
+    let expr = Expr {
+        id: DUMMY_NODE_ID,
+        kind: ExprKind::MacCall(mac),
+        span: Default::default(),
+        attrs: Default::default(),
+        tokens: None,
+    };
+    let input = pprust::expr_to_string(&expr);
+
+    // Get `mac` back out of `expr`.
+    let ast::Expr { kind: ExprKind::MacCall(mac), .. } = expr else { unreachable!() };
+
+    update_macro_stats(ecx, MacroKind::Bang, fragment_kind, span, &mac.path, &input, fragment);
+}
+
+pub(crate) fn update_attr_macro_stats(
+    ecx: &mut ExtCtxt<'_>,
+    fragment_kind: AstFragmentKind,
+    span: Span,
+    path: &ast::Path,
+    attr: &ast::Attribute,
+    item: Annotatable,
+    fragment: &AstFragment,
+) {
+    // Does this path match `#[derive(...)]` in any of its forms? If so,
+    // ignore it because the individual derives will go through the
+    // `Invocation::Derive` handling separately.
+    let is_derive_path = *path == sym::derive
+        // ::core::prelude::v1::derive
+        || *path == [kw::PathRoot, sym::core, sym::prelude, sym::v1, sym::derive].as_slice();
+    if is_derive_path {
+        return;
+    }
+
+    // The attribute plus the item itself constitute the input, which we
+    // measure.
+    let input = format!(
+        "{}\n{}",
+        pprust::attribute_to_string(attr),
+        fragment_kind.expect_from_annotatables(iter::once(item)).to_string(),
+    );
+    update_macro_stats(ecx, MacroKind::Attr, fragment_kind, span, path, &input, fragment);
+}
+
+pub(crate) fn update_derive_macro_stats(
+    ecx: &mut ExtCtxt<'_>,
+    fragment_kind: AstFragmentKind,
+    span: Span,
+    path: &ast::Path,
+    fragment: &AstFragment,
+) {
+    // Use something like `#[derive(Clone)]` for the measured input, even
+    // though it may have actually appeared in a multi-derive attribute
+    // like `#[derive(Clone, Copy, Debug)]`.
+    let input = format!("#[derive({})]", pprust::path_to_string(path));
+    update_macro_stats(ecx, MacroKind::Derive, fragment_kind, span, path, &input, fragment);
+}
+
+pub(crate) fn update_macro_stats(
+    ecx: &mut ExtCtxt<'_>,
+    macro_kind: MacroKind,
+    fragment_kind: AstFragmentKind,
+    span: Span,
+    path: &ast::Path,
+    input: &str,
+    fragment: &AstFragment,
+) {
+    fn lines_and_bytes(s: &str) -> (usize, usize) {
+        (s.trim_end().split('\n').count(), s.len())
+    }
+
+    // Measure the size of the output by pretty-printing it and counting
+    // the lines and bytes.
+    let name = Symbol::intern(&pprust::path_to_string(path));
+    let output = fragment.to_string();
+    let (in_l, in_b) = lines_and_bytes(input);
+    let (out_l, out_b) = lines_and_bytes(&output);
+
+    // This code is useful for debugging `-Zmacro-stats`. For every
+    // invocation it prints the full input and output.
+    if false {
+        let name = ExpnKind::Macro(macro_kind, name).descr();
+        let crate_name = &ecx.ecfg.crate_name;
+        let span = ecx
+            .sess
+            .source_map()
+            .span_to_string(span, rustc_span::FileNameDisplayPreference::Local);
+        eprint!(
+            "\
+            -------------------------------\n\
+            {name}: [{crate_name}] ({fragment_kind:?}) {span}\n\
+            -------------------------------\n\
+            {input}\n\
+            -- ({in_l} lines, {in_b} bytes) --> ({out_l} lines, {out_b} bytes) --\n\
+            {output}\n\
+        "
+        );
+    }
+
+    // The recorded size is the difference between the input and the output.
+    let entry = ecx.macro_stats.entry((name, macro_kind)).or_insert(MacroStat::default());
+    entry.uses += 1;
+    entry.lines += out_l as isize - in_l as isize;
+    entry.bytes += out_b as isize - in_b as isize;
+}
diff --git a/compiler/rustc_interface/src/passes.rs b/compiler/rustc_interface/src/passes.rs
index 99520a3fea3..0238d6a3947 100644
--- a/compiler/rustc_interface/src/passes.rs
+++ b/compiler/rustc_interface/src/passes.rs
@@ -8,9 +8,9 @@ use std::{env, fs, iter};
 use rustc_ast as ast;
 use rustc_codegen_ssa::traits::CodegenBackend;
 use rustc_data_structures::jobserver::Proxy;
-use rustc_data_structures::parallel;
 use rustc_data_structures::steal::Steal;
 use rustc_data_structures::sync::{AppendOnlyIndexVec, FreezeLock, WorkerLocal};
+use rustc_data_structures::{parallel, thousands};
 use rustc_expand::base::{ExtCtxt, LintStoreExpand};
 use rustc_feature::Features;
 use rustc_fs_util::try_canonicalize;
@@ -35,7 +35,8 @@ use rustc_session::parse::feature_err;
 use rustc_session::search_paths::PathKind;
 use rustc_session::{Limit, Session};
 use rustc_span::{
-    DUMMY_SP, ErrorGuaranteed, FileName, SourceFileHash, SourceFileHashAlgorithm, Span, Symbol, sym,
+    DUMMY_SP, ErrorGuaranteed, ExpnKind, FileName, SourceFileHash, SourceFileHashAlgorithm, Span,
+    Symbol, sym,
 };
 use rustc_target::spec::PanicStrategy;
 use rustc_trait_selection::traits;
@@ -205,7 +206,7 @@ fn configure_and_expand(
         // Expand macros now!
         let krate = sess.time("expand_crate", || ecx.monotonic_expander().expand_crate(krate));
 
-        // The rest is error reporting
+        // The rest is error reporting and stats
 
         sess.psess.buffered_lints.with_lock(|buffered_lints: &mut Vec<BufferedEarlyLint>| {
             buffered_lints.append(&mut ecx.buffered_early_lint);
@@ -228,6 +229,10 @@ fn configure_and_expand(
             }
         }
 
+        if ecx.sess.opts.unstable_opts.macro_stats {
+            print_macro_stats(&ecx);
+        }
+
         krate
     });
 
@@ -288,6 +293,76 @@ fn configure_and_expand(
     krate
 }
 
+fn print_macro_stats(ecx: &ExtCtxt<'_>) {
+    use std::fmt::Write;
+
+    // No instability because we immediately sort the produced vector.
+    #[allow(rustc::potential_query_instability)]
+    let mut macro_stats: Vec<_> = ecx
+        .macro_stats
+        .iter()
+        .map(|((name, kind), stat)| {
+            // This gives the desired sort order: sort by bytes, then lines, etc.
+            (stat.bytes, stat.lines, stat.uses, name, *kind)
+        })
+        .collect();
+    macro_stats.sort_unstable();
+    macro_stats.reverse(); // bigger items first
+
+    let prefix = "macro-stats";
+    let name_w = 32;
+    let uses_w = 7;
+    let lines_w = 11;
+    let avg_lines_w = 11;
+    let bytes_w = 11;
+    let avg_bytes_w = 11;
+    let banner_w = name_w + uses_w + lines_w + avg_lines_w + bytes_w + avg_bytes_w;
+
+    // We write all the text into a string and print it with a single
+    // `eprint!`. This is an attempt to minimize interleaved text if multiple
+    // rustc processes are printing macro-stats at the same time (e.g. with
+    // `RUSTFLAGS='-Zmacro-stats' cargo build`). It still doesn't guarantee
+    // non-interleaving, though.
+    let mut s = String::new();
+    _ = writeln!(s, "{prefix} {}", "=".repeat(banner_w));
+    _ = writeln!(s, "{prefix} MACRO EXPANSION STATS: {}", ecx.ecfg.crate_name);
+    _ = writeln!(
+        s,
+        "{prefix} {:<name_w$}{:>uses_w$}{:>lines_w$}{:>avg_lines_w$}{:>bytes_w$}{:>avg_bytes_w$}",
+        "Macro Name", "Uses", "Lines", "Avg Lines", "Bytes", "Avg Bytes",
+    );
+    _ = writeln!(s, "{prefix} {}", "-".repeat(banner_w));
+    // It's helpful to print something when there are no entries, otherwise it
+    // might look like something went wrong.
+    if macro_stats.is_empty() {
+        _ = writeln!(s, "{prefix} (none)");
+    }
+    for (bytes, lines, uses, name, kind) in macro_stats {
+        let mut name = ExpnKind::Macro(kind, *name).descr();
+        let avg_lines = lines as f64 / uses as f64;
+        let avg_bytes = bytes as f64 / uses as f64;
+        if name.len() >= name_w {
+            // If the name is long, print it on a line by itself, then
+            // set the name to empty and print things normally, to show the
+            // stats on the next line.
+            _ = writeln!(s, "{prefix} {:<name_w$}", name);
+            name = String::new();
+        }
+        _ = writeln!(
+            s,
+            "{prefix} {:<name_w$}{:>uses_w$}{:>lines_w$}{:>avg_lines_w$}{:>bytes_w$}{:>avg_bytes_w$}",
+            name,
+            thousands::usize_with_underscores(uses),
+            thousands::isize_with_underscores(lines),
+            thousands::f64p1_with_underscores(avg_lines),
+            thousands::isize_with_underscores(bytes),
+            thousands::f64p1_with_underscores(avg_bytes),
+        );
+    }
+    _ = writeln!(s, "{prefix} {}", "=".repeat(banner_w));
+    eprint!("{s}");
+}
+
 fn early_lint_checks(tcx: TyCtxt<'_>, (): ()) {
     let sess = tcx.sess;
     let (resolver, krate) = &*tcx.resolver_for_lowering().borrow();
diff --git a/compiler/rustc_interface/src/tests.rs b/compiler/rustc_interface/src/tests.rs
index 558f13a832c..5419d688caa 100644
--- a/compiler/rustc_interface/src/tests.rs
+++ b/compiler/rustc_interface/src/tests.rs
@@ -709,6 +709,7 @@ fn test_unstable_options_tracking_hash() {
     untracked!(llvm_time_trace, true);
     untracked!(ls, vec!["all".to_owned()]);
     untracked!(macro_backtrace, true);
+    untracked!(macro_stats, true);
     untracked!(meta_stats, true);
     untracked!(mir_include_spans, MirIncludeSpans::On);
     untracked!(nll_facts, true);
diff --git a/compiler/rustc_session/src/options.rs b/compiler/rustc_session/src/options.rs
index 12fa05118ca..a26d3bc4044 100644
--- a/compiler/rustc_session/src/options.rs
+++ b/compiler/rustc_session/src/options.rs
@@ -2305,6 +2305,8 @@ options! {
         (space separated)"),
     macro_backtrace: bool = (false, parse_bool, [UNTRACKED],
         "show macro backtraces (default: no)"),
+    macro_stats: bool = (false, parse_bool, [UNTRACKED],
+        "print some statistics about macro expansions (default: no)"),
     maximal_hir_to_mir_coverage: bool = (false, parse_bool, [TRACKED],
         "save as much information as possible about the correspondence between MIR and HIR \
         as source scopes (default: no)"),
diff --git a/compiler/rustc_span/src/hygiene.rs b/compiler/rustc_span/src/hygiene.rs
index b621920d62b..315dedec107 100644
--- a/compiler/rustc_span/src/hygiene.rs
+++ b/compiler/rustc_span/src/hygiene.rs
@@ -1135,7 +1135,7 @@ impl ExpnKind {
 }
 
 /// The kind of macro invocation or definition.
-#[derive(Clone, Copy, PartialEq, Eq, Encodable, Decodable, Hash, Debug)]
+#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Encodable, Decodable, Hash, Debug)]
 #[derive(HashStable_Generic)]
 pub enum MacroKind {
     /// A bang macro `foo!()`.
diff --git a/compiler/rustc_span/src/symbol.rs b/compiler/rustc_span/src/symbol.rs
index d66f98871b9..8c0e0795acb 100644
--- a/compiler/rustc_span/src/symbol.rs
+++ b/compiler/rustc_span/src/symbol.rs
@@ -2288,6 +2288,7 @@ symbols! {
         usize_legacy_fn_max_value,
         usize_legacy_fn_min_value,
         usize_legacy_mod,
+        v1,
         v8plus,
         va_arg,
         va_copy,
diff --git a/src/doc/unstable-book/src/compiler-flags/macro-stats.md b/src/doc/unstable-book/src/compiler-flags/macro-stats.md
new file mode 100644
index 00000000000..b2622cff057
--- /dev/null
+++ b/src/doc/unstable-book/src/compiler-flags/macro-stats.md
@@ -0,0 +1,24 @@
+# `macro-stats`
+
+This feature is perma-unstable and has no tracking issue.
+
+----
+
+Some macros, especially procedural macros, can generate a surprising amount of
+code, which can slow down compile times. This is hard to detect because the
+generated code is normally invisible to the programmer.
+
+This flag helps identify such cases. When enabled, the compiler measures the
+effect on code size of all used macros and prints a table summarizing that
+effect. For each distinct macro, it counts how many times it is used, and the
+net effect on code size (in terms of lines of code, and bytes of code). The
+code size evaluation uses the compiler's internal pretty-printing, and so will
+be independent of the formatting in the original code.
+
+Note that the net effect of a macro may be negative. E.g. the `cfg!` and
+`#[test]` macros often strip out code.
+
+If a macro is identified as causing a large increase in code size, it is worth
+using `cargo expand` to inspect the post-expansion code, which includes the
+code produced by all macros. It may be possible to optimize the macro to
+produce smaller code, or it may be possible to avoid using it altogether.
diff --git a/tests/ui/stats/auxiliary/include.rs b/tests/ui/stats/auxiliary/include.rs
new file mode 100644
index 00000000000..7fb7c781137
--- /dev/null
+++ b/tests/ui/stats/auxiliary/include.rs
@@ -0,0 +1,3 @@
+fn zzz(x: u32) -> u32 {
+    x
+}
diff --git a/tests/ui/stats/macro-stats.rs b/tests/ui/stats/macro-stats.rs
new file mode 100644
index 00000000000..ee265d682fd
--- /dev/null
+++ b/tests/ui/stats/macro-stats.rs
@@ -0,0 +1,130 @@
+//@ check-pass
+//@ compile-flags: -Zmacro-stats
+
+#[test]
+fn test_foo() {
+    let what = "this";
+    let how = "completely";
+    let when = "immediately";
+    println!("{what} disappears {how} and {when}");
+}
+
+#[rustfmt::skip] // non-macro attr, ignored by `-Zmacro-stats`
+fn rustfmt_skip() {
+    // Nothing to see here.
+}
+
+#[derive(Default, Clone, Copy, Hash)]
+enum E1 {
+    #[default] // non-macro attr, ignored by `-Zmacro-stats`
+    A,
+    B,
+}
+
+#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
+struct E2 {
+    a: u32,
+    b: String,
+    c: Vec<bool>,
+}
+
+#[derive(Clone)] struct S0;
+#[derive(Clone)] struct S1(u32);
+#[derive(Clone)] struct S2(u32, u32);
+#[derive(Clone)] struct S3(u32, u32, u32);
+#[derive(Clone)] struct S4(u32, u32, u32, u32);
+#[derive(Clone)] struct S5(u32, u32, u32, u32, u32);
+
+macro_rules! u32 {
+    () => { u32 }
+}
+
+macro_rules! none {
+    () => { None }
+}
+fn opt(x: Option<u32>) {
+    match x {
+        Some(_) => {}
+        none!() => {}           // AstFragmentKind::Pat
+    }
+}
+
+macro_rules! this_is_a_really_really_long_macro_name {
+    ($t:ty) => {
+        fn f(_: $t) {}
+    }
+}
+this_is_a_really_really_long_macro_name!(u32!()); // AstFragmentKind::{Items,Ty}
+
+macro_rules! trait_tys {
+    () => {
+        type A;
+        type B;
+    }
+}
+trait Tr {
+    trait_tys!();               // AstFragmentKind::TraitItems
+}
+
+macro_rules! impl_const { () => { const X: u32 = 0; } }
+struct U;
+impl U {
+    impl_const!();              // AstFragmentKind::ImplItems
+}
+
+macro_rules! trait_impl_tys {
+    () => {
+        type A = u32;
+        type B = bool;
+    }
+}
+struct Tr1;
+impl Tr for Tr1 {
+    trait_impl_tys!();          // AstFragment::TraitImplItems
+}
+
+macro_rules! foreign_item {
+    () => { fn fc(a: u32) -> u32; }
+}
+extern "C" {
+    foreign_item!();            // AstFragment::ForeignItems
+}
+
+// Include macros are ignored by `-Zmacro-stats`.
+mod includes {
+    mod z1 {
+        include!("auxiliary/include.rs");
+    }
+    mod z2 {
+        std::include!("auxiliary/include.rs");
+    }
+
+    const B1: &[u8] = include_bytes!("auxiliary/include.rs");
+    const B2: &[u8] = std::include_bytes!("auxiliary/include.rs");
+
+    const S1: &str = include_str!("auxiliary/include.rs");
+    const S2: &str = std::include_str!("auxiliary/include.rs");
+}
+
+fn main() {
+    macro_rules! n99 {
+        () => { 99 }
+    }
+    let x = n99!() + n99!();    // AstFragmentKind::Expr
+
+    macro_rules! p {
+        () => {
+            // blah
+            let x = 1;
+            let y = x;
+            let _ = y;
+        }
+    }
+    p!();                       // AstFragmentKind::Stmts
+
+    macro_rules! q {
+        () => {};
+        ($($x:ident),*) => { $( let $x: u32 = 12345; )* };
+    }
+    q!(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z);
+}
diff --git a/tests/ui/stats/macro-stats.stderr b/tests/ui/stats/macro-stats.stderr
new file mode 100644
index 00000000000..f87e34622b9
--- /dev/null
+++ b/tests/ui/stats/macro-stats.stderr
@@ -0,0 +1,26 @@
+macro-stats ===================================================================================
+macro-stats MACRO EXPANSION STATS: macro_stats
+macro-stats Macro Name                         Uses      Lines  Avg Lines      Bytes  Avg Bytes
+macro-stats -----------------------------------------------------------------------------------
+macro-stats #[derive(Clone)]                      8         56        7.0      1_660      207.5
+macro-stats #[derive(PartialOrd)]                 1         16       16.0        654      654.0
+macro-stats #[derive(Hash)]                       2         15        7.5        547      273.5
+macro-stats #[derive(Ord)]                        1         14       14.0        489      489.0
+macro-stats q!                                    1         24       24.0        435      435.0
+macro-stats #[derive(Default)]                    2         14        7.0        367      183.5
+macro-stats #[derive(Eq)]                         1         10       10.0        312      312.0
+macro-stats #[derive(Debug)]                      1          7        7.0        261      261.0
+macro-stats #[derive(PartialEq)]                  1          8        8.0        247      247.0
+macro-stats #[derive(Copy)]                       1          1        1.0         46       46.0
+macro-stats p!                                    1          2        2.0         28       28.0
+macro-stats trait_impl_tys!                       1          1        1.0         11       11.0
+macro-stats foreign_item!                         1          0        0.0          6        6.0
+macro-stats impl_const!                           1          0        0.0          4        4.0
+macro-stats trait_tys!                            1          1        1.0          3        3.0
+macro-stats u32!                                  1          0        0.0         -3       -3.0
+macro-stats none!                                 1          0        0.0         -3       -3.0
+macro-stats n99!                                  2          0        0.0         -8       -4.0
+macro-stats this_is_a_really_really_long_macro_name!
+macro-stats                                       1          0        0.0        -30      -30.0
+macro-stats #[test]                               1         -6       -6.0       -158     -158.0
+macro-stats ===================================================================================