./
Coverage:
low: ≥ 0%
medium: ≥ 75.0%
high: ≥ 90.0%
Lines:
138 / 142
97.2%
Branches:
84 / 112
75.0%
Functions:
19 / 19
100.0%

src/diagnostics.cpp
Line Branch Exec Source
1 /**
2 * @file diagnostics.cpp
3 * @brief Counters for DMK's intentional leak / detach paths, the per-reason module-pin counts, the diagnostic event
4 * bus, the live hook population tally, and the one-call Snapshot aggregator. All of it is scoped to one linked
5 * DMK instance.
6 */
7
8 #include "DetourModKit/anchor.hpp"
9 #include "DetourModKit/diagnostics.hpp"
10
11 #include "internal/diagnostics_population.hpp"
12 #include "internal/drain_backoff.hpp"
13 #include "internal/lifecycle_context.hpp"
14
15 #include <windows.h>
16
17 #include <array>
18 #include <atomic>
19 #include <chrono>
20 #include <cstddef>
21 #include <cstdint>
22 #include <new>
23
24 namespace DetourModKit
25 {
26 namespace diagnostics
27 {
28 namespace
29 {
30 // Published once each dispatcher exists, so teardown never constructs one.
31 constinit std::atomic<EventDispatcher<ScannerFaultEvent> *> s_scanner_faults{nullptr};
32 constinit std::atomic<EventDispatcher<HookLifecycleEvent> *> s_hook_lifecycle{nullptr};
33
34 constexpr std::size_t LEAK_SUBSYSTEM_COUNT = static_cast<std::size_t>(LeakSubsystem::Count);
35 // Snapshot::intentional_leaks indexes by value, so an existing bucket never moves.
36 static_assert(static_cast<std::size_t>(LeakSubsystem::HookResidue) + 1 == LEAK_SUBSYSTEM_COUNT);
37
38 // One independent event tally per subsystem. Relaxed throughout: the counters carry no ordering obligation
39 // toward any other state.
40 std::array<std::atomic<std::size_t>, LEAK_SUBSYSTEM_COUNT> s_leak_counts{};
41
42 constexpr std::size_t MODULE_PIN_REASON_COUNT = static_cast<std::size_t>(ModulePinReason::Count);
43 static_assert(
44 MODULE_PIN_REASON_COUNT == DetourModKit::detail::module_pin_observability::MODULE_PIN_REASON_COUNT,
45 "ModulePinReason::Count and the internal counter array size must stay equal"
46 );
47 } // namespace
48
49 1969 void record_intentional_leak(LeakSubsystem subsystem) noexcept
50 {
51 1969 const auto index = static_cast<std::size_t>(subsystem);
52
2/2
✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 4 taken 1968 times.
1969 if (index >= LEAK_SUBSYSTEM_COUNT)
53 {
54 1 return;
55 }
56 1968 s_leak_counts[index].fetch_add(1, std::memory_order_relaxed);
57 }
58
59 84654 std::size_t intentional_leak_count(LeakSubsystem subsystem) noexcept
60 {
61 84654 const auto index = static_cast<std::size_t>(subsystem);
62
2/2
✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 4 taken 84653 times.
84654 if (index >= LEAK_SUBSYSTEM_COUNT)
63 {
64 1 return 0;
65 }
66 169306 return s_leak_counts[index].load(std::memory_order_relaxed);
67 }
68
69 9766 std::size_t total_intentional_leaks() noexcept
70 {
71 9766 std::size_t total = 0;
72
2/2
✓ Branch 11 → 3 taken 97660 times.
✓ Branch 11 → 12 taken 9766 times.
107426 for (const auto &counter : s_leak_counts)
73 {
74 195320 total += counter.load(std::memory_order_relaxed);
75 }
76 9766 return total;
77 }
78
79 53 void reset_intentional_leaks() noexcept
80 {
81
2/2
✓ Branch 12 → 3 taken 530 times.
✓ Branch 12 → 13 taken 53 times.
583 for (auto &counter : s_leak_counts)
82 {
83 530 counter.store(0, std::memory_order_relaxed);
84 }
85 53 }
86
87 77567 std::size_t module_pin_count(ModulePinReason reason) noexcept
88 {
89 77567 const auto index = static_cast<std::size_t>(reason);
90
2/2
✓ Branch 2 → 3 taken 2 times.
✓ Branch 2 → 4 taken 77565 times.
77567 if (index >= MODULE_PIN_REASON_COUNT)
91 {
92 2 return 0;
93 }
94 155130 return DetourModKit::detail::module_pin_observability::s_outstanding[index].load(std::memory_order_relaxed);
95 }
96
97 9962 std::size_t total_module_pins() noexcept
98 {
99 9962 std::size_t total = 0;
100
2/2
✓ Branch 11 → 3 taken 109582 times.
✓ Branch 11 → 12 taken 9962 times.
119544 for (const auto &counter : DetourModKit::detail::module_pin_observability::s_outstanding)
101 {
102 219164 total += counter.load(std::memory_order_relaxed);
103 }
104 9962 return total;
105 }
106
107 4879 LifecycleCounters lifecycle_counters() noexcept
108 {
109 4879 LifecycleCounters counters;
110 4879 counters.reaper_started =
111 4879 DetourModKit::detail::lifecycle_observability::s_reaper_started.load(std::memory_order_relaxed);
112 4879 counters.permanent_pins =
113 4879 DetourModKit::detail::lifecycle_observability::s_permanent_pins.load(std::memory_order_relaxed);
114 4879 counters.abandoned_owners =
115 4879 DetourModKit::detail::lifecycle_observability::s_abandoned_owners.load(std::memory_order_relaxed);
116 4879 return counters;
117 }
118
119 5121 EventDispatcher<ScannerFaultEvent> &scanner_faults()
120 {
121 // Never destroyed, for the same reason as hook_lifecycle(). A scan can be driven from a namespace-scope
122 // object's destructor or from a module-pinned thread that outlives this TU's static destructors, and the
123 // consumer's own Subscription can likewise be destroyed after them; both would then reach a destroyed
124 // mutex and subscriber list, which no try/catch can contain because it is undefined behaviour rather than
125 // an exception.
126 alignas(
127 EventDispatcher<ScannerFaultEvent>
128 ) static unsigned char storage[sizeof(EventDispatcher<ScannerFaultEvent>)];
129 143 static EventDispatcher<ScannerFaultEvent> *const dispatcher = []
130 {
131
2/6
✓ Branch 3 → 4 taken 143 times.
✗ Branch 3 → 9 not taken.
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 143 times.
✗ Branch 9 → 10 not taken.
✗ Branch 9 → 11 not taken.
143 auto *const created = ::new (static_cast<void *>(storage)) EventDispatcher<ScannerFaultEvent>();
132 143 s_scanner_faults.store(created, std::memory_order_release);
133 143 return created;
134
4/8
✓ Branch 2 → 3 taken 143 times.
✓ Branch 2 → 8 taken 4978 times.
✓ Branch 4 → 5 taken 143 times.
✗ Branch 4 → 8 not taken.
✓ Branch 5 → 6 taken 143 times.
✗ Branch 5 → 10 not taken.
✗ Branch 11 → 12 not taken.
✗ Branch 11 → 13 not taken.
5121 }();
135 5121 return *dispatcher;
136 }
137
138 56438 EventDispatcher<HookLifecycleEvent> &hook_lifecycle()
139 {
140 // Never destroyed because ~Hook and ~VmtHook may emit after this translation unit's static destructors.
141 alignas(
142 EventDispatcher<HookLifecycleEvent>
143 ) static unsigned char storage[sizeof(EventDispatcher<HookLifecycleEvent>)];
144 1999 static EventDispatcher<HookLifecycleEvent> *const dispatcher = []
145 {
146
2/6
✓ Branch 3 → 4 taken 1999 times.
✗ Branch 3 → 9 not taken.
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 1999 times.
✗ Branch 9 → 10 not taken.
✗ Branch 9 → 11 not taken.
1999 auto *const created = ::new (static_cast<void *>(storage)) EventDispatcher<HookLifecycleEvent>();
147 1999 s_hook_lifecycle.store(created, std::memory_order_release);
148 1999 return created;
149
4/8
✓ Branch 2 → 3 taken 1999 times.
✓ Branch 2 → 8 taken 54439 times.
✓ Branch 4 → 5 taken 1999 times.
✗ Branch 4 → 8 not taken.
✓ Branch 5 → 6 taken 1999 times.
✗ Branch 5 → 10 not taken.
✗ Branch 11 → 12 not taken.
✗ Branch 11 → 13 not taken.
56438 }();
150 56438 return *dispatcher;
151 }
152
153 Snapshot
154 162 collect(std::span<const rtti::DriftEntry> drift_report, std::span<const anchor::ResolvedAnchor> anchor_report)
155 {
156 162 Snapshot snapshot;
157
158 // Derive the total by summing the per-subsystem values captured into this snapshot (rather than a second
159 // independent total_intentional_leaks() read), so snapshot.total_intentional_leaks always equals the sum of
160 // the breakdown even if a counter is incremented concurrently between the copy and the total.
161
2/2
✓ Branch 9 → 3 taken 1620 times.
✓ Branch 9 → 10 taken 162 times.
1944 for (std::size_t i = 0; i < snapshot.intentional_leaks.size(); ++i)
162 {
163 1620 snapshot.intentional_leaks[i] = intentional_leak_count(static_cast<LeakSubsystem>(i));
164 1620 snapshot.total_intentional_leaks += snapshot.intentional_leaks[i];
165 }
166
167 162 DetourModKit::detail::hook_population::read(
168 snapshot.hooks_total,
169 snapshot.hooks_active,
170 snapshot.hooks_disabled
171 );
172
173 // Same derivation rule as the leak total: sum the captured breakdown, not a second independent read.
174
2/2
✓ Branch 18 → 12 taken 1782 times.
✓ Branch 18 → 19 taken 162 times.
2106 for (std::size_t i = 0; i < snapshot.module_pins.size(); ++i)
175 {
176 1782 snapshot.module_pins[i] = module_pin_count(static_cast<ModulePinReason>(i));
177 1782 snapshot.total_module_pins += snapshot.module_pins[i];
178 }
179
180 162 snapshot.lifecycle = lifecycle_counters();
181
182 162 snapshot.drift_total = drift_report.size();
183
2/2
✓ Branch 37 → 23 taken 3 times.
✓ Branch 37 → 38 taken 162 times.
327 for (const rtti::DriftEntry &entry : drift_report)
184 {
185
2/2
✓ Branch 25 → 26 taken 2 times.
✓ Branch 25 → 27 taken 1 time.
3 if (entry.ok)
186 {
187 2 ++snapshot.drift_healed;
188 }
189 else
190 {
191 1 ++snapshot.drift_failed;
192 }
193 }
194
195 162 snapshot.anchor_quality = anchor::assess_quality(anchor_report);
196
197 162 return snapshot;
198 }
199 } // namespace diagnostics
200
201 namespace detail
202 {
203 struct DiagnosticsEmitOwner
204 {
205 /// The event-independent form of the dispatcher's release outcome.
206 enum class Outcome : std::uint8_t
207 {
208 Returned,
209 LiveSubscription,
210 Busy,
211 Unwaitable
212 };
213
214 template <typename Event>
215 10110 [[nodiscard]] static Outcome release(EventDispatcher<Event> &dispatcher, bool may_prune) noexcept
216 {
217 using Release = typename EventDispatcher<Event>::EmitOwnerRelease;
218
7/10
DetourModKit::detail::DiagnosticsEmitOwner::Outcome DetourModKit::detail::DiagnosticsEmitOwner::release<DetourModKit::diagnostics::ScannerFaultEvent>(DetourModKit::EventDispatcher<DetourModKit::diagnostics::ScannerFaultEvent>&, bool):
✓ Branch 3 → 4 taken 158 times.
✓ Branch 3 → 5 taken 1 time.
✓ Branch 3 → 6 taken 1 time.
✗ Branch 3 → 7 not taken.
✗ Branch 3 → 8 not taken.
DetourModKit::detail::DiagnosticsEmitOwner::Outcome DetourModKit::detail::DiagnosticsEmitOwner::release<DetourModKit::diagnostics::HookLifecycleEvent>(DetourModKit::EventDispatcher<DetourModKit::diagnostics::HookLifecycleEvent>&, bool):
✓ Branch 3 → 4 taken 9731 times.
✓ Branch 3 → 5 taken 8 times.
✓ Branch 3 → 6 taken 209 times.
✓ Branch 3 → 7 taken 2 times.
✗ Branch 3 → 8 not taken.
10110 switch (dispatcher.release_idle_emit_owner(may_prune))
219 {
220 9889 case Release::Released:
221 case Release::NotOwner:
222 9889 return Outcome::Returned;
223 9 case Release::LiveSubscription:
224 9 return Outcome::LiveSubscription;
225 210 case Release::Busy:
226 210 return Outcome::Busy;
227 2 case Release::Unwaitable:
228 2 break;
229 }
230 2 return Outcome::Unwaitable;
231 }
232 };
233
234 namespace
235 {
236 using Outcome = DiagnosticsEmitOwner::Outcome;
237
238 /// Bounds the teardown wait for a diagnostics emit. Expiry keeps the index ([B-73]).
239 constexpr auto DIAGNOSTICS_DRAIN_TIMEOUT = std::chrono::seconds{1};
240
241 // One releaser at a time settles the records below.
242 constinit std::atomic_flag s_release_running{};
243 // Set while a kept ownership holds its one LeakSubsystem::Diagnostics record.
244 constinit std::atomic<bool> s_scanner_faults_recorded{false};
245 constinit std::atomic<bool> s_hook_lifecycle_recorded{false};
246
247 /// The teardown result of one diagnostics dispatcher.
248 struct KeptIndex
249 {
250 const char *dispatcher_name;
251 Outcome outcome;
252 std::atomic<bool> &recorded;
253 };
254
255 10 [[nodiscard]] const char *kept_cause(Outcome outcome) noexcept
256 {
257
3/4
✓ Branch 2 → 3 taken 6 times.
✓ Branch 2 → 4 taken 2 times.
✓ Branch 2 → 5 taken 2 times.
✗ Branch 2 → 6 not taken.
10 switch (outcome)
258 {
259 6 case Outcome::LiveSubscription:
260 6 return "a subscription is still live. Drop every subscription before Session teardown";
261 2 case Outcome::Busy:
262 2 return "an emit still held a handler list";
263 2 case Outcome::Returned:
264 case Outcome::Unwaitable:
265 2 break;
266 }
267 2 return "the teardown ran inside a diagnostics emit or while an untracked emit ran";
268 }
269
270 19486 void settle(const KeptIndex &kept, DiagnosticsTeardown teardown, bool may_block) noexcept
271 {
272
2/2
✓ Branch 2 → 3 taken 19472 times.
✓ Branch 2 → 5 taken 14 times.
19486 if (kept.outcome == Outcome::Returned)
273 {
274 19472 kept.recorded.store(false, std::memory_order_relaxed);
275 19472 return;
276 }
277 // Outside Session teardown, a live subscription is an ordinary owner, for example across a cache
278 // restart.
279
4/4
✓ Branch 5 → 6 taken 9 times.
✓ Branch 5 → 8 taken 5 times.
✓ Branch 6 → 7 taken 2 times.
✓ Branch 6 → 8 taken 7 times.
14 if (kept.outcome == Outcome::LiveSubscription && teardown == DiagnosticsTeardown::CacheShutdown)
280 {
281 2 return;
282 }
283
2/2
✓ Branch 9 → 10 taken 1 time.
✓ Branch 9 → 11 taken 11 times.
12 if (kept.recorded.exchange(true, std::memory_order_relaxed))
284 {
285 1 return;
286 }
287 11 diagnostics::record_intentional_leak(diagnostics::LeakSubsystem::Diagnostics);
288
2/2
✓ Branch 12 → 13 taken 1 time.
✓ Branch 12 → 14 taken 10 times.
11 if (!may_block)
289 {
290 1 return;
291 }
292 try
293 {
294 10 (void)log().try_log(
295 LogLevel::Warning,
296 "Diagnostics: the {} dispatcher kept its emit TLS index at teardown. Cause: {}.",
297 10 kept.dispatcher_name,
298 10 kept_cause(kept.outcome)
299 );
300 }
301 catch (...)
302 {
303 }
304 }
305 } // namespace
306
307 14564 void release_diagnostics_emit_owners(DiagnosticsTeardown teardown, bool may_block) noexcept
308 {
309 // The index dies with the process, and a killed thread can still hold a list lock.
310
2/2
✓ Branch 3 → 4 taken 4 times.
✓ Branch 3 → 5 taken 14561 times.
14564 if (process_is_exiting())
311 {
312 4822 return;
313 }
314 14561 auto *const hooks = diagnostics::s_hook_lifecycle.load(std::memory_order_acquire);
315 14559 auto *const scans = diagnostics::s_scanner_faults.load(std::memory_order_acquire);
316
6/8
✓ Branch 7 → 8 taken 4818 times.
✓ Branch 7 → 9 taken 9743 times.
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 12 taken 4818 times.
✗ Branch 11 → 12 not taken.
✓ Branch 11 → 13 taken 9743 times.
✓ Branch 14 → 15 taken 4818 times.
✓ Branch 14 → 16 taken 9743 times.
24304 if ((hooks == nullptr && scans == nullptr) || s_release_running.test_and_set(std::memory_order_acquire))
317 {
318 4818 return;
319 }
320
321 // A wait from inside a diagnostics emit, or beside an untracked emit, can wait on this thread.
322
5/6
✓ Branch 17 → 18 taken 9741 times.
✗ Branch 17 → 20 not taken.
✓ Branch 19 → 20 taken 9740 times.
✓ Branch 19 → 33 taken 1 time.
✓ Branch 20 → 21 taken 157 times.
✓ Branch 20 → 23 taken 9583 times.
9741 const bool may_wait = may_block && !(hooks != nullptr && thread_is_emitting_dispatcher(hooks)) &&
323
3/4
✓ Branch 16 → 17 taken 9741 times.
✓ Branch 16 → 33 taken 2 times.
✓ Branch 22 → 23 taken 157 times.
✗ Branch 22 → 33 not taken.
19641 !(scans != nullptr && thread_is_emitting_dispatcher(scans)) &&
324
2/2
✓ Branch 31 → 32 taken 9739 times.
✓ Branch 31 → 33 taken 1 time.
19480 untracked_emit_frames().load(std::memory_order_seq_cst) == 0;
325 9950 const auto release_hooks = [hooks, may_block]() noexcept
326
1/2
✓ Branch 2 → 3 taken 9950 times.
✗ Branch 2 → 4 not taken.
9950 { return hooks != nullptr ? DiagnosticsEmitOwner::release(*hooks, may_block) : Outcome::Returned; };
327 9743 const auto release_scans = [scans, may_block]() noexcept
328
2/2
✓ Branch 2 → 3 taken 160 times.
✓ Branch 2 → 4 taken 9583 times.
9743 { return scans != nullptr ? DiagnosticsEmitOwner::release(*scans, may_block) : Outcome::Returned; };
329 9743 Outcome hook_outcome = release_hooks();
330 9743 Outcome scan_outcome = release_scans();
331
2/2
✓ Branch 36 → 37 taken 9739 times.
✓ Branch 36 → 56 taken 4 times.
9743 if (may_wait)
332 {
333 9739 const auto deadline = std::chrono::steady_clock::now() + DIAGNOSTICS_DRAIN_TIMEOUT;
334 9739 DrainBackoff backoff;
335
5/6
✓ Branch 46 → 47 taken 9738 times.
✓ Branch 46 → 48 taken 208 times.
✗ Branch 47 → 48 not taken.
✓ Branch 47 → 53 taken 9738 times.
✓ Branch 51 → 52 taken 207 times.
✓ Branch 51 → 53 taken 1 time.
10154 while ((hook_outcome == Outcome::Busy || scan_outcome == Outcome::Busy) &&
336
2/2
✓ Branch 54 → 40 taken 207 times.
✓ Branch 54 → 55 taken 9739 times.
10154 std::chrono::steady_clock::now() < deadline)
337 {
338 207 backoff.pause();
339
1/2
✓ Branch 41 → 42 taken 207 times.
✗ Branch 41 → 43 not taken.
207 if (hook_outcome == Outcome::Busy)
340 {
341 207 hook_outcome = release_hooks();
342 }
343
1/2
✗ Branch 43 → 44 not taken.
✓ Branch 43 → 45 taken 207 times.
207 if (scan_outcome == Outcome::Busy)
344 {
345 ✗ scan_outcome = release_scans();
346 }
347 }
348 }
349 9743 settle({"hook_lifecycle", hook_outcome, s_hook_lifecycle_recorded}, teardown, may_block);
350 9743 settle({"scanner_faults", scan_outcome, s_scanner_faults_recorded}, teardown, may_block);
351 s_release_running.clear(std::memory_order_release);
352 }
353 } // namespace detail
354 } // namespace DetourModKit
355