include/pops/runtime/program/profiler.hpp Source FileΒΆ

adc_cpp: include/pops/runtime/program/profiler.hpp Source File
adc_cpp 0.3.0
Model-free C++23 core for coupled hyperbolic-elliptic systems on adaptive (AMR) meshes, with MPI and GPU (Kokkos) backends
profiler.hpp
Go to the documentation of this file.
1#pragma once
2
3// A lightweight per-node / per-brick profiler for the compiled Program step (Spec 3 section 29-30,
4// ADC-459). It accumulates named wall-clock scopes -- one per Program node, native brick, or step
5// phase -- plus a few integer counters (kernels, cache hits/misses, scheduled nodes due/skipped),
6// and renders the report `sim.profile_report()` returns.
7//
8// Cost when disabled: record()/count() are a single predictable branch (zero work). A ProfileScope
9// is cheap but NOT free even when off -- it still constructs its name string and reads the clock
10// twice -- so wrap a per-node / per-brick scope (the intended granularity), not the tightest inner
11// loops. Single-threaded: a Profiler / ProfileScope is not thread-safe and must not be shared inside
12// a parallel region. Host/serial today; on a device backend a Kokkos::fence() must precede the scope
13// close so the timing reflects the kernel (the fence lives at the call site, not here -- this header
14// has no Kokkos dependency). MPI: each rank profiles itself; the report is per-rank (any reduction
15// belongs to the System integration).
16
17#include <algorithm>
18#include <chrono>
19#include <cstdint>
20#include <map>
21#include <sstream>
22#include <string>
23#include <vector>
24
25namespace pops::runtime::program {
26
27class Profiler {
28 public:
29 struct Entry {
30 std::uint64_t count = 0;
31 double total_s = 0.0;
32 double min_s = 0.0;
33 double max_s = 0.0;
34 double mean_s() const { return count != 0 ? total_s / static_cast<double>(count) : 0.0; }
35 };
36
37 void enable() { enabled_ = true; }
38 void disable() { enabled_ = false; }
39 bool enabled() const { return enabled_; }
40
41 // Drop all accumulated timings and counters (kept across enable/disable; cleared explicitly).
42 void reset() {
43 order_.clear();
44 entries_.clear();
45 counters_.clear();
46 counter_order_.clear();
47 }
48
49 // Record one timed sample of `name`, in seconds. No-op when disabled.
50 void record(const std::string& name, double seconds) {
51 if (!enabled_) {
52 return;
53 }
54 auto it = entries_.find(name);
55 if (it == entries_.end()) {
56 order_.push_back(name);
57 entries_.emplace(name,
58 Entry{.count = 1, .total_s = seconds, .min_s = seconds, .max_s = seconds});
59 } else {
60 Entry& e = it->second;
61 e.count += 1;
62 e.total_s += seconds;
63 e.min_s = std::min(e.min_s, seconds);
64 e.max_s = std::max(e.max_s, seconds);
65 }
66 }
67
68 // Bump a named integer counter (e.g. "kernels", "cache_hits", "nodes_skipped"). No-op when off.
69 void count(const std::string& name, std::int64_t by = 1) {
70 if (!enabled_) {
71 return;
72 }
73 auto it = counters_.find(name);
74 if (it == counters_.end()) {
75 counter_order_.push_back(name);
76 counters_.emplace(name, by);
77 } else {
78 it->second += by;
79 }
80 }
81
82 // Track a named PEAK counter: set it to max(current, value) instead of accumulating. Used for the
83 // scratch peak memory (the largest single scratch allocation seen, in bytes), where the running sum
84 // is meaningless. First-seen creates the counter at @p value. No-op when disabled.
85 void count_max(const std::string& name, std::int64_t value) {
86 if (!enabled_) {
87 return;
88 }
89 auto it = counters_.find(name);
90 if (it == counters_.end()) {
91 counter_order_.push_back(name);
92 counters_.emplace(name, value);
93 } else {
94 it->second = std::max(it->second, value);
95 }
96 }
97
98 const Entry* entry(const std::string& name) const {
99 auto it = entries_.find(name);
100 return it == entries_.end() ? nullptr : &it->second;
101 }
102
103 std::int64_t counter(const std::string& name) const {
104 auto it = counters_.find(name);
105 return it == counters_.end() ? 0 : it->second;
106 }
107
108 // Sum of every scope's total time (the "total" line of the report).
109 double total_s() const {
110 double t = 0.0;
111 for (const auto& name : order_) {
112 t += entries_.at(name).total_s;
113 }
114 return t;
115 }
116
117 std::size_t scope_count() const { return order_.size(); }
118
119 // A human-readable report in first-seen order: one line per scope (count / total / mean / min /
120 // max), then the counters. The exact text the Python `sim.profile_report()` returns.
121 std::string report() const {
122 std::ostringstream os;
123 os.setf(std::ios::fixed);
124 os.precision(6);
125 os << "Profiler report (total " << total_s() << " s, " << order_.size() << " scopes)\n";
126 for (const auto& name : order_) {
127 const Entry& e = entries_.at(name);
128 os << " " << name << " count=" << e.count << " total=" << e.total_s
129 << "s mean=" << e.mean_s() << "s min=" << e.min_s << "s max=" << e.max_s << "s\n";
130 }
131 if (!counter_order_.empty()) {
132 os << "counters:";
133 for (const auto& name : counter_order_) {
134 os << " " << name << "=" << counters_.at(name);
135 }
136 os << "\n";
137 }
138 return os.str();
139 }
140
141 private:
142 bool enabled_ = false;
143 std::vector<std::string> order_; // scope names, first-seen order (stable report)
144 std::map<std::string, Entry> entries_;
145 std::vector<std::string> counter_order_; // counter names, first-seen order
146 std::map<std::string, std::int64_t> counters_;
147};
148
149// RAII scope: times its own lifetime into `prof` under `name`. One per Program node / brick call.
150// Construct it at the top of the work; its destructor records the elapsed wall-clock seconds.
152 public:
153 ProfileScope(Profiler& prof, std::string name)
154 : prof_(prof), name_(std::move(name)), t0_(std::chrono::steady_clock::now()) {}
155
157 const auto t1 = std::chrono::steady_clock::now();
158 // A timing alloc failure must never abort the program nor escape this destructor: swallow any
159 // exception from record() (the worst case is one lost sample).
160 try {
161 prof_.record(name_, std::chrono::duration<double>(t1 - t0_).count());
162 } catch (...) { // NOLINT(bugprone-empty-catch) -- a profiler never throws out of a scope
163 }
164 }
165
166 ProfileScope(const ProfileScope&) = delete;
170
171 private:
172 Profiler& prof_;
173 std::string name_;
174 std::chrono::steady_clock::time_point t0_;
175};
176
177} // namespace pops::runtime::program
Definition profiler.hpp:151
ProfileScope(ProfileScope &&)=delete
ProfileScope(Profiler &prof, std::string name)
Definition profiler.hpp:153
~ProfileScope()
Definition profiler.hpp:156
ProfileScope & operator=(const ProfileScope &)=delete
ProfileScope & operator=(ProfileScope &&)=delete
ProfileScope(const ProfileScope &)=delete
Definition profiler.hpp:27
std::int64_t counter(const std::string &name) const
Definition profiler.hpp:103
void reset()
Definition profiler.hpp:42
void enable()
Definition profiler.hpp:37
const Entry * entry(const std::string &name) const
Definition profiler.hpp:98
void record(const std::string &name, double seconds)
Definition profiler.hpp:50
std::size_t scope_count() const
Definition profiler.hpp:117
void disable()
Definition profiler.hpp:38
void count(const std::string &name, std::int64_t by=1)
Definition profiler.hpp:69
double total_s() const
Definition profiler.hpp:109
std::string report() const
Definition profiler.hpp:121
void count_max(const std::string &name, std::int64_t value)
Definition profiler.hpp:85
bool enabled() const
Definition profiler.hpp:39
Definition cache_manager.hpp:37
Definition profiler.hpp:29
double total_s
Definition profiler.hpp:31
std::uint64_t count
Definition profiler.hpp:30
double max_s
Definition profiler.hpp:33
double min_s
Definition profiler.hpp:32
double mean_s() const
Definition profiler.hpp:34