Skip to content

Conversation

mgovers
Copy link
Member

@mgovers mgovers commented Aug 20, 2025

  • Use a logger interface as an abstraction for calculation info
  • The entire math solver translation unit does not know calculation info anymore

NOTE: this is an experiment. While it can be merged as-is, the thought process behind it is more important than the actual code changes

Signed-off-by: Martijn Govers <Martijn.Govers@Alliander.com>
Signed-off-by: Martijn Govers <Martijn.Govers@Alliander.com>
Signed-off-by: Martijn Govers <Martijn.Govers@Alliander.com>
…feature/logger-interface

Signed-off-by: Martijn Govers <Martijn.Govers@Alliander.com>
Signed-off-by: Martijn Govers <Martijn.Govers@Alliander.com>
Signed-off-by: Martijn Govers <Martijn.Govers@Alliander.com>
…feature/logger-interface

Signed-off-by: Martijn Govers <Martijn.Govers@Alliander.com>
@mgovers mgovers added the do-not-merge This should not be merged label Aug 20, 2025
@mgovers mgovers marked this pull request as draft August 20, 2025 09:34
Signed-off-by: Martijn Govers <Martijn.Govers@Alliander.com>
Signed-off-by: Martijn Govers <Martijn.Govers@Alliander.com>
Signed-off-by: Martijn Govers <Martijn.Govers@Alliander.com>
Signed-off-by: Martijn Govers <Martijn.Govers@Alliander.com>
Signed-off-by: Martijn Govers <Martijn.Govers@Alliander.com>
Signed-off-by: Martijn Govers <Martijn.Govers@Alliander.com>
Signed-off-by: Martijn Govers <Martijn.Govers@Alliander.com>
Signed-off-by: Martijn Govers <Martijn.Govers@Alliander.com>
Signed-off-by: Martijn Govers <Martijn.Govers@Alliander.com>
Comment on lines +20 to +21
void log(LogEvent tag, double value) override { log_impl(tag, value); }
void log(LogEvent tag, Idx value) override { log_impl(tag, static_cast<double>(value)); }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we add a super generic log(LogEvent tag) without value to just log the event taking place?

Suggested change
void log(LogEvent tag, double value) override { log_impl(tag, value); }
void log(LogEvent tag, Idx value) override { log_impl(tag, static_cast<double>(value)); }
void log(LogEvent tag, double value) override { log_impl(tag, value); }
void log(LogEvent tag, Idx value) override { log_impl(tag, static_cast<double>(value)); }
void log(LogEvent tag) override { log_impl(tag); }

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

apologies. i did add this but apparently didn't commit. reopening

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OHHH i see, i did add it to the logger interface but not here.

What should the report look like? integer counter (similar to how maximum_num_iterations is a double but represents a float)?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thinking about it, it is tricky because it depends on the event whether it should time as 0, or keep track of a counter... maybe it's good to add it when the first event comes into play that should just be an event counter

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I imagine it makes sense that we would sometimes simply want to know that things have happened, without needing to know how long the duration of each thing. For timers it makes perfect sense to associate a value to tell the duration.

Copy link
Member Author

@mgovers mgovers Aug 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i do think it would be useful. i just don't know how to report.

let's say: we have the following code

CalculationInfo info;
try {
  Timer timer{info, LogEvent::some_event_timer};             // current way
  CodeHit{info, LogEvent::some_event_started};               // new event

  do_something();

  CodeHit{info, LogEvent::some_event_hit}; // other new event
  CodeHit{info, LogEvent::some_event_hit}; // other new event
} except (...) {
  CodeHit{info, LogEvent::some_event_threw_exception}; // another event
}

// what should the output look like here?
auto const report = info.report();
// example output:
// std::map<LogEvent, double>{
//   {some_event_timer: 0.2},
//   {some_event_started: 1.0}, // 1.0 because it is hit once
//   {some_event_hit: 2.0}      // 2.0 because it was hit twice
// }

// now let's consider accidentally using a timed log event
CodeHit{info, LogEvent::some_event_timer};

// what should the output look like here?
auto const report = info.report();
// example output
// std::map<LogEvent, double>{
//   {some_event_timer, 1.2},   // CodeHit accidentally adds 1 to timer
//   {some_event_started, 1.0},
//   {some_event_hit, 2.0}
// }

