GCC Code Coverage Report


Directory: ./
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 100.0% 61 / 0 / 61
Functions: 100.0% 10 / 0 / 10
Branches: 68.2% 30 / 0 / 44

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
13 #include <array>
14 #include <atomic>
15 #include <cstddef>
16 #include <new>
17
18 namespace DetourModKit
19 {
20 namespace diagnostics
21 {
22 namespace
23 {
24 constexpr std::size_t LEAK_SUBSYSTEM_COUNT = static_cast<std::size_t>(LeakSubsystem::Count);
25
26 // One independent event tally per subsystem. Relaxed throughout: the counters carry no ordering obligation
27 // toward any other state.
28 std::array<std::atomic<std::size_t>, LEAK_SUBSYSTEM_COUNT> s_leak_counts{};
29
30 constexpr std::size_t MODULE_PIN_REASON_COUNT = static_cast<std::size_t>(ModulePinReason::Count);
31 static_assert(
32 MODULE_PIN_REASON_COUNT == DetourModKit::detail::module_pin_observability::MODULE_PIN_REASON_COUNT,
33 "ModulePinReason::Count and the internal counter array size must stay equal"
34 );
35 } // namespace
36
37 295 void record_intentional_leak(LeakSubsystem subsystem) noexcept
38 {
39 295 const auto index = static_cast<std::size_t>(subsystem);
40
2/2
✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 4 taken 294 times.
295 if (index >= LEAK_SUBSYSTEM_COUNT)
41 {
42 1 return;
43 }
44 294 s_leak_counts[index].fetch_add(1, std::memory_order_relaxed);
45 }
46
47 1916 std::size_t intentional_leak_count(LeakSubsystem subsystem) noexcept
48 {
49 1916 const auto index = static_cast<std::size_t>(subsystem);
50
2/2
✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 4 taken 1915 times.
1916 if (index >= LEAK_SUBSYSTEM_COUNT)
51 {
52 1 return 0;
53 }
54 3830 return s_leak_counts[index].load(std::memory_order_relaxed);
55 }
56
57 240 std::size_t total_intentional_leaks() noexcept
58 {
59 240 std::size_t total = 0;
60
2/2
✓ Branch 11 → 3 taken 1920 times.
✓ Branch 11 → 12 taken 240 times.
2160 for (const auto &counter : s_leak_counts)
61 {
62 3840 total += counter.load(std::memory_order_relaxed);
63 }
64 240 return total;
65 }
66
67 45 void reset_intentional_leaks() noexcept
68 {
69
2/2
✓ Branch 12 → 3 taken 360 times.
✓ Branch 12 → 13 taken 45 times.
405 for (auto &counter : s_leak_counts)
70 {
71 360 counter.store(0, std::memory_order_relaxed);
72 }
73 45 }
74
75 2992 std::size_t module_pin_count(ModulePinReason reason) noexcept
76 {
77 2992 const auto index = static_cast<std::size_t>(reason);
78
2/2
✓ Branch 2 → 3 taken 2 times.
✓ Branch 2 → 4 taken 2990 times.
2992 if (index >= MODULE_PIN_REASON_COUNT)
79 {
80 2 return 0;
81 }
82 5980 return DetourModKit::detail::module_pin_observability::s_outstanding[index].load(std::memory_order_relaxed);
83 }
84
85 330 std::size_t total_module_pins() noexcept
86 {
87 330 std::size_t total = 0;
88
2/2
✓ Branch 11 → 3 taken 3630 times.
✓ Branch 11 → 12 taken 330 times.
3960 for (const auto &counter : DetourModKit::detail::module_pin_observability::s_outstanding)
89 {
90 7260 total += counter.load(std::memory_order_relaxed);
91 }
92 330 return total;
93 }
94
95 2921 LifecycleCounters lifecycle_counters() noexcept
96 {
97 2921 LifecycleCounters counters;
98 2921 counters.reaper_started =
99 2921 DetourModKit::detail::lifecycle_observability::s_reaper_started.load(std::memory_order_relaxed);
100 2921 counters.permanent_pins =
101 2921 DetourModKit::detail::lifecycle_observability::s_permanent_pins.load(std::memory_order_relaxed);
102 2921 counters.abandoned_owners =
103 2921 DetourModKit::detail::lifecycle_observability::s_abandoned_owners.load(std::memory_order_relaxed);
104 2921 return counters;
105 }
106
107 3704 EventDispatcher<ScannerFaultEvent> &scanner_faults()
108 {
109 // Never destroyed, for the same reason as hook_lifecycle(). A scan can be driven from a namespace-scope
110 // object's destructor or from a module-pinned thread that outlives this TU's static destructors, and the
111 // consumer's own Subscription can likewise be destroyed after them; both would then reach a destroyed
112 // mutex and subscriber list, which no try/catch can contain because it is undefined behaviour rather than
113 // an exception.
114 alignas(
115 EventDispatcher<ScannerFaultEvent>
116 ) static unsigned char storage[sizeof(EventDispatcher<ScannerFaultEvent>)];
117 static EventDispatcher<ScannerFaultEvent> *const dispatcher =
118
5/12
✓ Branch 2 → 3 taken 8 times.
✓ Branch 2 → 10 taken 3696 times.
✓ Branch 4 → 5 taken 8 times.
✗ Branch 4 → 10 not taken.
✓ Branch 6 → 7 taken 8 times.
✗ Branch 6 → 12 not taken.
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 8 times.
✗ Branch 12 → 13 not taken.
✗ Branch 12 → 14 not taken.
✗ Branch 15 → 16 not taken.
✗ Branch 15 → 17 not taken.
3704 ::new (static_cast<void *>(storage)) EventDispatcher<ScannerFaultEvent>();
119 3704 return *dispatcher;
120 }
121
122 8595 EventDispatcher<HookLifecycleEvent> &hook_lifecycle()
123 {
124 // Never destroyed because ~Hook and ~VmtHook may emit after this translation unit's static destructors.
125 alignas(
126 EventDispatcher<HookLifecycleEvent>
127 ) static unsigned char storage[sizeof(EventDispatcher<HookLifecycleEvent>)];
128 static EventDispatcher<HookLifecycleEvent> *const dispatcher =
129
5/12
✓ Branch 2 → 3 taken 354 times.
✓ Branch 2 → 10 taken 8241 times.
✓ Branch 4 → 5 taken 354 times.
✗ Branch 4 → 10 not taken.
✓ Branch 6 → 7 taken 354 times.
✗ Branch 6 → 12 not taken.
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 354 times.
✗ Branch 12 → 13 not taken.
✗ Branch 12 → 14 not taken.
✗ Branch 15 → 16 not taken.
✗ Branch 15 → 17 not taken.
8595 ::new (static_cast<void *>(storage)) EventDispatcher<HookLifecycleEvent>();
130 8595 return *dispatcher;
131 }
132
133 Snapshot
134 151 collect(std::span<const rtti::DriftEntry> drift_report, std::span<const anchor::ResolvedAnchor> anchor_report)
135 {
136 151 Snapshot snapshot;
137
138 // Derive the total by summing the per-subsystem values captured into this snapshot (rather than a second
139 // independent total_intentional_leaks() read), so snapshot.total_intentional_leaks always equals the sum of
140 // the breakdown even if a counter is incremented concurrently between the copy and the total.
141
2/2
✓ Branch 9 → 3 taken 1208 times.
✓ Branch 9 → 10 taken 151 times.
1510 for (std::size_t i = 0; i < snapshot.intentional_leaks.size(); ++i)
142 {
143 1208 snapshot.intentional_leaks[i] = intentional_leak_count(static_cast<LeakSubsystem>(i));
144 1208 snapshot.total_intentional_leaks += snapshot.intentional_leaks[i];
145 }
146
147 151 DetourModKit::detail::hook_population::read(
148 snapshot.hooks_total,
149 snapshot.hooks_active,
150 snapshot.hooks_disabled
151 );
152
153 // Same derivation rule as the leak total: sum the captured breakdown, not a second independent read.
154
2/2
✓ Branch 18 → 12 taken 1661 times.
✓ Branch 18 → 19 taken 151 times.
1963 for (std::size_t i = 0; i < snapshot.module_pins.size(); ++i)
155 {
156 1661 snapshot.module_pins[i] = module_pin_count(static_cast<ModulePinReason>(i));
157 1661 snapshot.total_module_pins += snapshot.module_pins[i];
158 }
159
160 151 snapshot.lifecycle = lifecycle_counters();
161
162 151 snapshot.drift_total = drift_report.size();
163
2/2
✓ Branch 37 → 23 taken 3 times.
✓ Branch 37 → 38 taken 151 times.
305 for (const rtti::DriftEntry &entry : drift_report)
164 {
165
2/2
✓ Branch 25 → 26 taken 2 times.
✓ Branch 25 → 27 taken 1 time.
3 if (entry.ok)
166 {
167 2 ++snapshot.drift_healed;
168 }
169 else
170 {
171 1 ++snapshot.drift_failed;
172 }
173 }
174
175 151 snapshot.anchor_quality = anchor::assess_quality(anchor_report);
176
177 151 return snapshot;
178 }
179 } // namespace diagnostics
180 } // namespace DetourModKit
181