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 |