under the hood, what we could do is split up the report into separate maps, e.g.

struct Timing {
    double timing{};
};
struct MaximizedInteger {
    Idx count{};
};
struct HitCount {
    Idx count{};
};

// member variables; maybe combine into one but maybe has performance hit
std::map<LogEvent, Timing> timings;
std::map<LogEvent, MaximizedInteger> num_iter;
std::map<LogEvent, HitCount> hit_count;        // or maybe just std::vector<LogEvent>

using Metric = std::variant<Timing, MaximizedInteger, HitCount>;

std::map<LogEvent, Metric> report(); // combines the 3 maps and throws error if there
                                     // are events logged in 2 different maps

feels like overengineering in the short term, though...

Signed-off-by: Martijn Govers <Martijn.Govers@Alliander.com>
Signed-off-by: Martijn Govers <Martijn.Govers@Alliander.com>
Signed-off-by: Martijn Govers <Martijn.Govers@Alliander.com>
Signed-off-by: Martijn Govers <Martijn.Govers@Alliander.com>
Copy link

@@ -43,8 +43,26 @@ enum class LogEvent : int16_t {
max_num_iter = 2248, // TODO(mgovers): find other error code
};

struct Logger {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An interface is a good idea and definitely be incorporated in any design we agree on.

logger->log(tag, values...);
}
}
capturing::into_the_void(std::forward<T>(values)...);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

values can be any count of arguments and maybe not all get used in logging and hence unused ones get sent to void?

std::unique_ptr<Logger> clone() const override { return std::make_unique<NoLogger>(); }
};

class LogDispatcher final : public LogDispatch {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Although Dispatcher is currently not agreed on, I think this is a good experiment that we should refer back to in future when we include new logs.

~LogDispatcher() override = default;

private:
std::vector<Logger*> loggers_;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If multithreading related logic for log was to be moved to logger, it can be:
std::vector<std::vector<Logger*>> loggers_;
And there would be a separate aggregation function.

Comment on lines +13 to +15
inline Logger& merge_into(Logger& destination, CalculationInfo const& source) {
for (const auto& [tag, value] : source.report()) {
destination.log(tag, value);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this merge_into can call a different function merge_thread_into. And we can then have a maximum action for all rather than accumulate

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The base_log needs to be added and not maximized, but some similar logic can be a way to not have sum of threads as output of benchmark

Copy link
Member Author

@mgovers mgovers Aug 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i believe the merge_into is a nice example of something where it depends on the event type whether to add or maximize. e.g., the total amount of time spent within a thread is indeed something that could use the $L_{\infty}$ norm (maximizing) but there is a different event which is e.g. total computation resources used, which should use the $L_1$ norm (summation) across threads.

However, that should be a multi-threaded logger decision (e.g., it can take 1 event of a specific event type and convert it into 2 events, one that uses $L_1$ norm and one that uses $L_{\infty}$ norm.)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

implementation-wise, that could look like something along the lines of

class MultiThreadedCalculationInfo final : public Logger {
  private:
    using SubThreadLogger = CalculationInfo; // probably needs to be a wrapper class with mutex and/or thread id knowledge
    using MainThreadLogger = CalculationInfo;

  public:
    template <typename... Args>
    void log(Args&&... args) 
        requires requires(SubThreadLogger logger) {
            { logger.log(args) };
        }
    { 
        // assert it is the main thread and/or add a mutex
        main_log.log(std::forward<Args>(args));
    }

    void sync() {
        // assert it is the main thread and/or add a mutex
        for (auto& thread_log : thread_logs) {
            if (thread_log != nullptr) {
                sync_thread(*thread_log);
            }
        }
    }

    void report() const {
        return main_log.report();
    }

  private:
    MainThreadLogger main_log;
    std::vector<std::unique_ptr<SubThreadLogger>> thread_logs;

    void sync_thread(SubThreadLogger thread_log) {
        // guard with mutex
        for (auto& [tag, value] : thread_log.report) {
            switch(tag) {
                case total_time_in_thread:
                    log(maximum_time_in_thread, value);
                    log(tag, value);
                    break;
                default:
                    log(tag, value);
                    break;
            }
        }
    }
};

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
do-not-merge This should not be merged
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants