Skip to content

Commit 2c4150c

Browse files
mmlogger - Support multi-threaded logging
This was added specifically to support writing the log messages from multiple threads as part of the differential evolution and uniform grid search algorithms.
1 parent 2da21e3 commit 2c4150c

File tree

13 files changed

+260
-79
lines changed

13 files changed

+260
-79
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

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

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ fn run() -> Result<()> {
133133
format!("Failed to create log file: {}", log_path)
134134
})?;
135135
if args.quiet {
136-
let mut log = mmlogger::DualStreamLogger::new(
136+
let (log, handle) = mmlogger::channel_logger(
137137
std::io::stdout(),
138138
LogFormat::Plain,
139139
LevelFilter::WARN,
@@ -142,9 +142,12 @@ fn run() -> Result<()> {
142142
LevelFilter::ALL,
143143
);
144144

145-
run_camera_solve(&args, &mut log)
145+
let result = run_camera_solve(&args, &log);
146+
drop(log);
147+
handle.shutdown();
148+
result
146149
} else {
147-
let mut log = mmlogger::DualStreamLogger::new(
150+
let (log, handle) = mmlogger::channel_logger(
148151
std::io::stdout(),
149152
LogFormat::Plain,
150153
LevelFilter::ALL,
@@ -153,14 +156,17 @@ fn run() -> Result<()> {
153156
LevelFilter::ALL,
154157
);
155158

156-
run_camera_solve(&args, &mut log)
159+
let result = run_camera_solve(&args, &log);
160+
drop(log);
161+
handle.shutdown();
162+
result
157163
}
158164
}
159165

160166
#[cfg(not(feature = "logging"))]
161167
{
162-
let mut log = mmlogger::NoOpLogger;
163-
run_camera_solve(&args, &mut log)
168+
let log = mmlogger::NoOpLogger;
169+
run_camera_solve(&args, &log)
164170
}
165171
}
166172

@@ -276,7 +282,10 @@ impl IntermediateResultWriter for FileIntermediateResultWriter {
276282
}
277283
}
278284

279-
fn run_camera_solve<L: Logger>(args: &CliArgs, logger: &mut L) -> Result<()> {
285+
fn run_camera_solve<L: Logger + Clone + Send + Sync>(
286+
args: &CliArgs,
287+
logger: &L,
288+
) -> Result<()> {
280289
let total_start = Instant::now();
281290

282291
// Load solver settings file if provided.

lib/rust/mmlogger/src/lib.rs

Lines changed: 105 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
//! completely eliminated by the compiler.
2626
2727
use std::io::Write;
28+
use std::sync::mpsc;
29+
use std::thread;
2830
use std::time::SystemTime;
2931

3032
// ====================================================================
@@ -188,23 +190,23 @@ pub trait Logger {
188190
// and I think the `log` method is simply too flexible and adds
189191
// the cost of a string rather than a well defined enum that we
190192
// can exhaustively match and ensure all cases are covered.
191-
fn log(&mut self, level: &str, msg: &str);
193+
fn log(&self, level: &str, msg: &str);
192194

193-
fn info(&mut self, msg: &str) {
195+
fn info(&self, msg: &str) {
194196
self.log("INFO", msg);
195197
}
196198

197199
// TODO: Add "progress" method between info and warn.
198200

199-
fn warn(&mut self, msg: &str) {
201+
fn warn(&self, msg: &str) {
200202
self.log("WARN", msg);
201203
}
202204

203-
fn error(&mut self, msg: &str) {
205+
fn error(&self, msg: &str) {
204206
self.log("ERROR", msg);
205207
}
206208

207-
fn debug(&mut self, msg: &str) {
209+
fn debug(&self, msg: &str) {
208210
self.log("DEBUG", msg);
209211
}
210212
}
@@ -250,10 +252,9 @@ impl<W1: Write, W2: Write> DualStreamLogger<W1, W2> {
250252
levels2,
251253
}
252254
}
253-
}
254255

255-
impl<W1: Write, W2: Write> Logger for DualStreamLogger<W1, W2> {
256-
fn log(&mut self, level: &str, msg: &str) {
256+
/// Write a log message directly (used by the channel writer thread).
257+
pub fn write_log(&mut self, level: &str, msg: &str) {
257258
if self.levels1.allows(level) {
258259
write_formatted(&mut self.writer1, &self.format1, level, msg);
259260
}
@@ -268,11 +269,105 @@ impl<W1: Write, W2: Write> Logger for DualStreamLogger<W1, W2> {
268269
// ====================================================================
269270

270271
/// No-op logger with zero runtime cost.
272+
#[derive(Clone)]
271273
pub struct NoOpLogger;
272274

273275
impl Logger for NoOpLogger {
274276
#[inline(always)]
275-
fn log(&mut self, _level: &str, _msg: &str) {}
277+
fn log(&self, _level: &str, _msg: &str) {}
278+
}
279+
280+
// ====================================================================
281+
// ChannelLogger
282+
// ====================================================================
283+
284+
/// A log message sent through the channel.
285+
struct LogMessage {
286+
level: String,
287+
msg: String,
288+
}
289+
290+
/// Thread-safe logger that sends messages through an MPSC channel.
291+
///
292+
/// Cloning a `ChannelLogger` clones the sender, allowing multiple
293+
/// threads to log concurrently without blocking.
294+
#[derive(Clone)]
295+
pub struct ChannelLogger {
296+
sender: mpsc::Sender<LogMessage>,
297+
}
298+
299+
impl Logger for ChannelLogger {
300+
fn log(&self, level: &str, msg: &str) {
301+
let _ = self.sender.send(LogMessage {
302+
level: level.to_owned(),
303+
msg: msg.to_owned(),
304+
});
305+
}
306+
}
307+
308+
/// Handle for the log writer thread.
309+
///
310+
/// Call [`LogHandle::shutdown`] to flush remaining messages and join
311+
/// the writer thread. If dropped without calling `shutdown`, the
312+
/// writer thread will exit once all senders are dropped.
313+
pub struct LogHandle {
314+
join_handle: Option<thread::JoinHandle<()>>,
315+
}
316+
317+
impl LogHandle {
318+
/// Wait for the writer thread to finish.
319+
///
320+
/// All `ChannelLogger` clones must be dropped before calling this,
321+
/// otherwise it will block forever (the writer thread exits only
322+
/// when the channel closes, which requires all senders to be dropped).
323+
pub fn shutdown(mut self) {
324+
if let Some(handle) = self.join_handle.take() {
325+
let _ = handle.join();
326+
}
327+
}
328+
}
329+
330+
impl Drop for LogHandle {
331+
fn drop(&mut self) {
332+
if let Some(handle) = self.join_handle.take() {
333+
let _ = handle.join();
334+
}
335+
}
336+
}
337+
338+
/// Create a channel-based logger with a dedicated writer thread.
339+
///
340+
/// Returns a `ChannelLogger` (cloneable, `Send + Sync`) and a
341+
/// `LogHandle` that owns the writer thread.
342+
pub fn channel_logger<W1, W2>(
343+
writer1: W1,
344+
format1: LogFormat,
345+
levels1: LevelFilter,
346+
writer2: W2,
347+
format2: LogFormat,
348+
levels2: LevelFilter,
349+
) -> (ChannelLogger, LogHandle)
350+
where
351+
W1: Write + Send + 'static,
352+
W2: Write + Send + 'static,
353+
{
354+
let (sender, receiver) = mpsc::channel::<LogMessage>();
355+
356+
let join_handle = thread::spawn(move || {
357+
let mut backend = DualStreamLogger::new(
358+
writer1, format1, levels1, writer2, format2, levels2,
359+
);
360+
while let Ok(msg) = receiver.recv() {
361+
backend.write_log(&msg.level, &msg.msg);
362+
}
363+
});
364+
365+
let logger = ChannelLogger { sender };
366+
let handle = LogHandle {
367+
join_handle: Some(join_handle),
368+
};
369+
370+
(logger, handle)
276371
}
277372

278373
// ====================================================================
@@ -294,7 +389,7 @@ macro_rules! mm_debug_eprintln {
294389

295390
/// Debug log macro that routes through a logger instead of eprintln.
296391
///
297-
/// Requires a `const DEBUG: bool` in the calling scope and a mutable
392+
/// Requires a `const DEBUG: bool` in the calling scope and a
298393
/// logger as the first argument. When `DEBUG` is `false`, the compiler
299394
/// optimizes the entire call away.
300395
#[macro_export]

lib/rust/mmoptimise/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ anyhow = { workspace = true }
1515
log = { workspace = true }
1616
mmcholmod = { package = "mmcholmod_rust", path = "../mmcholmod" }
1717
mmcore = { package = "mmcore_rust", path = "../mmcore" }
18+
mmlogger = { package = "mmlogger", path = "../mmlogger" }
1819
nalgebra = { workspace = true }
1920
nalgebra-sparse = { workspace = true }
2021
num-traits = { workspace = true }

lib/rust/mmoptimise/benches/bench_differential_evolution.rs

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,11 @@ fn bench_de_rosenbrock(c: &mut Criterion) {
9191
let mut solver =
9292
DifferentialEvolution::new(cfg.clone()).unwrap();
9393
let mut best = vec![0.0; cfg.num_dimensions];
94-
black_box(solver.run(&evaluator, &mut best).unwrap())
94+
black_box(
95+
solver
96+
.run(&evaluator, &mut best, &mmlogger::NoOpLogger)
97+
.unwrap(),
98+
)
9599
});
96100
});
97101
}
@@ -126,7 +130,11 @@ fn bench_de_goldstein_price(c: &mut Criterion) {
126130
let mut solver =
127131
DifferentialEvolution::new(cfg.clone()).unwrap();
128132
let mut best = vec![0.0; cfg.num_dimensions];
129-
black_box(solver.run(&evaluator, &mut best).unwrap())
133+
black_box(
134+
solver
135+
.run(&evaluator, &mut best, &mmlogger::NoOpLogger)
136+
.unwrap(),
137+
)
130138
});
131139
});
132140
}
@@ -161,7 +169,11 @@ fn bench_de_powell(c: &mut Criterion) {
161169
let mut solver =
162170
DifferentialEvolution::new(cfg.clone()).unwrap();
163171
let mut best = vec![0.0; cfg.num_dimensions];
164-
black_box(solver.run(&evaluator, &mut best).unwrap())
172+
black_box(
173+
solver
174+
.run(&evaluator, &mut best, &mmlogger::NoOpLogger)
175+
.unwrap(),
176+
)
165177
});
166178
});
167179
}
@@ -196,7 +208,11 @@ fn bench_de_bukin_n6(c: &mut Criterion) {
196208
let mut solver =
197209
DifferentialEvolution::new(cfg.clone()).unwrap();
198210
let mut best = vec![0.0; cfg.num_dimensions];
199-
black_box(solver.run(&evaluator, &mut best).unwrap())
211+
black_box(
212+
solver
213+
.run(&evaluator, &mut best, &mmlogger::NoOpLogger)
214+
.unwrap(),
215+
)
200216
});
201217
});
202218
}
@@ -236,7 +252,11 @@ fn bench_de_strategy_comparison(c: &mut Criterion) {
236252
let mut solver =
237253
DifferentialEvolution::new(cfg.clone()).unwrap();
238254
let mut best = vec![0.0; cfg.num_dimensions];
239-
black_box(solver.run(&evaluator, &mut best).unwrap())
255+
black_box(
256+
solver
257+
.run(&evaluator, &mut best, &mmlogger::NoOpLogger)
258+
.unwrap(),
259+
)
240260
});
241261
});
242262
}

0 commit comments

Comments
 (0)