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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 327 | for (const rtti::DriftEntry &entry : drift_report) | |
| 184 | { | ||
| 185 | 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 | 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 | 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 | 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 | 14 | if (kept.outcome == Outcome::LiveSubscription && teardown == DiagnosticsTeardown::CacheShutdown) | |
| 280 | { | ||
| 281 | 2 | return; | |
| 282 | } | ||
| 283 | 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 | 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 | 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 | 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 | 9741 | const bool may_wait = may_block && !(hooks != nullptr && thread_is_emitting_dispatcher(hooks)) && | |
| 323 | 19641 | !(scans != nullptr && thread_is_emitting_dispatcher(scans)) && | |
| 324 | 19480 | untracked_emit_frames().load(std::memory_order_seq_cst) == 0; | |
| 325 | 9950 | const auto release_hooks = [hooks, may_block]() noexcept | |
| 326 | 9950 | { return hooks != nullptr ? DiagnosticsEmitOwner::release(*hooks, may_block) : Outcome::Returned; }; | |
| 327 | 9743 | const auto release_scans = [scans, may_block]() noexcept | |
| 328 | 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 | 9743 | if (may_wait) | |
| 332 | { | ||
| 333 | 9739 | const auto deadline = std::chrono::steady_clock::now() + DIAGNOSTICS_DRAIN_TIMEOUT; | |
| 334 | 9739 | DrainBackoff backoff; | |
| 335 | 10154 | while ((hook_outcome == Outcome::Busy || scan_outcome == Outcome::Busy) && | |
| 336 | 10154 | std::chrono::steady_clock::now() < deadline) | |
| 337 | { | ||
| 338 | 207 | backoff.pause(); | |
| 339 | 207 | if (hook_outcome == Outcome::Busy) | |
| 340 | { | ||
| 341 | 207 | hook_outcome = release_hooks(); | |
| 342 | } | ||
| 343 | 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 |