1use std::collections::HashMap;
24use std::sync::Arc;
25use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
26use std::time::{Duration, Instant};
27
28use crate::Engine;
29use crate::hash::{CsId, NO_CS};
30use crate::input::{Level, LineSource};
31
32pub static PENDING: AtomicU64 = AtomicU64::new(0);
34
35pub const INTERVAL: Duration = Duration::from_millis(1);
37
38#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
40pub enum Work {
41 Lua,
43 Fonts,
45 LineBreak,
47 Shipout,
49 Images,
51}
52
53impl Work {
54 pub fn label(self) -> &'static str {
56 match self {
57 Work::Lua => "Lua",
58 Work::Fonts => "font loading",
59 Work::LineBreak => "line breaking",
60 Work::Shipout => "shipping pages (PDF)",
61 Work::Images => "images",
62 }
63 }
64}
65
66#[derive(Clone, Debug, PartialEq, Eq)]
68pub struct FileInfo {
69 pub name: String,
71 pub package: Option<String>,
73}
74
75#[derive(Default)]
77pub struct Origins {
78 pub files: Vec<FileInfo>,
80 places: Vec<(u32, u32)>,
82 ids: HashMap<(u32, u32), u32>,
84 pub history: HashMap<CsId, Vec<u32>>,
87}
88
89impl Origins {
90 pub fn place(&self, origin: u32) -> Option<(&FileInfo, u32)> {
92 let &(file, line) = self.places.get(origin.checked_sub(1)? as usize)?;
93 Some((self.files.get(file.checked_sub(1)? as usize)?, line))
94 }
95
96 pub fn file_of(&self, origin: u32) -> Option<u32> {
98 self.places
99 .get(origin.checked_sub(1)? as usize)
100 .map(|p| p.0)
101 }
102}
103
104fn is_document_file(name: &str) -> bool {
107 let ext = std::path::Path::new(name)
108 .extension()
109 .map(|e| e.to_string_lossy().to_ascii_lowercase());
110 matches!(
111 ext.as_deref(),
112 None | Some(
113 "tex"
114 | "ltx"
115 | "aux"
116 | "toc"
117 | "lof"
118 | "lot"
119 | "bbl"
120 | "ind"
121 | "gls"
122 | "out"
123 | "nav"
124 | "snm"
125 | "vrb"
126 | "loa"
127 | "thm"
128 )
129 )
130}
131
132fn package_name(name: &str) -> String {
135 let path = std::path::Path::new(name);
136 let file = path
137 .file_name()
138 .map_or_else(|| name.to_owned(), |f| f.to_string_lossy().into_owned());
139 let stem = path
140 .file_stem()
141 .map_or_else(|| file.clone(), |f| f.to_string_lossy().into_owned());
142 match path.extension().and_then(|e| e.to_str()) {
143 Some("sty") => stem,
144 Some("cls") => format!("{stem} (class)"),
145 _ => file,
146 }
147}
148
149#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
151pub enum Frame {
152 File(u32),
154 Macro(CsId, u32),
156 Work(Work),
158}
159
160pub struct Profiler {
162 stop: Arc<AtomicBool>,
164 timer: Option<std::thread::JoinHandle<()>>,
165 started: Instant,
166 stacks: HashMap<Box<[Frame]>, u64>,
168 work: Vec<(Work, usize)>,
170 calls: Vec<u64>,
172 first_file: Option<Duration>,
174 document: Option<Duration>,
176 pages: Vec<Duration>,
178}
179
180impl Profiler {
181 fn start() -> Profiler {
183 PENDING.store(0, Ordering::Relaxed);
184 let stop = Arc::new(AtomicBool::new(false));
185 let flag = Arc::clone(&stop);
186 let timer = std::thread::Builder::new()
187 .name("profiler".to_owned())
188 .spawn(move || {
189 let mut last = Instant::now();
190 while !flag.load(Ordering::Relaxed) {
191 std::thread::sleep(INTERVAL);
192 let now = Instant::now();
193 let us = (now - last).as_micros() as u64;
194 last = now;
195 PENDING.fetch_add(us.max(1), Ordering::Relaxed);
196 }
197 })
198 .ok();
199 Profiler {
200 stop,
201 timer,
202 started: Instant::now(),
203 stacks: HashMap::new(),
204 work: Vec::new(),
205 calls: Vec::new(),
206 first_file: None,
207 document: None,
208 pages: Vec::new(),
209 }
210 }
211
212 fn stop_timer(&mut self) {
213 self.stop.store(true, Ordering::Relaxed);
214 if let Some(t) = self.timer.take() {
215 let _ = t.join();
216 }
217 }
218}
219
220impl Drop for Profiler {
221 fn drop(&mut self) {
222 self.stop_timer();
223 PENDING.store(0, Ordering::Relaxed);
224 }
225}
226
227#[derive(Clone, Debug, PartialEq, Eq)]
229pub enum SummaryFrame {
230 File {
232 name: String,
234 package: Option<String>,
236 },
237 Macro {
239 name: String,
241 defined: Option<(String, u32)>,
244 package: Option<String>,
247 },
248 Work(Work),
250}
251
252#[derive(Clone, Debug, Default)]
254pub struct Summary {
255 pub total: Duration,
257 pub first_file: Option<Duration>,
259 pub document: Option<Duration>,
261 pub pages: Vec<Duration>,
263 pub frames: Vec<SummaryFrame>,
265 pub stacks: Vec<(Vec<u32>, u64)>,
268 pub calls: HashMap<String, u64>,
270}
271
272impl Engine {
273 pub fn record_origins(&mut self) {
275 if self.origins.is_none() {
276 self.origins = Some(Box::default());
277 }
278 }
279
280 pub fn start_profile(&mut self) {
282 self.record_origins();
283 self.profiler = Some(Box::new(Profiler::start()));
284 }
285
286 #[inline]
289 pub fn profile_tick(&mut self) {
290 if PENDING.load(Ordering::Relaxed) != 0 {
291 self.take_sample(None);
292 }
293 }
294
295 pub(crate) fn profile_call(&mut self, cs: CsId) {
297 if PENDING.load(Ordering::Relaxed) != 0 {
301 self.take_sample(Some(cs));
302 }
303 let Some(p) = self.profiler.as_mut() else {
304 return;
305 };
306 let i = cs.0 as usize;
307 if p.calls.len() <= i {
308 p.calls.resize(i + 1, 0);
309 }
310 p.calls[i] += 1;
311 if p.document.is_none() && self.hash.name(cs) == b"document" {
313 p.document = Some(p.started.elapsed());
314 }
315 }
316
317 pub fn work_begin(&mut self, w: Work) {
319 if self.profiler.is_none() {
320 return;
321 }
322 self.profile_tick();
323 let depth = self.input.stack.len();
324 if let Some(p) = self.profiler.as_mut() {
325 p.work.push((w, depth));
326 }
327 }
328
329 pub fn work_end(&mut self) {
331 if self.profiler.is_none() {
332 return;
333 }
334 self.profile_tick();
335 if let Some(p) = self.profiler.as_mut()
336 && let Some((w, _)) = p.work.pop()
337 && w == Work::Shipout
338 {
339 p.pages.push(p.started.elapsed());
340 }
341 }
342
343 #[cold]
346 fn take_sample(&mut self, called: Option<CsId>) {
347 let us = PENDING.swap(0, Ordering::Relaxed);
348 if us == 0 || self.profiler.is_none() {
349 return;
350 }
351 let mut frames = Vec::new();
352 let work = self
353 .profiler
354 .as_ref()
355 .map(|p| p.work.clone())
356 .unwrap_or_default();
357 let mut work = work.into_iter().peekable();
358 let mut saw_file = false;
359 for i in 0..self.input.stack.len() {
360 while let Some(&(w, _)) = work.peek().filter(|(_, d)| *d <= i) {
361 frames.push(Frame::Work(w));
362 work.next();
363 }
364 match &self.input.stack[i] {
365 Level::Text(t) if matches!(t.source, LineSource::File { .. }) => {
366 saw_file = true;
367 let id = self.file_id(i);
368 frames.push(Frame::File(id));
369 }
370 Level::Tokens(t) if t.name != NO_CS => {
371 let origin = self
372 .eqtb
373 .meaning(t.name)
374 .mac
375 .as_ref()
376 .map_or(0, |m| m.origin.get());
377 frames.push(Frame::Macro(t.name, origin));
378 }
379 _ => {}
380 }
381 }
382 frames.extend(work.map(|(w, _)| Frame::Work(w)));
383 if let Some(cs) = called {
384 let origin = self
385 .eqtb
386 .meaning(cs)
387 .mac
388 .as_ref()
389 .map_or(0, |m| m.origin.get());
390 frames.push(Frame::Macro(cs, origin));
391 }
392 let Some(p) = self.profiler.as_mut() else {
393 return;
394 };
395 if saw_file && p.first_file.is_none() {
396 p.first_file = Some(p.started.elapsed());
397 }
398 *p.stacks.entry(frames.into_boxed_slice()).or_default() += us;
399 }
400
401 fn file_id(&mut self, i: usize) -> u32 {
404 let Some(Level::Text(t)) = self.input.stack.get(i) else {
405 return 0;
406 };
407 if t.profile_file != 0 {
408 return t.profile_file;
409 }
410 let Some(origins) = self.origins.as_mut() else {
411 return 0;
412 };
413 let names: Vec<String> = self.input.stack[..=i]
417 .iter()
418 .filter_map(|l| match l {
419 Level::Text(t) if matches!(t.source, LineSource::File { .. }) => {
420 Some(String::from_utf8_lossy(&t.name).into_owned())
421 }
422 _ => None,
423 })
424 .collect();
425 let package = names
426 .iter()
427 .skip(1)
428 .find(|n| !is_document_file(n))
429 .map(|n| package_name(n));
430 let name = names.last().cloned().unwrap_or_default();
431 origins.files.push(FileInfo { name, package });
432 let id = origins.files.len() as u32;
433 if let Some(Level::Text(t)) = self.input.stack.get_mut(i) {
434 t.profile_file = id;
435 }
436 id
437 }
438
439 pub fn origin_here(&mut self) -> u32 {
442 if self.origins.is_none() {
443 return 0;
444 }
445 let Some((i, line)) =
446 self.input
447 .stack
448 .iter()
449 .enumerate()
450 .rev()
451 .find_map(|(i, l)| match l {
452 Level::Text(t) if matches!(t.source, LineSource::File { .. }) => {
453 Some((i, t.line.max(0) as u32))
454 }
455 _ => None,
456 })
457 else {
458 return 0;
459 };
460 let file = self.file_id(i);
461 let Some(o) = self.origins.as_mut() else {
462 return 0;
463 };
464 if let Some(&id) = o.ids.get(&(file, line)) {
465 return id;
466 }
467 o.places.push((file, line));
468 let id = o.places.len() as u32;
469 o.ids.insert((file, line), id);
470 id
471 }
472
473 pub(crate) fn record_definition(&mut self, cs: CsId) {
475 let Some(o) = self.origins.as_ref() else {
476 return;
477 };
478 if !o.history.contains_key(&cs) {
479 return;
480 }
481 let origin = match &self.eqtb.meaning(cs).mac {
482 Some(m) if m.origin.get() != 0 => m.origin.get(),
483 _ => self.origin_here(),
484 };
485 if let Some(h) = self.origins.as_mut().and_then(|o| o.history.get_mut(&cs)) {
486 h.push(origin);
487 }
488 }
489
490 pub(crate) fn dump_origins(&self) -> Option<Vec<u8>> {
494 let o = self.origins.as_ref()?;
495 let mut w = crate::format::Writer::default();
496 w.u(o.files.len() as u64);
497 for f in &o.files {
498 w.bytes(f.name.as_bytes());
499 }
500 w.u(o.places.len() as u64);
501 for &(file, line) in &o.places {
502 w.u(file.into());
503 w.u(line.into());
504 }
505 let macros: Vec<(usize, u32)> = self
506 .eqtb
507 .cs
508 .iter()
509 .enumerate()
510 .filter_map(|(i, s)| Some((i, s.val.mac.as_ref()?.origin.get())))
511 .filter(|&(_, o)| o != 0)
512 .collect();
513 w.u(macros.len() as u64);
514 for (cs, origin) in macros {
515 w.u(cs as u64);
516 w.u(origin.into());
517 }
518 Some(w.buf)
519 }
520
521 pub(crate) fn undump_origins(&mut self, data: &[u8]) -> Result<(), crate::format::FormatError> {
525 if self.origins.is_none() {
526 return Ok(());
527 }
528 let kernel = if self.latex_release().is_some() {
529 "LaTeX kernel".to_owned()
530 } else {
531 format!("format {}", self.format_name)
532 };
533 let mut r = crate::format::Reader::new(data);
534 let mut o = Origins::default();
535 for _ in 0..r.u()? {
536 let name = String::from_utf8_lossy(r.bytes()?).into_owned();
537 let file = std::path::Path::new(&name)
538 .file_name()
539 .map(|f| f.to_string_lossy().into_owned())
540 .unwrap_or_default();
541 let package = if file.starts_with("expl3") || file.starts_with("l3") {
542 "expl3".to_owned()
543 } else {
544 kernel.clone()
545 };
546 o.files.push(FileInfo {
547 name,
548 package: Some(package),
549 });
550 }
551 for i in 0..r.u()? {
552 let place = (r.u32()?, r.u32()?);
553 o.places.push(place);
554 o.ids.insert(place, i as u32 + 1);
555 }
556 for _ in 0..r.u()? {
557 let (cs, origin) = (r.u()? as usize, r.u32()?);
558 if let Some(m) = self.eqtb.cs.get(cs).and_then(|s| s.val.mac.as_ref()) {
559 m.origin.set(origin);
560 }
561 }
562 if let Some(old) = self.origins.take() {
565 o.history = old.history;
566 }
567 self.origins = Some(Box::new(o));
568 Ok(())
569 }
570
571 pub fn finish_profile(&mut self) -> Option<Summary> {
573 self.profile_tick();
574 let mut p = self.profiler.take()?;
575 p.stop_timer();
576 let total = p.started.elapsed();
577 let rest = PENDING.swap(0, Ordering::Relaxed);
579 if rest > 0 {
580 *p.stacks.entry(Box::default()).or_default() += rest;
581 }
582 let mut s = Summary {
583 total,
584 first_file: p.first_file,
585 document: p.document,
586 pages: std::mem::take(&mut p.pages),
587 ..Summary::default()
588 };
589 let mut index: HashMap<Frame, u32> = HashMap::new();
590 for (stack, us) in std::mem::take(&mut p.stacks) {
591 let ids = stack
592 .iter()
593 .map(|f| match index.get(f) {
594 Some(&i) => i,
595 None => {
596 let i = s.frames.len() as u32;
597 s.frames.push(self.summary_frame(*f));
598 index.insert(*f, i);
599 i
600 }
601 })
602 .collect();
603 s.stacks.push((ids, us));
604 }
605 s.stacks
606 .sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(&b.0)));
607 for (i, &n) in p.calls.iter().enumerate() {
608 if n > 0 {
609 let name = String::from_utf8_lossy(self.hash.name(CsId(i as u32))).into_owned();
610 *s.calls.entry(name).or_default() += n;
611 }
612 }
613 Some(s)
614 }
615
616 fn summary_frame(&self, f: Frame) -> SummaryFrame {
617 let file = |id: u32| {
618 self.origins
619 .as_ref()
620 .and_then(|o| o.files.get(id.checked_sub(1)? as usize))
621 };
622 match f {
623 Frame::File(id) => match file(id) {
624 Some(fi) => SummaryFrame::File {
625 name: fi.name.clone(),
626 package: fi.package.clone(),
627 },
628 None => SummaryFrame::File {
629 name: "?".to_owned(),
630 package: None,
631 },
632 },
633 Frame::Macro(cs, origin) => {
634 let name = String::from_utf8_lossy(self.hash.name(cs)).into_owned();
635 let place = self.origins.as_ref().and_then(|o| o.place(origin));
636 SummaryFrame::Macro {
637 name,
638 defined: place.map(|(fi, line)| (fi.name.clone(), line)),
639 package: place.and_then(|(fi, _)| fi.package.clone()),
640 }
641 }
642 Frame::Work(w) => SummaryFrame::Work(w),
643 }
644 }
645}
646
647#[cfg(test)]
648mod tests {
649 use super::*;
650
651 #[test]
652 fn packages_of_files() {
653 assert!(is_document_file("chapter1.tex"));
654 assert!(is_document_file("./doc.aux"));
655 assert!(is_document_file("Makefile"));
656 assert!(!is_document_file("tikz.sty"));
657 assert_eq!(
658 package_name("/usr/share/texmf/tex/latex/pgf/tikz.sty"),
659 "tikz"
660 );
661 assert_eq!(package_name("article.cls"), "article (class)");
662 assert_eq!(package_name("t1lmr.fd"), "t1lmr.fd");
663 }
664}