1use crate::types::Number;
7use crate::utils::{cpu_time, sys_time, wallclock_time};
8use std::cell::Cell;
9use std::rc::Rc;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum DeadlineKind {
14 Wall,
16 Cpu,
18}
19
20#[derive(Debug, Clone)]
42pub struct Deadline {
43 inner: Rc<DeadlineInner>,
44}
45
46#[derive(Debug)]
47struct DeadlineInner {
48 wall_start: Number,
49 cpu_start: Number,
50 max_wall: Number,
51 max_cpu: Number,
52}
53
54impl Deadline {
55 pub fn new(max_wall: Number, max_cpu: Number) -> Self {
60 Self {
61 inner: Rc::new(DeadlineInner {
62 wall_start: wallclock_time(),
63 cpu_start: cpu_time(),
64 max_wall,
65 max_cpu,
66 }),
67 }
68 }
69
70 pub fn exceeded(&self) -> Option<DeadlineKind> {
77 if cpu_time() - self.inner.cpu_start >= self.inner.max_cpu {
78 return Some(DeadlineKind::Cpu);
79 }
80 if wallclock_time() - self.inner.wall_start >= self.inner.max_wall {
81 return Some(DeadlineKind::Wall);
82 }
83 None
84 }
85
86 pub fn max_wall(&self) -> Number {
88 self.inner.max_wall
89 }
90
91 pub fn max_cpu(&self) -> Number {
93 self.inner.max_cpu
94 }
95
96 pub fn remaining_wall(&self) -> Number {
103 self.inner.max_wall - (wallclock_time() - self.inner.wall_start)
104 }
105
106 pub fn remaining_cpu(&self) -> Number {
108 self.inner.max_cpu - (cpu_time() - self.inner.cpu_start)
109 }
110}
111
112#[derive(Debug)]
116pub struct TimedTask {
117 enabled: Cell<bool>,
118 start_called: Cell<bool>,
119 end_called: Cell<bool>,
120 start_cpu: Cell<Number>,
121 start_sys: Cell<Number>,
122 start_wall: Cell<Number>,
123 total_cpu: Cell<Number>,
124 total_sys: Cell<Number>,
125 total_wall: Cell<Number>,
126}
127
128impl Default for TimedTask {
129 fn default() -> Self {
130 Self {
131 enabled: Cell::new(true),
132 start_called: Cell::new(false),
133 end_called: Cell::new(true),
134 start_cpu: Cell::new(0.0),
135 start_sys: Cell::new(0.0),
136 start_wall: Cell::new(0.0),
137 total_cpu: Cell::new(0.0),
138 total_sys: Cell::new(0.0),
139 total_wall: Cell::new(0.0),
140 }
141 }
142}
143
144impl TimedTask {
145 pub fn new() -> Self {
146 Self::default()
147 }
148
149 pub fn enable(&self) {
150 self.enabled.set(true);
151 }
152 pub fn disable(&self) {
153 self.enabled.set(false);
154 }
155 pub fn is_enabled(&self) -> bool {
156 self.enabled.get()
157 }
158 pub fn is_started(&self) -> bool {
159 self.start_called.get()
160 }
161
162 pub fn reset(&self) {
163 self.total_cpu.set(0.0);
164 self.total_sys.set(0.0);
165 self.total_wall.set(0.0);
166 self.start_called.set(false);
167 self.end_called.set(true);
168 }
169
170 pub fn start(&self) {
171 if !self.enabled.get() {
172 return;
173 }
174 self.end_called.set(false);
175 self.start_called.set(true);
176 self.start_cpu.set(cpu_time());
177 self.start_sys.set(sys_time());
178 self.start_wall.set(wallclock_time());
179 }
180
181 pub fn end(&self) {
182 if !self.enabled.get() {
183 return;
184 }
185 self.end_called.set(true);
186 self.start_called.set(false);
187 self.total_cpu
188 .set(self.total_cpu.get() + cpu_time() - self.start_cpu.get());
189 self.total_sys
190 .set(self.total_sys.get() + sys_time() - self.start_sys.get());
191 self.total_wall
192 .set(self.total_wall.get() + wallclock_time() - self.start_wall.get());
193 }
194
195 pub fn end_if_started(&self) {
196 if !self.enabled.get() {
197 return;
198 }
199 if self.start_called.get() {
200 self.end();
201 }
202 }
203
204 pub fn total_cpu_time(&self) -> Number {
205 self.total_cpu.get()
206 }
207 pub fn total_sys_time(&self) -> Number {
208 self.total_sys.get()
209 }
210 pub fn total_wallclock_time(&self) -> Number {
211 self.total_wall.get()
212 }
213
214 pub fn live_wallclock_time(&self) -> Number {
220 if self.enabled.get() && self.start_called.get() {
221 self.total_wall.get() + wallclock_time() - self.start_wall.get()
222 } else {
223 self.total_wall.get()
224 }
225 }
226
227 pub fn live_cpu_time(&self) -> Number {
230 if self.enabled.get() && self.start_called.get() {
231 self.total_cpu.get() + cpu_time() - self.start_cpu.get()
232 } else {
233 self.total_cpu.get()
234 }
235 }
236
237 pub fn guard(&self) -> TimedGuard<'_> {
241 self.start();
242 TimedGuard { task: Some(self) }
243 }
244}
245
246#[must_use = "the guard ends the timer when dropped; bind it to a variable"]
250pub struct TimedGuard<'a> {
251 task: Option<&'a TimedTask>,
252}
253
254impl<'a> TimedGuard<'a> {
255 pub fn stop(mut self) {
259 if let Some(t) = self.task.take() {
260 t.end_if_started();
261 }
262 }
263}
264
265impl<'a> Drop for TimedGuard<'a> {
266 fn drop(&mut self) {
267 if let Some(t) = self.task.take() {
268 t.end_if_started();
269 }
270 }
271}
272
273#[derive(Debug, Default)]
279pub struct TimingStatistics {
280 pub overall_alg: TimedTask,
281 pub print_problem_statistics: TimedTask,
282 pub initialize_iterates: TimedTask,
283 pub update_hessian: TimedTask,
284 pub output_iteration: TimedTask,
285 pub update_barrier_parameter: TimedTask,
286 pub compute_search_direction: TimedTask,
287 pub compute_acceptable_trial_point: TimedTask,
288 pub accept_trial_point: TimedTask,
289 pub check_convergence: TimedTask,
290
291 pub linear_system_factorization: TimedTask,
292 pub linear_system_back_solve: TimedTask,
293 pub linear_system_structure_converter: TimedTask,
294 pub linear_system_structure_converter_init: TimedTask,
295 pub quality_function_search: TimedTask,
296 pub total_callback_time: TimedTask,
297 pub total_function_evaluation_time: TimedTask,
298 pub eval_obj: TimedTask,
299 pub eval_grad_obj: TimedTask,
300 pub eval_constr: TimedTask,
301 pub eval_constr_jac: TimedTask,
302 pub eval_lag_hess: TimedTask,
303}
304
305impl TimingStatistics {
306 pub fn new() -> Self {
307 Self::default()
308 }
309
310 pub fn report(&self) -> String {
318 use std::fmt::Write as _;
319 let mut s = String::new();
320 let row = |s: &mut String, label: &str, t: &TimedTask| {
321 let _ = writeln!(
322 s,
323 "{label:<42} {wall:>10.3}s",
324 wall = t.total_wallclock_time()
325 );
326 };
327 s.push_str("\nTiming Statistics:\n");
328 row(
329 &mut s,
330 "OverallAlgorithm....................:",
331 &self.overall_alg,
332 );
333 row(
334 &mut s,
335 " InitializeIterates.................:",
336 &self.initialize_iterates,
337 );
338 row(
339 &mut s,
340 " UpdateHessian......................:",
341 &self.update_hessian,
342 );
343 row(
344 &mut s,
345 " OutputIteration....................:",
346 &self.output_iteration,
347 );
348 row(
349 &mut s,
350 " UpdateBarrierParameter.............:",
351 &self.update_barrier_parameter,
352 );
353 row(
354 &mut s,
355 " ComputeSearchDirection.............:",
356 &self.compute_search_direction,
357 );
358 row(
359 &mut s,
360 " ComputeAcceptableTrialPoint........:",
361 &self.compute_acceptable_trial_point,
362 );
363 row(
364 &mut s,
365 " AcceptTrialPoint...................:",
366 &self.accept_trial_point,
367 );
368 row(
369 &mut s,
370 " CheckConvergence...................:",
371 &self.check_convergence,
372 );
373 row(
374 &mut s,
375 "LinearSystemFactorization...........:",
376 &self.linear_system_factorization,
377 );
378 row(
379 &mut s,
380 "LinearSystemBackSolve...............:",
381 &self.linear_system_back_solve,
382 );
383 row(
384 &mut s,
385 "QualityFunctionSearch...............:",
386 &self.quality_function_search,
387 );
388 row(
389 &mut s,
390 "TotalFunctionEvaluations............:",
391 &self.total_function_evaluation_time,
392 );
393 row(
394 &mut s,
395 " ObjectiveFunctionEvaluations.......:",
396 &self.eval_obj,
397 );
398 row(
399 &mut s,
400 " ObjectiveGradientEvaluations.......:",
401 &self.eval_grad_obj,
402 );
403 row(
404 &mut s,
405 " ConstraintEvaluations..............:",
406 &self.eval_constr,
407 );
408 row(
409 &mut s,
410 " ConstraintJacobianEvaluations......:",
411 &self.eval_constr_jac,
412 );
413 row(
414 &mut s,
415 " LagrangianHessianEvaluations.......:",
416 &self.eval_lag_hess,
417 );
418 s
419 }
420
421 pub fn wall_time_breakdown(&self) -> Vec<(&'static str, Number)> {
437 let factorization = self.linear_system_factorization.total_wallclock_time();
438 let back_solve = self.linear_system_back_solve.total_wallclock_time();
439 vec![
440 ("overall_alg", self.overall_alg.total_wallclock_time()),
441 ("update_hessian", self.update_hessian.total_wallclock_time()),
442 (
443 "compute_search_direction",
444 self.compute_search_direction.total_wallclock_time(),
445 ),
446 ("linear_system_total", factorization + back_solve),
447 ("linear_system_factorization", factorization),
448 ("linear_system_back_solve", back_solve),
449 (
450 "function_evaluations_total",
451 self.total_function_evaluation_time.total_wallclock_time(),
452 ),
453 ("eval_objective", self.eval_obj.total_wallclock_time()),
454 ("eval_gradient", self.eval_grad_obj.total_wallclock_time()),
455 ("eval_constraints", self.eval_constr.total_wallclock_time()),
456 (
457 "eval_constraint_jacobian",
458 self.eval_constr_jac.total_wallclock_time(),
459 ),
460 (
461 "eval_lagrangian_hessian",
462 self.eval_lag_hess.total_wallclock_time(),
463 ),
464 (
465 "total_callback",
466 self.total_callback_time.total_wallclock_time(),
467 ),
468 ]
469 }
470
471 pub fn set_detailed_enabled(&self, on: bool) {
488 let set = |t: &TimedTask| {
489 if on {
490 t.enable();
491 } else {
492 t.disable();
493 }
494 };
495 set(&self.print_problem_statistics);
497 set(&self.initialize_iterates);
498 set(&self.update_hessian);
499 set(&self.output_iteration);
500 set(&self.update_barrier_parameter);
501 set(&self.compute_search_direction);
502 set(&self.compute_acceptable_trial_point);
503 set(&self.accept_trial_point);
504 set(&self.check_convergence);
505 set(&self.linear_system_factorization);
506 set(&self.linear_system_back_solve);
507 set(&self.linear_system_structure_converter);
508 set(&self.linear_system_structure_converter_init);
509 set(&self.quality_function_search);
510 set(&self.total_callback_time);
511 set(&self.total_function_evaluation_time);
512 set(&self.eval_obj);
513 set(&self.eval_grad_obj);
514 set(&self.eval_constr);
515 set(&self.eval_constr_jac);
516 set(&self.eval_lag_hess);
517 }
518
519 pub fn reset(&self) {
521 self.overall_alg.reset();
522 self.print_problem_statistics.reset();
523 self.initialize_iterates.reset();
524 self.update_hessian.reset();
525 self.output_iteration.reset();
526 self.update_barrier_parameter.reset();
527 self.compute_search_direction.reset();
528 self.compute_acceptable_trial_point.reset();
529 self.accept_trial_point.reset();
530 self.check_convergence.reset();
531 self.linear_system_factorization.reset();
532 self.linear_system_back_solve.reset();
533 self.linear_system_structure_converter.reset();
534 self.linear_system_structure_converter_init.reset();
535 self.quality_function_search.reset();
536 self.total_callback_time.reset();
537 self.total_function_evaluation_time.reset();
538 self.eval_obj.reset();
539 self.eval_grad_obj.reset();
540 self.eval_constr.reset();
541 self.eval_constr_jac.reset();
542 self.eval_lag_hess.reset();
543 }
544}
545
546#[cfg(test)]
547mod tests {
548 use super::*;
549
550 #[test]
551 fn deadline_unbounded_never_trips() {
552 let d = Deadline::new(1e6, 1e6);
555 assert!(d.exceeded().is_none());
556 assert_eq!(d.max_wall(), 1e6);
557 assert_eq!(d.max_cpu(), 1e6);
558 }
559
560 #[test]
561 fn deadline_zero_wall_trips_wall() {
562 let d = Deadline::new(0.0, 1e6);
566 for _ in 0..10_000 {
570 if d.exceeded().is_some() {
571 break;
572 }
573 std::hint::black_box(0u64);
574 }
575 assert_eq!(d.exceeded(), Some(DeadlineKind::Wall));
576 }
577
578 #[test]
579 fn deadline_zero_cpu_takes_priority() {
580 let d = Deadline::new(0.0, 0.0);
584 for _ in 0..10_000 {
585 if d.exceeded().is_some() {
586 break;
587 }
588 std::hint::black_box(0u64);
589 }
590 assert_eq!(d.exceeded(), Some(DeadlineKind::Cpu));
591 }
592
593 #[test]
594 fn deadline_remaining_reports_budget_left() {
595 let d = Deadline::new(1e6, 1e6);
599 let rw = d.remaining_wall();
600 let rc = d.remaining_cpu();
601 assert!(rw > 0.0 && rw <= 1e6, "remaining_wall out of range: {rw}");
602 assert!(rc > 0.0 && rc <= 1e6, "remaining_cpu out of range: {rc}");
603 }
604
605 #[test]
606 fn deadline_remaining_goes_nonpositive_once_crossed() {
607 let d = Deadline::new(0.0, 1e6);
610 for _ in 0..10_000 {
611 if d.exceeded().is_some() {
612 break;
613 }
614 std::hint::black_box(0u64);
615 }
616 assert!(
617 d.remaining_wall() <= 0.0,
618 "remaining_wall should be non-positive once the budget is crossed"
619 );
620 }
621
622 #[test]
623 fn deadline_is_cheaply_clonable_and_shares_start() {
624 let d = Deadline::new(1e6, 1e6);
628 let d2 = d.clone();
629 assert_eq!(d2.max_wall(), d.max_wall());
630 assert_eq!(d2.max_cpu(), d.max_cpu());
631 assert!(d2.exceeded().is_none());
632 }
633
634 #[test]
635 fn start_end_accumulates_nonneg() {
636 let t = TimedTask::new();
637 t.start();
638 for _ in 0..1000 {
639 std::hint::black_box(0u64);
640 }
641 t.end();
642 assert!(t.total_wallclock_time() >= 0.0);
643 }
644
645 #[test]
646 fn disabled_is_noop() {
647 let t = TimedTask::new();
648 t.disable();
649 t.start();
650 t.end();
651 assert_eq!(t.total_wallclock_time(), 0.0);
652 }
653
654 #[test]
655 fn set_detailed_enabled_gates_all_but_overall_alg() {
656 let stats = TimingStatistics::new();
657 assert!(stats.overall_alg.is_enabled());
659 assert!(stats.eval_obj.is_enabled());
660 assert!(stats.check_convergence.is_enabled());
661
662 stats.set_detailed_enabled(false);
667 assert!(stats.overall_alg.is_enabled(), "overall_alg must stay live");
668 assert!(!stats.eval_obj.is_enabled());
669 assert!(!stats.check_convergence.is_enabled());
670 assert!(!stats.total_function_evaluation_time.is_enabled());
671 assert!(!stats.linear_system_factorization.is_enabled());
672
673 stats.eval_obj.start();
675 stats.eval_obj.end();
676 assert_eq!(stats.eval_obj.total_wallclock_time(), 0.0);
677
678 stats.set_detailed_enabled(true);
680 assert!(stats.eval_obj.is_enabled());
681 assert!(stats.check_convergence.is_enabled());
682 }
683
684 #[test]
685 fn end_if_started_handles_unstarted() {
686 let t = TimedTask::new();
687 t.end_if_started();
688 assert_eq!(t.total_wallclock_time(), 0.0);
689 }
690
691 #[test]
692 fn wall_time_breakdown_reports_subsystems() {
693 let stats = TimingStatistics::new();
694 stats.linear_system_factorization.start();
698 stats.linear_system_factorization.end();
699 stats.eval_lag_hess.start();
700 stats.eval_lag_hess.end();
701
702 let bd = stats.wall_time_breakdown();
703 let get = |k: &str| bd.iter().find(|(label, _)| *label == k).map(|(_, v)| *v);
704
705 for key in [
707 "overall_alg",
708 "linear_system_total",
709 "linear_system_factorization",
710 "linear_system_back_solve",
711 "function_evaluations_total",
712 "eval_objective",
713 "eval_gradient",
714 "eval_constraints",
715 "eval_constraint_jacobian",
716 "eval_lagrangian_hessian",
717 ] {
718 assert!(get(key).is_some(), "missing breakdown key {key}");
719 assert!(get(key).unwrap() >= 0.0, "negative time for {key}");
720 }
721
722 let total = get("linear_system_total").unwrap();
724 let fact = get("linear_system_factorization").unwrap();
725 let back = get("linear_system_back_solve").unwrap();
726 assert_eq!(total, fact + back);
727 }
728}