OpenSWMM Engine  6.0.0-alpha.4
Data-oriented, plugin-extensible SWMM Engine (6.0.0-alpha.4)
Loading...
Searching...
No Matches
ProcessPerf.hpp
Go to the documentation of this file.
1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 Caleb Buahin. All rights reserved.
3#pragma once
4
5#include "PerfTimers.hpp"
6#include <atomic>
7#include <optional>
8#include <string_view>
9#include <cstdint>
10#include <unordered_set>
11#if defined(__APPLE__)
12#include <dlfcn.h>
13#endif
14
15namespace openswmm::perf {
16
17// Self times across the process models. Keep these outside solver/checkpoint
18// state: trial copies and rollbacks must not copy or discard profiling evidence.
28struct ProcessPhaseName { const char* group; const char* key; };
29inline constexpr std::array<ProcessPhaseName, std::size_t(ProcessPhase::count)> process_names{{
30 {"2D", "faces"}, {"2D", "cells"}, {"2D", "rebuild"}, {"2D", "boundary"},
31 {"2D", "exchange"}, {"2D", "publish"}, {"2D", "species_faces"},
32 {"2D", "species_cells"}, {"2D", "reactions"},
33 {"GW2D", "faces"}, {"GW2D", "cells"}, {"GW2D", "node_exchange"},
34 {"GW2D", "refresh_dt"}, {"GW2D", "infiltration"}, {"GW2D", "species"},
35 {"GW2D", "reactions"}, {"GW2D", "sigma_map"}, {"GW2D", "heat_columns"},
36 {"2D", "quality"}, {"HYD", "runoff"}, {"HYD", "groundwater"}, {"HYD", "lid"},
37 {"HYD", "lid_nodes"}, {"HYD", "richards"}, {"HYD", "mesh_snow"},
38 {"HYD", "subcatch_snow"}, {"HYD", "common_source"}, {"HYD", "source_trial"},
39 {"HYD", "outfall"}, {"HYD", "source_results"}, {"HYD", "engine_source_results"}
40}};
41inline std::array<std::atomic<double>, std::size_t(ProcessPhase::count)> process_seconds{};
42inline std::array<std::atomic<unsigned long long>, std::size_t(ProcessPhase::count)> process_calls{};
43inline std::array<std::atomic<unsigned long long>, std::size_t(ProcessPhase::count)> process_allocations{};
44inline std::array<std::atomic<unsigned long long>, std::size_t(ProcessPhase::count)> process_allocated_bytes{};
45// Process totals are qualified only for isolated engine lifetimes. An overlap
46// invalidates the whole cohort, including the engine that remains open last.
47inline std::atomic<bool> process_attribution_valid{true};
48inline std::mutex process_models_mutex;
49inline std::unordered_set<const void*> process_models;
50inline bool process_tracking_failed = false; // protected by process_models_mutex
51
52// Optional malloc interposer supplied by the benchmark tooling on macOS. The
53// shipping engine never replaces malloc. Counts are inclusive, on the calling
54// thread, and count successful allocation/reallocation requests, not live blocks.
55using AllocationSnapshot = void (*)(std::uint64_t*, std::uint64_t*);
57#if defined(__APPLE__)
58 static const auto snapshot = reinterpret_cast<AllocationSnapshot>(
59 dlsym(RTLD_DEFAULT, "openswmm_perf_malloc_snapshot"));
60 return snapshot;
61#else
62 return nullptr;
63#endif
64}
65
71inline constexpr std::array<const char*, std::size_t(ProcessCounter::count)> process_counter_names{{
72 "sigma.evaluate", "sigma.firings", "sigma.hist.le1", "sigma.hist.2_8",
73 "sigma.hist.9_32", "sigma.hist.33_64", "sigma.hist.gt64", "sigma_map.refresh",
74 "copy.water_trial", "copy.source_stage", "copy.rk2_snapshot", "copy.mesh_snow"
75}};
76inline std::array<std::atomic<unsigned long long>, std::size_t(ProcessCounter::count)> process_counts{};
77
78inline void count(ProcessCounter counter, unsigned long long n = 1) noexcept {
79 if (enabled() && process_attribution_valid.load(std::memory_order_relaxed))
80 process_counts[std::size_t(counter)].fetch_add(n, std::memory_order_relaxed);
81}
82inline unsigned long long value(ProcessCounter counter) noexcept {
83 return process_counts[std::size_t(counter)].load(std::memory_order_relaxed);
84}
85
86// GatedTimer preserves nested_wall across module boundaries. Each scope books
87// into private storage before merging, including scopes inside OMP face loops.
88// With profiling disabled there are no clock reads, allocations or atomic writes.
90 double seconds_ = 0;
91 ProcessPhase phase_;
92 std::optional<GatedTimer> timer_;
93 AllocationSnapshot allocations_ = nullptr;
94 std::uint64_t allocations_begin_ = 0, bytes_begin_ = 0;
95public:
96 explicit ProcessTimer(ProcessPhase phase) noexcept : phase_(phase) {
97 if (enabled() && process_attribution_valid.load(std::memory_order_relaxed)) {
98 allocations_ = allocation_snapshot();
99 if (allocations_) allocations_(&allocations_begin_, &bytes_begin_);
100 timer_.emplace(seconds_);
101 }
102 }
103 void stop() noexcept {
104 if (!timer_)
105 return;
106 timer_.reset();
107 if (!process_attribution_valid.load(std::memory_order_relaxed))
108 return;
109 if (allocations_) {
110 std::uint64_t calls = 0, bytes = 0;
111 allocations_(&calls, &bytes);
112 process_allocations[std::size_t(phase_)].fetch_add(calls - allocations_begin_, std::memory_order_relaxed);
113 process_allocated_bytes[std::size_t(phase_)].fetch_add(bytes - bytes_begin_, std::memory_order_relaxed);
114 }
115 auto& total = process_seconds[std::size_t(phase_)];
116 double old = total.load(std::memory_order_relaxed);
117 while (!total.compare_exchange_weak(old, old + seconds_, std::memory_order_relaxed)) {}
118 process_calls[std::size_t(phase_)].fetch_add(1, std::memory_order_relaxed);
119 }
120 ~ProcessTimer() noexcept { stop(); }
121 ProcessTimer(const ProcessTimer&) = delete;
123};
124
125// A histogram entry describes one SIGMA cell firing, including the final capture
126// sweep. Failed firings are counted as attempted work as well.
140
141// Called between simulations, with no active scopes/worksharing regions.
142inline void clear_process_totals() noexcept {
143 for (auto& seconds : process_seconds) seconds.store(0, std::memory_order_relaxed);
144 for (auto& calls : process_calls) calls.store(0, std::memory_order_relaxed);
145 for (auto& calls : process_allocations) calls.store(0, std::memory_order_relaxed);
146 for (auto& bytes : process_allocated_bytes) bytes.store(0, std::memory_order_relaxed);
147 for (auto& count : process_counts) count.store(0, std::memory_order_relaxed);
148}
149// Standalone kernel benchmarks/tests call this only between complete models.
150inline void reset_process() noexcept {
151 std::lock_guard<std::mutex> lock(process_models_mutex);
152 if (!process_models.empty() || process_tracking_failed) {
153 process_attribution_valid.store(false, std::memory_order_relaxed);
154 return;
155 }
157 process_attribution_valid.store(true, std::memory_order_relaxed);
158}
159inline void begin_process_model(const void* model) noexcept {
160 if (!enabled()) return;
161 try {
162 std::lock_guard<std::mutex> lock(process_models_mutex);
163 if (!process_models.insert(model).second) return;
164 if (process_models.size() == 1 && !process_tracking_failed) {
166 process_attribution_valid.store(true, std::memory_order_relaxed);
167 } else {
168 process_attribution_valid.store(false, std::memory_order_relaxed);
169 }
170 } catch (...) {
171 // Optional instrumentation must not turn an allocation failure into
172 // failure of the numerical run. No later run may bless these totals.
173 std::lock_guard<std::mutex> lock(process_models_mutex);
175 process_attribution_valid.store(false, std::memory_order_relaxed);
176 std::fprintf(stderr, "[PERF-PROCESS-INVALID] reason=tracking_failed\n");
177 }
178}
179inline void dump_process() noexcept {
180 if (!process_attribution_valid.load(std::memory_order_relaxed)) {
181 std::fprintf(stderr, "[PERF-PROCESS-INVALID] reason=overlapping_engine_lifetimes\n");
182 return;
183 }
184 for (const char* group : {"2D", "GW2D", "HYD"}) {
185 bool any = false;
186 for (std::size_t i = 0; i < process_names.size(); ++i)
187 if (std::string_view(process_names[i].group) == group && process_calls[i].load()) any = true;
188 if (!any) continue;
189 std::fprintf(stderr, "[PERF-%s]", group);
190 for (std::size_t i = 0; i < process_names.size(); ++i) {
191 if (std::string_view(process_names[i].group) != group) continue;
192 std::fprintf(stderr, " %s=%.9f n.%s=%llu", process_names[i].key,
193 process_seconds[i].load(), process_names[i].key, process_calls[i].load());
194 }
195 std::fprintf(stderr, "\n");
196 }
197 bool any = false;
198 for (const auto& count : process_counts) if (count.load()) any = true;
199 if (any) {
200 std::fprintf(stderr, "[PERF-PROCESS-COUNTS]");
201 for (std::size_t i = 0; i < process_counts.size(); ++i)
202 std::fprintf(stderr, " %s=%llu", process_counter_names[i], process_counts[i].load());
203 std::fprintf(stderr, "\n");
204 }
205 if (allocation_snapshot()) {
206 std::fprintf(stderr, "[PERF-ALLOC] inclusive=1 calling_thread=1");
207 for (std::size_t i = 0; i < process_names.size(); ++i)
208 if (process_calls[i].load())
209 std::fprintf(stderr, " %s.%s.calls=%llu %s.%s.bytes=%llu",
210 process_names[i].group, process_names[i].key, process_allocations[i].load(),
211 process_names[i].group, process_names[i].key, process_allocated_bytes[i].load());
212 std::fprintf(stderr, "\n");
213 }
214}
215inline void end_process_model(const void* model, bool report = true) noexcept {
216 if (!enabled()) return;
217 std::lock_guard<std::mutex> lock(process_models_mutex);
218 const auto found = process_models.find(model);
219 if (found == process_models.end()) return;
220 if (report) dump_process();
221 process_models.erase(found);
222}
223
224} // namespace openswmm::perf
Performance timing and phase accumulators for engine execution.
void stop() noexcept
Definition ProcessPerf.hpp:103
ProcessTimer(ProcessPhase phase) noexcept
Definition ProcessPerf.hpp:96
~ProcessTimer() noexcept
Definition ProcessPerf.hpp:120
ProcessTimer & operator=(const ProcessTimer &)=delete
ProcessTimer(const ProcessTimer &)=delete
Definition PerfTimers.hpp:26
ProcessCounter
Definition ProcessPerf.hpp:66
@ water_trial_copies
Definition ProcessPerf.hpp:68
@ rk2_snapshots
Definition ProcessPerf.hpp:69
@ sigma_le_8
Definition ProcessPerf.hpp:67
@ sigma_gt_64
Definition ProcessPerf.hpp:68
@ mesh_snow_copies
Definition ProcessPerf.hpp:69
@ sigma_evaluations
Definition ProcessPerf.hpp:67
@ sigma_le_32
Definition ProcessPerf.hpp:67
@ sigma_firings
Definition ProcessPerf.hpp:67
@ source_stage_copies
Definition ProcessPerf.hpp:69
@ sigma_le_1
Definition ProcessPerf.hpp:67
@ count
Definition ProcessPerf.hpp:69
@ sigma_map_refreshes
Definition ProcessPerf.hpp:68
@ sigma_le_64
Definition ProcessPerf.hpp:68
long value(FvCounter counter) noexcept
Read one process-global completed FV counter total.
Definition PerfTimers.hpp:136
std::array< std::atomic< unsigned long long >, std::size_t(ProcessCounter::count)> process_counts
Definition ProcessPerf.hpp:76
std::array< std::atomic< double >, std::size_t(ProcessPhase::count)> process_seconds
Definition ProcessPerf.hpp:41
void reset_process() noexcept
Definition ProcessPerf.hpp:150
constexpr std::array< ProcessPhaseName, std::size_t(ProcessPhase::count)> process_names
Definition ProcessPerf.hpp:29
std::array< std::atomic< unsigned long long >, std::size_t(ProcessPhase::count)> process_allocations
Definition ProcessPerf.hpp:43
void end_process_model(const void *model, bool report=true) noexcept
Definition ProcessPerf.hpp:215
bool process_tracking_failed
Definition ProcessPerf.hpp:50
bool enabled() noexcept
Read the process-cached profiling environment gate.
Definition PerfTimers.hpp:238
void clear_process_totals() noexcept
Definition ProcessPerf.hpp:142
std::atomic< bool > process_attribution_valid
Definition ProcessPerf.hpp:47
std::array< std::atomic< unsigned long long >, std::size_t(ProcessPhase::count)> process_calls
Definition ProcessPerf.hpp:42
void begin_process_model(const void *model) noexcept
Definition ProcessPerf.hpp:159
constexpr std::array< const char *, std::size_t(ProcessCounter::count)> process_counter_names
Definition ProcessPerf.hpp:71
void(*)(std::uint64_t *, std::uint64_t *) AllocationSnapshot
Definition ProcessPerf.hpp:55
void dump_process() noexcept
Definition ProcessPerf.hpp:179
ProcessPhase
Definition ProcessPerf.hpp:19
@ surface_cells
Definition ProcessPerf.hpp:20
@ gw_reactions
Definition ProcessPerf.hpp:23
@ gw_infiltration
Definition ProcessPerf.hpp:22
@ surface_boundary
Definition ProcessPerf.hpp:20
@ lid_nodes
Definition ProcessPerf.hpp:24
@ gw_faces
Definition ProcessPerf.hpp:22
@ common_source
Definition ProcessPerf.hpp:25
@ gw_dt
Definition ProcessPerf.hpp:22
@ surface_faces
Definition ProcessPerf.hpp:20
@ gw_cells
Definition ProcessPerf.hpp:22
@ groundwater
Definition ProcessPerf.hpp:24
@ gw_sigma_map
Definition ProcessPerf.hpp:23
@ source_results
Definition ProcessPerf.hpp:25
@ gw_node_exchange
Definition ProcessPerf.hpp:22
@ gw_species
Definition ProcessPerf.hpp:23
@ surface_rebuild
Definition ProcessPerf.hpp:20
@ heat_columns
Definition ProcessPerf.hpp:23
@ surface_quality
Definition ProcessPerf.hpp:23
@ surface_species_cells
Definition ProcessPerf.hpp:21
@ lid
Definition ProcessPerf.hpp:24
@ mesh_snow
Definition ProcessPerf.hpp:24
@ richards
Definition ProcessPerf.hpp:24
@ surface_species
Definition ProcessPerf.hpp:21
@ surface_reactions
Definition ProcessPerf.hpp:21
@ engine_source_results
Definition ProcessPerf.hpp:25
@ source_trial
Definition ProcessPerf.hpp:25
@ count
Definition ProcessPerf.hpp:26
@ outfall
Definition ProcessPerf.hpp:25
@ surface_publish
Definition ProcessPerf.hpp:21
@ surface_exchange
Definition ProcessPerf.hpp:21
@ runoff
Definition ProcessPerf.hpp:24
@ subcatch_snow
Definition ProcessPerf.hpp:24
void count(FvCounter counter, long n=1) noexcept
Accumulate a batch of events into worker-private or serial global counters.
Definition PerfTimers.hpp:480
AllocationSnapshot allocation_snapshot() noexcept
Definition ProcessPerf.hpp:56
std::array< std::atomic< unsigned long long >, std::size_t(ProcessPhase::count)> process_allocated_bytes
Definition ProcessPerf.hpp:44
std::unordered_set< const void * > process_models
Definition ProcessPerf.hpp:49
std::mutex process_models_mutex
Definition ProcessPerf.hpp:48
Definition ProcessPerf.hpp:28
const char * key
Definition ProcessPerf.hpp:28
const char * group
Definition ProcessPerf.hpp:28
Definition ProcessPerf.hpp:127
unsigned long long evaluations
Definition ProcessPerf.hpp:128
void evaluate() noexcept
Definition ProcessPerf.hpp:129
~SigmaEvaluations() noexcept
Definition ProcessPerf.hpp:130