1use std::collections::HashMap;
15use std::fmt::Write as _;
16
17use crate::Engine;
18use crate::hash::CsId;
19use crate::input::Level;
20
21#[derive(Default)]
23pub struct MacroProfile {
24 path: String,
26 calls: HashMap<CsId, u64>,
28 inclusive: HashMap<CsId, u64>,
30 total: u64,
32 samples: u64,
34}
35
36const SAMPLE_EVERY: u64 = 64;
38
39impl MacroProfile {
40 pub fn from_env() -> Option<Box<MacroProfile>> {
42 let path = std::env::var("RETEX_MACRO_PROFILE").ok()?;
43 Some(Box::new(MacroProfile {
44 path,
45 ..MacroProfile::default()
46 }))
47 }
48}
49
50fn package_of(name: &str) -> String {
52 let n = name.trim_start_matches('_');
53 if name.contains(':') || name.starts_with("__") {
55 let module = n.split(['_', ':']).next().unwrap_or(n);
56 return format!("expl3 {module}");
57 }
58 for p in [
59 "pgfmath",
60 "pgfplots",
61 "pgfkeys",
62 "pgfsys",
63 "pgfutil",
64 "pgfpoint",
65 "pgfpath",
66 "pgf@",
67 "pgf",
68 "tikz@",
69 "tikz",
70 "tcb@",
71 "tcb",
72 "sisetup",
73 "si@",
74 "SI",
75 "num",
76 "bbl@",
77 "beamer@",
78 "beamer",
79 "Hy@",
80 "HyPsd@",
81 "hyper",
82 "Gin@",
83 "Gread@",
84 "@float",
85 "@makecaption",
86 "@ifnextchar",
87 "@ifstar",
88 "@xifnextchar",
89 "@tempa",
90 "@for",
91 "@tfor",
92 "@tempb",
93 "@protected@",
94 "@expl@",
95 "fontspec",
96 "f@",
97 ] {
98 if n.starts_with(p) {
99 return p.trim_end_matches('@').to_owned();
100 }
101 }
102 if n.contains('@') {
103 return "LaTeX internals (@)".to_owned();
104 }
105 "(other)".to_owned()
106}
107
108impl Engine {
109 #[cold]
111 pub(crate) fn profile_macro_call(&mut self, cs: CsId) {
112 let Some(p) = self.macro_profile.as_mut() else {
113 return;
114 };
115 *p.calls.entry(cs).or_default() += 1;
116 p.total += 1;
117 if p.total % SAMPLE_EVERY != 0 {
118 return;
119 }
120 p.samples += 1;
121 let mut seen: Vec<CsId> = vec![cs];
122 for level in &self.input.stack {
123 if let Level::Tokens(t) = level
124 && t.name != crate::hash::NO_CS
125 && !seen.contains(&t.name)
126 {
127 seen.push(t.name);
128 }
129 }
130 for c in seen {
131 *p.inclusive.entry(c).or_default() += 1;
132 }
133 }
134
135 pub fn write_macro_profile(&mut self) {
137 let Some(p) = self.macro_profile.take() else {
138 return;
139 };
140 let name = |e: &Engine, c: CsId| String::from_utf8_lossy(e.hash.name(c)).into_owned();
141 let mut out = String::new();
142 let _ = writeln!(
143 out,
144 "macro calls: {}, input-stack samples: {} (every {SAMPLE_EVERY} calls)\n",
145 p.total, p.samples
146 );
147 let mut by_pkg: HashMap<String, (u64, u64)> = HashMap::new();
148 for (&c, &n) in &p.calls {
149 by_pkg.entry(package_of(&name(self, c))).or_default().0 += n;
150 }
151 for (&c, &n) in &p.inclusive {
154 let e = by_pkg.entry(package_of(&name(self, c))).or_default();
155 e.1 = e.1.max(n);
156 }
157 let mut pk: Vec<_> = by_pkg.into_iter().collect();
158 pk.sort_by(|a, b| b.1.0.cmp(&a.1.0));
159 let _ = writeln!(
160 out,
161 "{:>7} {:>7} package (calls %, max inclusive %)",
162 "calls", "incl"
163 );
164 for (k, (c, i)) in pk.iter().take(40) {
165 let _ = writeln!(
166 out,
167 "{:>6.2}% {:>6.2}% {k}",
168 *c as f64 * 100.0 / p.total.max(1) as f64,
169 *i as f64 * 100.0 / p.samples.max(1) as f64
170 );
171 }
172 let mut calls: Vec<_> = p.calls.iter().map(|(&c, &n)| (n, c)).collect();
173 calls.sort_by(|a, b| b.0.cmp(&a.0));
174 let _ = writeln!(out, "\ntop macros by calls:");
175 for (n, c) in calls.iter().take(60) {
176 let _ = writeln!(
177 out,
178 "{:>6.2}% {:>10} \\{}",
179 *n as f64 * 100.0 / p.total.max(1) as f64,
180 n,
181 name(self, *c)
182 );
183 }
184 let mut incl: Vec<_> = p.inclusive.iter().map(|(&c, &n)| (n, c)).collect();
185 incl.sort_by(|a, b| b.0.cmp(&a.0));
186 let _ = writeln!(out, "\ntop macros by inclusive samples:");
187 for (n, c) in incl.iter().take(60) {
188 let _ = writeln!(
189 out,
190 "{:>6.2}% \\{}",
191 *n as f64 * 100.0 / p.samples.max(1) as f64,
192 name(self, *c)
193 );
194 }
195 let _ = std::fs::write(&p.path, out);
196 let mut tsv = String::from("calls\tinclusive\tname\n");
198 for (n, c) in &calls {
199 let incl = p.inclusive.get(c).copied().unwrap_or(0);
200 let _ = writeln!(tsv, "{n}\t{incl}\t\\{}", name(self, *c));
201 }
202 let _ = std::fs::write(format!("{}.tsv", p.path), tsv);
203 }
204}