use std::collections::HashMap;
use std::fmt::Write as _;
use crate::Engine;
use crate::hash::CsId;
use crate::input::Level;
#[derive(Default)]
pub struct MacroProfile {
path: String,
calls: HashMap<CsId, u64>,
inclusive: HashMap<CsId, u64>,
total: u64,
samples: u64,
}
const SAMPLE_EVERY: u64 = 64;
impl MacroProfile {
pub fn from_env() -> Option<Box<MacroProfile>> {
let path = std::env::var("RETEX_MACRO_PROFILE").ok()?;
Some(Box::new(MacroProfile {
path,
..MacroProfile::default()
}))
}
}
fn package_of(name: &str) -> String {
let n = name.trim_start_matches('_');
if name.contains(':') || name.starts_with("__") {
let module = n.split(['_', ':']).next().unwrap_or(n);
return format!("expl3 {module}");
}
for p in [
"pgfmath",
"pgfplots",
"pgfkeys",
"pgfsys",
"pgfutil",
"pgfpoint",
"pgfpath",
"pgf@",
"pgf",
"tikz@",
"tikz",
"tcb@",
"tcb",
"sisetup",
"si@",
"SI",
"num",
"bbl@",
"beamer@",
"beamer",
"Hy@",
"HyPsd@",
"hyper",
"Gin@",
"Gread@",
"@float",
"@makecaption",
"@ifnextchar",
"@ifstar",
"@xifnextchar",
"@tempa",
"@for",
"@tfor",
"@tempb",
"@protected@",
"@expl@",
"fontspec",
"f@",
] {
if n.starts_with(p) {
return p.trim_end_matches('@').to_owned();
}
}
if n.contains('@') {
return "LaTeX internals (@)".to_owned();
}
"(other)".to_owned()
}
impl Engine {
#[cold]
pub(crate) fn profile_macro_call(&mut self, cs: CsId) {
let Some(p) = self.macro_profile.as_mut() else {
return;
};
*p.calls.entry(cs).or_default() += 1;
p.total += 1;
if p.total % SAMPLE_EVERY != 0 {
return;
}
p.samples += 1;
let mut seen: Vec<CsId> = vec![cs];
for level in &self.input.stack {
if let Level::Tokens(t) = level
&& t.name != crate::hash::NO_CS
&& !seen.contains(&t.name)
{
seen.push(t.name);
}
}
for c in seen {
*p.inclusive.entry(c).or_default() += 1;
}
}
pub fn write_macro_profile(&mut self) {
let Some(p) = self.macro_profile.take() else {
return;
};
let name = |e: &Engine, c: CsId| String::from_utf8_lossy(e.hash.name(c)).into_owned();
let mut out = String::new();
let _ = writeln!(
out,
"macro calls: {}, input-stack samples: {} (every {SAMPLE_EVERY} calls)\n",
p.total, p.samples
);
let mut by_pkg: HashMap<String, (u64, u64)> = HashMap::new();
for (&c, &n) in &p.calls {
by_pkg.entry(package_of(&name(self, c))).or_default().0 += n;
}
for (&c, &n) in &p.inclusive {
let e = by_pkg.entry(package_of(&name(self, c))).or_default();
e.1 = e.1.max(n);
}
let mut pk: Vec<_> = by_pkg.into_iter().collect();
pk.sort_by(|a, b| b.1.0.cmp(&a.1.0));
let _ = writeln!(
out,
"{:>7} {:>7} package (calls %, max inclusive %)",
"calls", "incl"
);
for (k, (c, i)) in pk.iter().take(40) {
let _ = writeln!(
out,
"{:>6.2}% {:>6.2}% {k}",
*c as f64 * 100.0 / p.total.max(1) as f64,
*i as f64 * 100.0 / p.samples.max(1) as f64
);
}
let mut calls: Vec<_> = p.calls.iter().map(|(&c, &n)| (n, c)).collect();
calls.sort_by(|a, b| b.0.cmp(&a.0));
let _ = writeln!(out, "\ntop macros by calls:");
for (n, c) in calls.iter().take(60) {
let _ = writeln!(
out,
"{:>6.2}% {:>10} \\{}",
*n as f64 * 100.0 / p.total.max(1) as f64,
n,
name(self, *c)
);
}
let mut incl: Vec<_> = p.inclusive.iter().map(|(&c, &n)| (n, c)).collect();
incl.sort_by(|a, b| b.0.cmp(&a.0));
let _ = writeln!(out, "\ntop macros by inclusive samples:");
for (n, c) in incl.iter().take(60) {
let _ = writeln!(
out,
"{:>6.2}% \\{}",
*n as f64 * 100.0 / p.samples.max(1) as f64,
name(self, *c)
);
}
let _ = std::fs::write(&p.path, out);
let mut tsv = String::from("calls\tinclusive\tname\n");
for (n, c) in &calls {
let incl = p.inclusive.get(c).copied().unwrap_or(0);
let _ = writeln!(tsv, "{n}\t{incl}\t\\{}", name(self, *c));
}
let _ = std::fs::write(format!("{}.tsv", p.path), tsv);
}
}