1#![cfg_attr(test, allow(clippy::unwrap_used, clippy::expect_used))]
40
41use std::cell::RefCell;
42
43use pounce_common::types::{Index, Number};
44use pounce_nlp::solve_statistics::IterRecord;
45use tracing::field::{Field, Visit};
46use tracing_subscriber::layer::{Context, Layer};
47use tracing_subscriber::registry::LookupSpan;
48
49pub const ITER_TARGET: &str = "pounce::iteration";
53
54pub const RESTORATION_SPAN: &str = "restoration";
58
59thread_local! {
62 static CAPTURE: RefCell<Option<Vec<IterRecord>>> = const { RefCell::new(None) };
65}
66
67static JSON_LOGGING: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
71
72static GLOBAL_COLLECTOR: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
77
78pub fn iteration_event_wanted() -> bool {
88 if JSON_LOGGING.load(std::sync::atomic::Ordering::Relaxed) {
89 return true;
90 }
91 CAPTURE.with(|c| c.borrow().is_some())
92}
93
94#[must_use = "call finish() to retrieve the captured iteration history"]
102pub struct IterCaptureGuard {
103 prev: Option<Vec<IterRecord>>,
106}
107
108impl IterCaptureGuard {
109 pub fn start() -> Self {
111 let prev = CAPTURE.with(|c| c.borrow_mut().replace(Vec::new()));
112 Self { prev }
113 }
114
115 pub fn finish(mut self) -> Vec<IterRecord> {
119 let prev = self.prev.take();
120 let captured = CAPTURE
121 .with(|c| std::mem::replace(&mut *c.borrow_mut(), prev))
122 .unwrap_or_default();
123 std::mem::forget(self);
126 captured
127 }
128}
129
130impl Drop for IterCaptureGuard {
131 fn drop(&mut self) {
132 let prev = self.prev.take();
134 CAPTURE.with(|c| *c.borrow_mut() = prev);
135 }
136}
137
138fn push_record(rec: IterRecord) {
140 CAPTURE.with(|c| {
141 if let Some(buf) = c.borrow_mut().as_mut() {
142 buf.push(rec);
143 }
144 });
145}
146
147pub fn extend_active_capture(records: &[IterRecord]) {
154 if records.is_empty() {
155 return;
156 }
157 CAPTURE.with(|c| {
158 if let Some(buf) = c.borrow_mut().as_mut() {
159 buf.extend_from_slice(records);
160 }
161 });
162}
163
164#[derive(Default)]
167struct IterVisitor {
168 rec: IterRecord,
169}
170
171impl Visit for IterVisitor {
172 fn record_f64(&mut self, field: &Field, value: f64) {
173 let v = value as Number;
174 match field.name() {
175 "objective" => self.rec.objective = v,
176 "inf_pr" => self.rec.inf_pr = v,
177 "inf_du" => self.rec.inf_du = v,
178 "mu" => self.rec.mu = v,
179 "d_norm" => self.rec.d_norm = v,
180 "regularization" => self.rec.regularization = v,
181 "alpha_dual" => self.rec.alpha_dual = v,
182 "alpha_primal" => self.rec.alpha_primal = v,
183 _ => {}
184 }
185 }
186
187 fn record_i64(&mut self, field: &Field, value: i64) {
188 match field.name() {
189 "iter" => self.rec.iter = value as Index,
190 "ls_trials" => self.rec.ls_trials = value as Index,
191 _ => {}
192 }
193 }
194
195 fn record_u64(&mut self, field: &Field, value: u64) {
196 match field.name() {
201 "iter" => self.rec.iter = value as Index,
202 "ls_trials" => self.rec.ls_trials = value as Index,
203 _ => {}
204 }
205 }
206
207 fn record_str(&mut self, field: &Field, value: &str) {
208 if field.name() == "alpha_char" {
209 self.rec.alpha_primal_char = value.chars().next().unwrap_or(' ');
210 }
211 }
212
213 fn record_debug(&mut self, _field: &Field, _value: &dyn std::fmt::Debug) {
214 }
217}
218
219#[derive(Debug, Default, Clone)]
224pub struct IterCollectorLayer;
225
226impl<S> Layer<S> for IterCollectorLayer
227where
228 S: tracing::Subscriber + for<'a> LookupSpan<'a>,
229{
230 fn on_event(&self, event: &tracing::Event<'_>, ctx: Context<'_, S>) {
231 if event.metadata().target() != ITER_TARGET {
232 return;
233 }
234 if let Some(scope) = ctx.event_scope(event) {
237 for span in scope.from_root() {
238 if span.name() == RESTORATION_SPAN {
239 return;
240 }
241 }
242 }
243 let mut visitor = IterVisitor::default();
244 event.record(&mut visitor);
245 push_record(visitor.rec);
246 }
247}
248
249fn collector_admits(m: &tracing::Metadata<'_>) -> bool {
260 m.is_span() || m.target() == ITER_TARGET
261}
262
263#[must_use = "the collector uninstalls as soon as this guard drops"]
268pub struct CollectorScope {
269 _default: Option<tracing::subscriber::DefaultGuard>,
270}
271
272pub fn collector_scope() -> CollectorScope {
293 use tracing_subscriber::filter::filter_fn;
294 use tracing_subscriber::prelude::*;
295
296 if GLOBAL_COLLECTOR.load(std::sync::atomic::Ordering::Relaxed) {
297 return CollectorScope { _default: None };
298 }
299 let collector = IterCollectorLayer.with_filter(filter_fn(collector_admits));
300 let subscriber = tracing_subscriber::registry().with(collector);
301 CollectorScope {
302 _default: Some(tracing::subscriber::set_default(subscriber)),
303 }
304}
305
306#[must_use = "call finish() to retrieve the captured iteration history"]
314pub struct ScopedIterCapture {
315 capture: IterCaptureGuard,
316 _scope: CollectorScope,
317}
318
319impl ScopedIterCapture {
320 pub fn start() -> Self {
322 let scope = collector_scope();
323 let capture = IterCaptureGuard::start();
324 Self {
325 capture,
326 _scope: scope,
327 }
328 }
329
330 pub fn finish(self) -> Vec<IterRecord> {
335 let Self { capture, _scope } = self;
336 let records = capture.finish();
337 drop(_scope);
338 records
339 }
340}
341
342pub fn with_iter_capture<R>(f: impl FnOnce() -> R) -> (R, Vec<IterRecord>) {
357 let scope = ScopedIterCapture::start();
358 let result = f();
359 (result, scope.finish())
360}
361
362fn level_style(level: tracing::Level) -> anstyle::Style {
366 use pounce_common::style::{ALPHA_HOT, TAN, TIGER_ORANGE};
367 let color = match level {
368 tracing::Level::ERROR => ALPHA_HOT,
369 tracing::Level::WARN => TIGER_ORANGE,
370 tracing::Level::INFO => TAN,
371 tracing::Level::DEBUG => anstyle::RgbColor(0x9a, 0x8c, 0x70),
372 tracing::Level::TRACE => anstyle::RgbColor(0x6a, 0x5d, 0x48),
373 };
374 anstyle::Style::new().fg_color(Some(anstyle::Color::Rgb(color)))
375}
376
377struct TigerFormat;
380
381impl<S, N> tracing_subscriber::fmt::FormatEvent<S, N> for TigerFormat
382where
383 S: tracing::Subscriber + for<'a> LookupSpan<'a>,
384 N: for<'a> tracing_subscriber::fmt::FormatFields<'a> + 'static,
385{
386 fn format_event(
387 &self,
388 ctx: &tracing_subscriber::fmt::FmtContext<'_, S, N>,
389 mut writer: tracing_subscriber::fmt::format::Writer<'_>,
390 event: &tracing::Event<'_>,
391 ) -> std::fmt::Result {
392 let meta = event.metadata();
393 let level = *meta.level();
394 if writer.has_ansi_escapes() {
395 let style = level_style(level);
396 write!(
397 writer,
398 "{}{:>5}{} ",
399 style.render(),
400 level,
401 style.render_reset()
402 )?;
403 } else {
404 write!(writer, "{level:>5} ")?;
405 }
406 write!(writer, "{}: ", meta.target())?;
407 ctx.field_format().format_fields(writer.by_ref(), event)?;
408 writeln!(writer)
409 }
410}
411
412pub fn init_subscriber() {
421 install();
422}
423
424pub fn init_for_tests() {
433 install();
434}
435
436fn install() {
437 use tracing_subscriber::EnvFilter;
438 use tracing_subscriber::filter::filter_fn;
439 use tracing_subscriber::prelude::*;
440
441 let _ = tracing_log::LogTracer::init();
446
447 let want_json = std::env::var("POUNCE_LOG_FORMAT")
448 .map(|v| v.eq_ignore_ascii_case("json"))
449 .unwrap_or(false);
450 JSON_LOGGING.store(want_json, std::sync::atomic::Ordering::Relaxed);
454
455 let claimed = if want_json {
460 let collector = IterCollectorLayer.with_filter(filter_fn(collector_admits));
461 let json_layer = tracing_subscriber::fmt::layer()
462 .json()
463 .with_writer(std::io::stderr)
464 .with_filter(env_filter());
465 tracing_subscriber::registry()
466 .with(json_layer)
467 .with(collector)
468 .try_init()
469 .is_ok()
470 } else {
471 let collector = IterCollectorLayer.with_filter(filter_fn(collector_admits));
472 let ansi = ansi_enabled();
473 let text_layer = tracing_subscriber::fmt::layer()
474 .event_format(TigerFormat)
475 .with_ansi(ansi)
476 .with_writer(std::io::stderr)
477 .with_filter(console_filter());
478 tracing_subscriber::registry()
479 .with(text_layer)
480 .with(collector)
481 .try_init()
482 .is_ok()
483 };
484 if claimed {
485 GLOBAL_COLLECTOR.store(true, std::sync::atomic::Ordering::Relaxed);
486 }
487
488 fn env_filter() -> EnvFilter {
490 EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"))
491 }
492
493 fn console_filter() -> EnvFilter {
496 let base = env_filter();
497 match format!("{ITER_TARGET}=off").parse() {
498 Ok(directive) => base.add_directive(directive),
499 Err(_) => base,
500 }
501 }
502
503 fn ansi_enabled() -> bool {
506 if anstyle_query::clicolor_force() {
507 return true;
508 }
509 if anstyle_query::no_color() {
510 return false;
511 }
512 anstyle_query::term_supports_ansi_color()
513 }
514}
515
516#[cfg(test)]
517mod tests {
518 use super::*;
519
520 fn sample_record(iter: i32, alpha: f64, c: char) -> IterRecord {
521 IterRecord {
522 iter,
523 objective: 1.0,
524 inf_pr: 2.0,
525 inf_du: 3.0,
526 mu: 4.0,
527 d_norm: 5.0,
528 regularization: 6.0,
529 alpha_dual: 7.0,
530 alpha_primal: alpha,
531 alpha_primal_char: c,
532 ls_trials: 1,
533 }
534 }
535
536 #[test]
537 fn iteration_event_wanted_tracks_active_capture() {
538 assert!(!iteration_event_wanted());
541 let guard = IterCaptureGuard::start();
542 assert!(iteration_event_wanted(), "capture active → event wanted");
543 let _ = guard.finish();
544 assert!(
545 !iteration_event_wanted(),
546 "capture ended → event suppressed"
547 );
548 }
549
550 #[test]
551 fn guard_captures_pushed_records() {
552 let guard = IterCaptureGuard::start();
553 push_record(sample_record(0, 1.0, ' '));
554 push_record(sample_record(1, 0.5, 'R'));
555 let got = guard.finish();
556 assert_eq!(got.len(), 2);
557 assert_eq!(got[1].iter, 1);
558 assert_eq!(got[1].alpha_primal_char, 'R');
559 }
560
561 #[test]
562 fn no_guard_means_records_are_dropped() {
563 push_record(sample_record(0, 1.0, ' '));
565 let guard = IterCaptureGuard::start();
566 let got = guard.finish();
567 assert!(got.is_empty());
568 }
569
570 #[test]
571 fn guard_restores_previous_slot_on_finish() {
572 let outer = IterCaptureGuard::start();
573 push_record(sample_record(0, 1.0, ' '));
574 {
575 let inner = IterCaptureGuard::start();
576 push_record(sample_record(99, 0.1, 'R'));
577 let inner_got = inner.finish();
578 assert_eq!(inner_got.len(), 1);
579 assert_eq!(inner_got[0].iter, 99);
580 }
581 push_record(sample_record(1, 1.0, ' '));
583 let outer_got = outer.finish();
584 assert_eq!(outer_got.len(), 2);
585 assert_eq!(outer_got[0].iter, 0);
586 assert_eq!(outer_got[1].iter, 1);
587 }
588
589 #[test]
590 fn collector_excludes_restoration_nested_iterations() {
591 use tracing_subscriber::filter::filter_fn;
592 use tracing_subscriber::prelude::*;
593
594 fn emit(iter: i64, ch: char) {
595 let s = ch.to_string();
596 tracing::info!(
597 target: ITER_TARGET,
598 iter = iter,
599 objective = 0.0,
600 alpha_primal = 1.0,
601 alpha_char = s.as_str(),
602 );
603 }
604
605 let collector = IterCollectorLayer.with_filter(filter_fn(collector_admits));
611 let subscriber = tracing_subscriber::registry().with(collector);
612
613 let captured = tracing::subscriber::with_default(subscriber, || {
614 let guard = IterCaptureGuard::start();
615 emit(0, ' '); {
617 let _resto = tracing::info_span!("restoration").entered();
618 let _inner_solve = tracing::info_span!("solve").entered();
619 let _inner_iter = tracing::info_span!("iteration").entered();
620 emit(99, 'R'); }
622 emit(1, ' '); guard.finish()
624 });
625
626 let iters: Vec<i32> = captured.iter().map(|r| r.iter).collect();
627 assert_eq!(
628 iters,
629 vec![0, 1],
630 "inner restoration iteration leaked: {iters:?}"
631 );
632 }
633
634 #[test]
635 fn log_records_bridge_into_tracing() {
636 use std::sync::{Arc, Mutex};
637 use tracing_subscriber::prelude::*;
638
639 #[derive(Clone)]
641 struct CaptureLayer {
642 buf: Arc<Mutex<Vec<String>>>,
643 }
644 impl<S: tracing::Subscriber> tracing_subscriber::Layer<S> for CaptureLayer {
645 fn on_event(
646 &self,
647 event: &tracing::Event<'_>,
648 _ctx: tracing_subscriber::layer::Context<'_, S>,
649 ) {
650 struct V<'a>(&'a mut Vec<String>);
651 impl tracing::field::Visit for V<'_> {
652 fn record_debug(&mut self, f: &Field, value: &dyn std::fmt::Debug) {
653 if f.name() == "message" {
654 self.0.push(format!("{value:?}"));
655 }
656 }
657 }
658 let mut g = self.buf.lock().unwrap_or_else(|p| p.into_inner());
659 event.record(&mut V(&mut g));
660 }
661 }
662
663 let buf = Arc::new(Mutex::new(Vec::new()));
664 let subscriber = tracing_subscriber::registry().with(CaptureLayer { buf: buf.clone() });
665
666 let _ = tracing_log::LogTracer::init();
668 tracing::subscriber::with_default(subscriber, || {
669 log::error!(target: "some_transitive_dep", "bridged log record");
671 });
672
673 let got = buf.lock().unwrap_or_else(|p| p.into_inner());
674 assert!(
675 got.iter().any(|m| m.contains("bridged log record")),
676 "log record did not reach the tracing layer; captured: {got:?}"
677 );
678 }
679
680 fn emit_iter(iter: i64, ch: char) {
682 let s = ch.to_string();
683 tracing::info!(
684 target: ITER_TARGET,
685 iter = iter,
686 objective = 0.5,
687 alpha_primal = 1.0,
688 alpha_char = s.as_str(),
689 );
690 }
691
692 #[test]
693 fn extend_active_capture_appends_to_enclosing_buffer() {
694 let outer = IterCaptureGuard::start();
695 push_record(sample_record(0, 1.0, ' '));
696 let inner = IterCaptureGuard::start();
697 push_record(sample_record(1, 0.5, ' '));
698 let inner_got = inner.finish();
699 extend_active_capture(&inner_got);
700 let outer_got = outer.finish();
701 let iters: Vec<i32> = outer_got.iter().map(|r| r.iter).collect();
702 assert_eq!(iters, vec![0, 1]);
703
704 extend_active_capture(&inner_got);
705 let fresh = IterCaptureGuard::start();
706 assert!(fresh.finish().is_empty());
707 }
708
709 #[test]
710 fn with_iter_capture_captures_events_and_threads_result() {
711 let (result, records) = with_iter_capture(|| {
712 emit_iter(0, ' ');
713 emit_iter(1, 'R');
714 "sentinel"
715 });
716 assert_eq!(result, "sentinel");
717 assert_eq!(records.len(), 2);
718 assert_eq!(records[0].iter, 0);
719 assert_eq!(records[1].iter, 1);
720 assert_eq!(records[1].alpha_primal_char, 'R');
721 assert!((records[1].objective - 0.5).abs() < 1e-12);
722 }
723
724 #[test]
725 fn with_iter_capture_ignores_events_outside_scope() {
726 emit_iter(7, ' '); let ((), records) = with_iter_capture(|| ());
728 emit_iter(8, ' '); assert!(records.is_empty());
730 assert!(
731 !iteration_event_wanted(),
732 "capture slot must be torn down after with_iter_capture returns"
733 );
734 }
735
736 #[test]
737 fn with_iter_capture_excludes_restoration_subsolve() {
738 let ((), records) = with_iter_capture(|| {
739 emit_iter(0, ' ');
740 {
741 let _resto = tracing::info_span!("restoration").entered();
742 let _inner_iter = tracing::info_span!("iteration").entered();
743 emit_iter(99, 'R');
744 }
745 emit_iter(1, ' ');
746 });
747 let iters: Vec<i32> = records.iter().map(|r| r.iter).collect();
748 assert_eq!(
749 iters,
750 vec![0, 1],
751 "inner restoration iteration leaked: {iters:?}"
752 );
753 }
754
755 #[test]
756 fn scoped_iter_capture_nesting_restores_outer_buffer() {
757 let outer = ScopedIterCapture::start();
758 emit_iter(0, ' ');
759 let ((), inner) = with_iter_capture(|| emit_iter(99, 'R'));
760 emit_iter(1, ' ');
761 let outer_got = outer.finish();
762 assert_eq!(inner.len(), 1);
763 assert_eq!(inner[0].iter, 99);
764 let iters: Vec<i32> = outer_got.iter().map(|r| r.iter).collect();
765 assert_eq!(iters, vec![0, 1]);
766 }
767
768 #[test]
769 fn collector_scope_feeds_manual_guard() {
770 let scope = collector_scope();
771 let guard = IterCaptureGuard::start();
772 emit_iter(0, ' ');
773 let records = guard.finish();
774 drop(scope);
775 assert_eq!(records.len(), 1);
776 assert_eq!(records[0].iter, 0);
777
778 let guard = IterCaptureGuard::start();
779 emit_iter(1, ' ');
780 assert!(guard.finish().is_empty());
781 }
782
783 #[test]
784 fn with_iter_capture_is_panic_safe() {
785 let unwound = std::panic::catch_unwind(|| {
786 let _ = with_iter_capture(|| panic!("solve blew up"));
787 });
788 assert!(unwound.is_err());
789 assert!(
790 !iteration_event_wanted(),
791 "capture slot must be restored when the closure unwinds"
792 );
793 }
794
795 #[test]
796 fn iter_record_default_and_assignment() {
797 let mut v = IterVisitor::default();
803 v.rec.iter = 7;
804 v.rec.alpha_primal = 0.25;
805 v.rec.alpha_primal_char = 'S';
806 assert_eq!(v.rec.iter, 7);
807 assert_eq!(v.rec.alpha_primal_char, 'S');
808 }
809}