Skip to content

Commit 2da21e3

Browse files
mmsfm - Use mmlogger for printing details.
Not all println! macros are replaced, but a lot. The mmlogger has also been improved with a timestamp, and some logging information.
1 parent 9ca3cb0 commit 2da21e3

File tree

12 files changed

+1031
-588
lines changed

12 files changed

+1031
-588
lines changed

lib/rust/mmcamerasolve-bin/src/main.rs

Lines changed: 205 additions & 256 deletions
Large diffs are not rendered by default.

lib/rust/mmcamerasolve-bin/test.bash

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ fi
3333
# Build release version for faster execution.
3434
echo "Building mmcamerasolve-bin (release mode)..."
3535
# cargo build --release -p mmcamerasolve-bin
36+
# cargo build --release -p mmcamerasolve-bin --features visualization
37+
# cargo build --release -p mmcamerasolve-bin --features logging
3638
cargo build --release -p mmcamerasolve-bin --features visualization,logging
3739
echo ""
3840

lib/rust/mmlogger/src/lib.rs

Lines changed: 289 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -25,52 +25,321 @@
2525
//! completely eliminated by the compiler.
2626
2727
use std::io::Write;
28+
use std::time::SystemTime;
2829

29-
/// Trait for structured logging with zero-cost when disabled.
30-
pub trait Logger {
31-
fn log(&mut self, level: &str, msg: &str);
30+
// ====================================================================
31+
// Log level filter
32+
// ====================================================================
33+
34+
/// Bitmask of allowed log levels for a stream.
35+
///
36+
/// Combine levels with `|`:
37+
/// ```
38+
/// use mmlogger::LevelFilter;
39+
/// let filter = LevelFilter::INFO | LevelFilter::WARN | LevelFilter::ERROR;
40+
/// ```
41+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42+
pub struct LevelFilter(u8);
43+
44+
impl LevelFilter {
45+
pub const NONE: LevelFilter = LevelFilter(0b0000);
46+
pub const DEBUG: LevelFilter = LevelFilter(0b0001);
47+
pub const INFO: LevelFilter = LevelFilter(0b0010);
48+
// TODO: Add "PROGRESS" level between INFO and WARN.
49+
pub const WARN: LevelFilter = LevelFilter(0b0100);
50+
pub const ERROR: LevelFilter = LevelFilter(0b1000);
51+
pub const ALL: LevelFilter = LevelFilter(0b1111);
52+
53+
pub fn allows(self, level: &str) -> bool {
54+
let bit: u8 = match level {
55+
"DEBUG" => 0b0001,
56+
"INFO" => 0b0010,
57+
"WARN" => 0b0100,
58+
"ERROR" => 0b1000,
59+
_ => 0,
60+
};
61+
self.0 & bit != 0
62+
}
63+
}
64+
65+
impl std::ops::BitOr for LevelFilter {
66+
type Output = Self;
67+
fn bitor(self, rhs: Self) -> Self {
68+
LevelFilter(self.0 | rhs.0)
69+
}
3270
}
3371

34-
/// Logger that writes to any `Write` implementor.
35-
pub struct BaseLogger<W: Write> {
36-
writer: W,
72+
// ====================================================================
73+
// Log format
74+
// ====================================================================
75+
76+
/// Output format for a log stream.
77+
pub enum LogFormat {
78+
/// Plain message with no prefix: `Message here`
79+
///
80+
/// WARN and ERROR levels are prefixed with `Warning: ` and `Error: `
81+
/// respectively.
82+
Plain,
83+
/// Level-prefixed: `[LEVEL] Message here`
84+
LevelPrefix,
85+
/// Timestamped: `YYYY-MM-DD HH:MM:SS [LEVEL] Message here`
86+
Timestamp,
3787
}
3888

39-
impl<W: Write> BaseLogger<W> {
40-
pub fn new(writer: W) -> Self {
41-
Self { writer }
89+
// ====================================================================
90+
// Timestamp helpers (stdlib only, no extra dependencies)
91+
// ====================================================================
92+
93+
/// Format a Unix timestamp (seconds since epoch) as `YYYY-MM-DD HH:MM:SS`.
94+
///
95+
/// Uses the Gregorian calendar proleptic algorithm (valid for all
96+
/// positive Unix timestamps).
97+
fn format_unix_timestamp(secs: u64, buf: &mut [u8; 19]) {
98+
// Time-of-day components.
99+
let time_of_day = secs % 86400;
100+
let hh = time_of_day / 3600;
101+
let mm = (time_of_day % 3600) / 60;
102+
let ss = time_of_day % 60;
103+
104+
// Date components via the civil calendar algorithm.
105+
//
106+
// http://howardhinnant.github.io/date_algorithms.html
107+
let days = (secs / 86400) as i64 + 719468; // shift epoch to 0000-03-01
108+
let era = if days >= 0 { days } else { days - 146096 } / 146097;
109+
let doe = days - era * 146097; // day of era [0, 146096]
110+
let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365; // [0, 399]
111+
let y = yoe + era * 400;
112+
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); // [0, 365]
113+
let mp = (5 * doy + 2) / 153; // [0, 11]
114+
let d = doy - (153 * mp + 2) / 5 + 1; // [1, 31]
115+
let m = if mp < 10 { mp + 3 } else { mp - 9 }; // [1, 12]
116+
let y = if m <= 2 { y + 1 } else { y };
117+
118+
// Write directly into the fixed-size buffer.
119+
//
120+
// Format: YYYY-MM-DD HH:MM:SS (19 bytes)
121+
let write2 = |buf: &mut [u8], offset: usize, v: u64| {
122+
buf[offset] = b'0' + (v / 10) as u8;
123+
buf[offset + 1] = b'0' + (v % 10) as u8;
124+
};
125+
let write4 = |buf: &mut [u8], offset: usize, v: i64| {
126+
buf[offset] = b'0' + (v / 1000) as u8;
127+
buf[offset + 1] = b'0' + (v / 100 % 10) as u8;
128+
buf[offset + 2] = b'0' + (v / 10 % 10) as u8;
129+
buf[offset + 3] = b'0' + (v % 10) as u8;
130+
};
131+
132+
write4(buf, 0, y);
133+
buf[4] = b'-';
134+
write2(buf, 5, m as u64);
135+
buf[7] = b'-';
136+
write2(buf, 8, d as u64);
137+
buf[10] = b' ';
138+
write2(buf, 11, hh);
139+
buf[13] = b':';
140+
write2(buf, 14, mm);
141+
buf[16] = b':';
142+
write2(buf, 17, ss);
143+
}
144+
145+
fn write_formatted<W: Write>(
146+
writer: &mut W,
147+
format: &LogFormat,
148+
level: &str,
149+
msg: &str,
150+
) {
151+
match format {
152+
LogFormat::Plain => match level {
153+
"WARN" => {
154+
let _ = writeln!(writer, "Warning: {}", msg);
155+
}
156+
"ERROR" => {
157+
let _ = writeln!(writer, "Error: {}", msg);
158+
}
159+
_ => {
160+
let _ = writeln!(writer, "{}", msg);
161+
}
162+
},
163+
LogFormat::LevelPrefix => {
164+
let _ = writeln!(writer, "[{}] {}", level, msg);
165+
}
166+
LogFormat::Timestamp => {
167+
let secs = SystemTime::now()
168+
.duration_since(SystemTime::UNIX_EPOCH)
169+
.unwrap_or_default()
170+
.as_secs();
171+
let mut buf = [0u8; 19];
172+
format_unix_timestamp(secs, &mut buf);
173+
let ts = std::str::from_utf8(&buf).unwrap_or("0000-00-00 00:00:00");
174+
let _ = writeln!(writer, "[{}] [{}] {}", ts, level, msg);
175+
}
42176
}
43177
}
44178

45-
impl<W: Write> Logger for BaseLogger<W> {
46-
fn log(&mut self, level: &str, msg: &str) {
47-
let _ = writeln!(self.writer, "[{}] {}", level, msg);
179+
// ====================================================================
180+
// Logger trait
181+
// ====================================================================
182+
183+
/// Trait for structured logging with zero-cost when disabled.
184+
pub trait Logger {
185+
// TODO: Get rid of this general method and force people to either
186+
// create a new level, or reuse an existing one. We need to have a
187+
// trade-off between flexibility and performance and simplicity,
188+
// and I think the `log` method is simply too flexible and adds
189+
// the cost of a string rather than a well defined enum that we
190+
// can exhaustively match and ensure all cases are covered.
191+
fn log(&mut self, level: &str, msg: &str);
192+
193+
fn info(&mut self, msg: &str) {
194+
self.log("INFO", msg);
195+
}
196+
197+
// TODO: Add "progress" method between info and warn.
198+
199+
fn warn(&mut self, msg: &str) {
200+
self.log("WARN", msg);
201+
}
202+
203+
fn error(&mut self, msg: &str) {
204+
self.log("ERROR", msg);
205+
}
206+
207+
fn debug(&mut self, msg: &str) {
208+
self.log("DEBUG", msg);
48209
}
49210
}
50211

51-
/// Logger that writes to two outputs simultaneously.
52-
pub struct TeeLogger<W1: Write, W2: Write> {
212+
// ====================================================================
213+
// DualStreamLogger
214+
// ====================================================================
215+
216+
/// Logger that writes to two outputs simultaneously, with independent
217+
/// level filtering and output formatting per stream.
218+
///
219+
/// Default construction via [`DualStreamLogger::new`] uses:
220+
/// - Stream 1: plain format (`Message here`), all levels allowed.
221+
/// - Stream 2: timestamped format (`YYYY-MM-DD HH:MM:SS [LEVEL] Message here`),
222+
/// all levels allowed.
223+
///
224+
pub struct DualStreamLogger<W1: Write, W2: Write> {
53225
writer1: W1,
226+
format1: LogFormat,
227+
levels1: LevelFilter,
54228
writer2: W2,
229+
format2: LogFormat,
230+
levels2: LevelFilter,
55231
}
56232

57-
impl<W1: Write, W2: Write> TeeLogger<W1, W2> {
58-
pub fn new(writer1: W1, writer2: W2) -> Self {
59-
Self { writer1, writer2 }
233+
impl<W1: Write, W2: Write> DualStreamLogger<W1, W2> {
234+
/// Create a `DualStreamLogger` with explicit format and level
235+
/// configuration for each stream.
236+
pub fn new(
237+
writer1: W1,
238+
format1: LogFormat,
239+
levels1: LevelFilter,
240+
writer2: W2,
241+
format2: LogFormat,
242+
levels2: LevelFilter,
243+
) -> Self {
244+
Self {
245+
writer1,
246+
format1,
247+
levels1,
248+
writer2,
249+
format2,
250+
levels2,
251+
}
60252
}
61253
}
62254

63-
impl<W1: Write, W2: Write> Logger for TeeLogger<W1, W2> {
255+
impl<W1: Write, W2: Write> Logger for DualStreamLogger<W1, W2> {
64256
fn log(&mut self, level: &str, msg: &str) {
65-
let _ = writeln!(self.writer1, "[{}] {}", level, msg);
66-
let _ = writeln!(self.writer2, "[{}] {}", level, msg);
257+
if self.levels1.allows(level) {
258+
write_formatted(&mut self.writer1, &self.format1, level, msg);
259+
}
260+
if self.levels2.allows(level) {
261+
write_formatted(&mut self.writer2, &self.format2, level, msg);
262+
}
67263
}
68264
}
69265

266+
// ====================================================================
267+
// NoOpLogger
268+
// ====================================================================
269+
70270
/// No-op logger with zero runtime cost.
71271
pub struct NoOpLogger;
72272

73273
impl Logger for NoOpLogger {
74274
#[inline(always)]
75275
fn log(&mut self, _level: &str, _msg: &str) {}
76276
}
277+
278+
// ====================================================================
279+
// Macros
280+
// ====================================================================
281+
282+
/// Debug print macro that expands to `if DEBUG { eprintln!(...) }`.
283+
///
284+
/// Requires a `const DEBUG: bool` in the calling scope. When `DEBUG`
285+
/// is `false`, the compiler optimizes the branch away entirely.
286+
#[macro_export]
287+
macro_rules! mm_debug_eprintln {
288+
($($arg:tt)*) => {
289+
if DEBUG {
290+
eprintln!($($arg)*);
291+
}
292+
};
293+
}
294+
295+
/// Debug log macro that routes through a logger instead of eprintln.
296+
///
297+
/// Requires a `const DEBUG: bool` in the calling scope and a mutable
298+
/// logger as the first argument. When `DEBUG` is `false`, the compiler
299+
/// optimizes the entire call away.
300+
#[macro_export]
301+
macro_rules! mm_debug_log {
302+
($logger:expr, $msg:literal) => {
303+
if DEBUG {
304+
$logger.debug($msg);
305+
}
306+
};
307+
($logger:expr, $fmt:literal, $($arg:tt)*) => {
308+
if DEBUG {
309+
$logger.debug(&format!($fmt, $($arg)*));
310+
}
311+
};
312+
}
313+
314+
/// Info log macro — avoids the `&format!(...)` boilerplate.
315+
#[macro_export]
316+
macro_rules! mm_info_log {
317+
($logger:expr, $msg:literal) => {
318+
$logger.info($msg);
319+
};
320+
($logger:expr, $fmt:literal, $($arg:tt)*) => {
321+
$logger.info(&format!($fmt, $($arg)*));
322+
};
323+
}
324+
325+
/// Warn log macro — avoids the `&format!(...)` boilerplate.
326+
#[macro_export]
327+
macro_rules! mm_warn_log {
328+
($logger:expr, $msg:literal) => {
329+
$logger.warn($msg);
330+
};
331+
($logger:expr, $fmt:literal, $($arg:tt)*) => {
332+
$logger.warn(&format!($fmt, $($arg)*));
333+
};
334+
}
335+
336+
/// Error log macro — avoids the `&format!(...)` boilerplate.
337+
#[macro_export]
338+
macro_rules! mm_error_log {
339+
($logger:expr, $msg:literal) => {
340+
$logger.error($msg);
341+
};
342+
($logger:expr, $fmt:literal, $($arg:tt)*) => {
343+
$logger.error(&format!($fmt, $($arg)*));
344+
};
345+
}

lib/rust/mmsfm/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ visualization = ["dep:plotters"]
1717
[dependencies]
1818
anyhow = { workspace = true }
1919
log = { workspace = true }
20+
mmlogger = { path = "../mmlogger" }
2021
mmcore = { package = "mmcore_rust", path = "../mmcore" }
2122
mmio = { package = "mmio_rust", path = "../mmio" }
2223
mmoptimise = { package = "mmoptimise_rust", path = "../mmoptimise" }

lib/rust/mmsfm/src/lib.rs

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -49,17 +49,10 @@
4949
5050
#![allow(non_snake_case)]
5151

52-
/// Debug print macro that expands to `if DEBUG { eprintln!(...) }`.
53-
///
54-
/// Requires a `const DEBUG: bool` in the calling scope. When `DEBUG`
55-
/// is `false`, the compiler optimizes the branch away entirely.
56-
macro_rules! mm_debug_eprintln {
57-
($($arg:tt)*) => {
58-
if DEBUG {
59-
eprintln!($($arg)*);
60-
}
61-
};
62-
}
52+
// Import all log macros from mmlogger so sub-modules can use them
53+
// without explicit path prefixes.
54+
#[macro_use]
55+
extern crate mmlogger;
6356

6457
pub mod bundle_adjust;
6558
pub mod bundle_triangulation;

lib/rust/mmsfm/src/sfm_camera/focal_length_evaluator.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,8 +114,12 @@ impl Evaluator for CameraSolveFocalLengthEvaluator {
114114
let mut quality_metrics = SolveQualityMetrics::default();
115115

116116
// Run camera solve with this focal length.
117+
// Use NoOpLogger since evaluations run in parallel threads
118+
// and we only care about the final solve result.
119+
let mut noop_logger = mmlogger::NoOpLogger;
117120
let print_summary = false;
118121
let result = match camera_solve_inner(
122+
&mut noop_logger,
119123
self.frame_range,
120124
&self.markers,
121125
&intrinsics,

0 commit comments

Comments
 (0)