Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
#include <algorithm>
#include <concepts>
#include <map>
#include <string_view>
#include <sstream>
#include <string>
#include <type_traits>

namespace power_grid_model {
Expand Down Expand Up @@ -92,6 +93,15 @@ class CalculationInfo : public Logger {
Report report() const { return data_; }
void clear() { data_.clear(); }

std::string string_report() const {
std::ostringstream result;
for (auto const& [tag, value] : data_) {
// Each line has format: EVENT_CODE\tVALUE
result << std::to_underlying(tag) << '\t' << value << '\n';
}
return std::move(result).str();
}

template <std::derived_from<Logger> T> T& merge_into(T& destination) const {
if (&destination == this) {
return destination; // nothing to do
Expand All @@ -109,7 +119,11 @@ class MultiThreadedCalculationInfo : public MultiThreadedLoggerImpl<CalculationI
using Report = CalculationInfo::Report;

Report report() const { return get().report(); }
void clear() { get().clear(); }
std::string string_report() const { return get().string_report(); }

protected:
std::string snapshot_thread_unsafe_impl() const override { return get().string_report(); }
void clear_thread_unsafe_impl() override { get().clear(); }
};
} // namespace common::logging

Expand Down
Comment thread
mgovers marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
// SPDX-FileCopyrightText: Contributors to the Power Grid Model project <powergridmodel@lfenergy.org>
//
// SPDX-License-Identifier: MPL-2.0

#pragma once

#include "logging.hpp"

#include <algorithm>
#include <memory>
#include <ranges>
#include <string_view>
#include <vector>

namespace power_grid_model::common::logging {

// Owns a list of child loggers (created by MultiThreadedCompositeLogger::create_child) and fans all log calls out to
// each of them. The children are owned by this logger; their lifetimes are tied to this object.
class CompositeChildLogger : public Logger {
public:
explicit CompositeChildLogger(std::vector<std::unique_ptr<Logger>> children) : children_{std::move(children)} {}

using Logger::log;

void log(LogEvent tag) override { log_all(tag); }
void log(LogEvent tag, std::string_view message) override { log_all(tag, message); }
void log(LogEvent tag, double value) override { log_all(tag, value); }
void log(LogEvent tag, Idx value) override { log_all(tag, value); }

private:
std::vector<std::unique_ptr<Logger>> children_;

template <typename... Args> void log_all(Args const&... args) {
for (auto const& child : children_) {
child->log(args...);
}
}
};

// Owning fan-out MultiThreadedLogger. Holds shared ownership of MultiThreadedLogger instances and forwards
// all log calls to each. create_child() creates a CompositeChildLogger that owns one child per registered logger.
//
// Lifetime contract: each registered logger is kept alive by this composite for as long as it remains
// registered (shared ownership), regardless of whether any other owner (e.g. a C API wrapper) has released
// its own reference. This is what makes destroying the wrapper while still registered safe.
// Dedupe: registering the same logger twice is a no-op (idempotent, consistent with logging conventions).
// UB: modifying the logger list while a calculation is in progress.
Comment thread
nitbharambe marked this conversation as resolved.
class MultiThreadedCompositeLogger : public MultiThreadedLogger {
Comment thread
nitbharambe marked this conversation as resolved.
public:
MultiThreadedCompositeLogger() = default;
explicit MultiThreadedCompositeLogger(std::vector<std::shared_ptr<MultiThreadedLogger>> loggers)
: loggers_{std::move(loggers)} {}

// Add/remove a logger. The object address is unchanged so any existing reference_wrapper
// pointing to this composite remains valid. Do not call while a calculation is in progress.
void add(std::shared_ptr<MultiThreadedLogger> logger) {
if (logger == nullptr) {
return; // defensively ignore null registrations
}
if (std::ranges::any_of(loggers_, [&](auto const& existing) { return existing.get() == logger.get(); })) {
return; // already registered — dedupe silently, consistent with logging API conventions
}
loggers_.push_back(std::move(logger));
}
void remove(MultiThreadedLogger const* logger) {
Comment thread
mgovers marked this conversation as resolved.
if (logger == nullptr) {
return; // defensively ignore null removals but it should be unreachable.
}
if (auto it = std::ranges::find_if(loggers_, [&](auto const& existing) { return existing.get() == logger; });
it != loggers_.end()) {
loggers_.erase(it);
}
}
void reset() { loggers_.clear(); }

std::unique_ptr<Logger> create_child() override {
Comment thread
mgovers marked this conversation as resolved.
Comment thread
mgovers marked this conversation as resolved.
std::vector<std::unique_ptr<Logger>> child_loggers;
child_loggers.reserve(loggers_.size());
for (auto const& logger : loggers_) {
child_loggers.push_back(logger->create_child());
}
return std::make_unique<CompositeChildLogger>(std::move(child_loggers));
}

void log(LogEvent tag) override { log_all(tag); }
void log(LogEvent tag, std::string_view message) override { log_all(tag, message); }
void log(LogEvent tag, double value) override { log_all(tag, value); }
void log(LogEvent tag, Idx value) override { log_all(tag, value); }

using MultiThreadedLogger::log;

// Fan out clear() to every registered logger.
void clear() override {
Comment thread
nitbharambe marked this conversation as resolved.
for (auto const& logger : loggers_) {
logger->clear();
}
}

[[nodiscard]] bool empty() const { return loggers_.empty(); }

private:
std::vector<std::shared_ptr<MultiThreadedLogger>> loggers_; // owning

template <typename... Args> void log_all(Args const&... args) {
for (auto const& logger : loggers_) {
logger->log(args...);
}
}
};

} // namespace power_grid_model::common::logging
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#include "common.hpp"

#include <cstdint>
#include <functional>
#include <memory>
#include <string_view>

Expand Down Expand Up @@ -71,6 +72,14 @@ class Logger {

struct MultiThreadedLogger : public Logger {
virtual std::unique_ptr<Logger> create_child() = 0;

// The function is called exactly once with a string_view valid only for the duration of the call.
// Default: no op / delivers an empty view
virtual void get_output(std::function<void(std::string_view)> const& callback) const { callback({}); }
Comment thread
nitbharambe marked this conversation as resolved.

virtual void clear() {
// Clear accumulated output. Default: no-op.
}
};

} // namespace common::logging
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,11 +75,39 @@ class MultiThreadedLoggerImpl : public MultiThreadedLogger {

using MultiThreadedLogger::log;

// Lock-safe overrides. Marked final so subclasses cannot bypass the lock; override
// snapshot_thread_unsafe_impl / clear_thread_unsafe_impl instead to add type-specific behaviour.
void get_output(std::function<void(std::string_view)> const& fn) const final {
// Snapshot under the lock, then call fn without the lock so user callbacks
// cannot re-enter logger APIs and deadlock on the non-recursive mutex.
std::string const snapshot = [&] {
std::lock_guard const lock{mutex_};
return snapshot_thread_unsafe_impl();
}();
fn(snapshot);
Comment thread
nitbharambe marked this conversation as resolved.
}
void clear() final {
Comment thread
nitbharambe marked this conversation as resolved.
std::lock_guard const lock{mutex_};
clear_thread_unsafe_impl();
}

protected:
// Snapshot implementation. Thread-safety must be handled by the caller
virtual std::string snapshot_thread_unsafe_impl() const {
return {
// The default logger has no state to snapshot; stateful loggers override this hook.
};
}
virtual void clear_thread_unsafe_impl() {
// The default logger has no state to clear; stateful loggers override this hook.
}

private:
friend class ThreadLogger;

LoggerType log_;
std::mutex mutex_;
// Mutable to enable locking in const methods like snapshot_thread_unsafe_impl and get_output.
mutable std::mutex mutex_;
Comment thread
nitbharambe marked this conversation as resolved.
Comment thread
mgovers marked this conversation as resolved.

void sync(ThreadLogger const& logger) {
assert(&logger != &log_);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ class TextLogger : public Logger {
data_.clear(); // reset error flags
}
std::string report() const { return data_.str(); }
std::string_view report_view() const { return data_.view(); }
void flush() {
if (flush_handler_) {
// exception swallowing: if the handler throws, we leave the logger in valid state and the caller handles it
Expand Down Expand Up @@ -113,8 +114,12 @@ class MultiThreadedTextLogger : public MultiThreadedLoggerImpl<TextLogger> {
using MultiThreadedLoggerImpl<TextLogger>::MultiThreadedLoggerImpl;

std::string report() const { return get().report(); }
void clear() { get().clear(); }
std::string_view report_view() const { return get().report_view(); }
void flush() { get().flush(); }

protected:
std::string snapshot_thread_unsafe_impl() const override { return get().report(); }
void clear_thread_unsafe_impl() override { get().clear(); }
};
} // namespace common::logging

Expand Down
1 change: 1 addition & 0 deletions tests/cpp_unit_tests/logging/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ add_executable(
power_grid_model_unit_tests_logging
"../test_entry_point.cpp"
"test_calculation_info.cpp"
"test_composite_logging.cpp"
"test_timer.cpp"
"test_text_logger.cpp"
)
Expand Down
33 changes: 33 additions & 0 deletions tests/cpp_unit_tests/logging/test_calculation_info.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

#include <doctest/doctest.h>

#include <string>
#include <thread>
#include <utility>
#include <vector>
Expand Down Expand Up @@ -189,6 +190,38 @@ TEST_CASE("Test MultiThreadedCalculationInfo") {
CHECK(clean_report.empty());
}

SUBCASE("Get output snapshot") {
logger_helper(multi_threaded_info);
auto const expected_output = multi_threaded_info.string_report();
std::string output;

// Re-enter from the callback to verify get_output releases its mutex before
// invoking user code and that the callback receives a pre-clear snapshot.
multi_threaded_info.get_output([&output, &multi_threaded_info](std::string_view snapshot) {
output = snapshot;
multi_threaded_info.clear();
});

CHECK(output == expected_output);
CHECK(multi_threaded_info.report().empty());
}

SUBCASE("Get output snapshot - multi threaded") {
run_parallel_jobs(arbitrary_n_threads, single_thread_job);
auto const expected_output = multi_threaded_info.string_report();
std::string output;

// Re-enter from the callback to verify get_output releases its mutex before
// invoking user code and that the callback receives a pre-clear snapshot.
multi_threaded_info.get_output([&output, &multi_threaded_info](std::string_view snapshot) {
output = snapshot;
multi_threaded_info.clear();
});

CHECK(output == expected_output);
CHECK(multi_threaded_info.report().empty());
}

SUBCASE("Getters of underlying CalculationInfo") {
auto const n_threads = static_cast<Idx>(std::jthread::hardware_concurrency());
run_parallel_jobs(n_threads, single_thread_job);
Expand Down
Loading
Loading