src/internal/diagnostics_population.hpp
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | #ifndef DETOURMODKIT_INTERNAL_DIAGNOSTICS_POPULATION_HPP | ||
| 2 | #define DETOURMODKIT_INTERNAL_DIAGNOSTICS_POPULATION_HPP | ||
| 3 | |||
| 4 | /** | ||
| 5 | * @file diagnostics_population.hpp | ||
| 6 | * @brief Defines constant-initialized module-pin, hook-population, and lifecycle counters. | ||
| 7 | */ | ||
| 8 | |||
| 9 | #include <array> | ||
| 10 | #include <atomic> | ||
| 11 | #include <cstddef> | ||
| 12 | #include <cstdint> | ||
| 13 | |||
| 14 | namespace DetourModKit::diagnostics | ||
| 15 | { | ||
| 16 | enum class ModulePinReason : std::uint8_t; // Full definition: DetourModKit/diagnostics.hpp. | ||
| 17 | } // namespace DetourModKit::diagnostics | ||
| 18 | |||
| 19 | namespace DetourModKit::detail | ||
| 20 | { | ||
| 21 | namespace hook_population | ||
| 22 | { | ||
| 23 | /// Active occupies the low 32 bits, total the high 32. | ||
| 24 | inline constexpr std::uint64_t ACTIVE_UNIT = 1ULL; | ||
| 25 | inline constexpr std::uint64_t TOTAL_UNIT = 1ULL << 32U; | ||
| 26 | inline constexpr std::uint64_t ACTIVE_MASK = 0xFFFFFFFFULL; | ||
| 27 | |||
| 28 | // Loader-lock teardown requires updates that cannot allocate or enter a lock-backed atomic implementation. | ||
| 29 | static_assert( | ||
| 30 | std::atomic<std::uint64_t>::is_always_lock_free, | ||
| 31 | "the hook population tally must be lock-free to stay callable under the loader lock" | ||
| 32 | ); | ||
| 33 | |||
| 34 | // constinit makes any future dynamic initialization a compile-time error. | ||
| 35 | inline constinit std::atomic<std::uint64_t> s_counts{0}; | ||
| 36 | |||
| 37 | /** | ||
| 38 | * @brief Records a completed install. | ||
| 39 | * @param active True if creation publishes Active. VMT hooks start Active. Inline and mid hooks start Disabled. | ||
| 40 | */ | ||
| 41 | 14768 | inline void record_created(bool active) noexcept | |
| 42 | { | ||
| 43 |
2/2✓ Branch 2 → 3 taken 103 times.
✓ Branch 2 → 4 taken 14665 times.
|
14768 | s_counts.fetch_add(active ? TOTAL_UNIT + ACTIVE_UNIT : TOTAL_UNIT, std::memory_order_relaxed); |
| 44 | 14768 | } | |
| 45 | |||
| 46 | /// Records a completed disabled-to-active transition. | ||
| 47 | 18159 | inline void record_enabled() noexcept | |
| 48 | { | ||
| 49 | s_counts.fetch_add(ACTIVE_UNIT, std::memory_order_relaxed); | ||
| 50 | 18159 | } | |
| 51 | |||
| 52 | /// Records a completed active-to-disabled transition. | ||
| 53 | 17154 | inline void record_disabled() noexcept | |
| 54 | { | ||
| 55 | s_counts.fetch_sub(ACTIVE_UNIT, std::memory_order_relaxed); | ||
| 56 | 17154 | } | |
| 57 | |||
| 58 | /** | ||
| 59 | * @brief Records removal from the live population. | ||
| 60 | * @param was_active True if the entry state counted this hook as Active. Teardown can force Disabled before it | ||
| 61 | * emits. Pass the entry state so removal subtracts the Active unit. | ||
| 62 | */ | ||
| 63 | 15655 | inline void record_removed(bool was_active) noexcept | |
| 64 | { | ||
| 65 |
2/2✓ Branch 2 → 3 taken 322 times.
✓ Branch 2 → 4 taken 15333 times.
|
15655 | s_counts.fetch_sub(was_active ? TOTAL_UNIT + ACTIVE_UNIT : TOTAL_UNIT, std::memory_order_relaxed); |
| 66 | 15655 | } | |
| 67 | |||
| 68 | /// Reads all three figures from one load, so they are always mutually consistent. | ||
| 69 | 14045 | inline void read(std::size_t &total, std::size_t &active, std::size_t &disabled) noexcept | |
| 70 | { | ||
| 71 | 14348 | const std::uint64_t packed = s_counts.load(std::memory_order_relaxed); | |
| 72 | 14348 | total = static_cast<std::size_t>(packed >> 32U); | |
| 73 | 14348 | active = static_cast<std::size_t>(packed & ACTIVE_MASK); | |
| 74 | 14348 | disabled = total - active; | |
| 75 | 14348 | } | |
| 76 | } // namespace hook_population | ||
| 77 | |||
| 78 | /** | ||
| 79 | * @brief Provides counters behind @ref DetourModKit::diagnostics::lifecycle_counters. | ||
| 80 | * @details Each relaxed atomic is an independent monotonic event tally with no cross-counter order obligation. The | ||
| 81 | * recorders run on reaper and teardown paths: | ||
| 82 | * - They allocate no memory. | ||
| 83 | * - They take no lock. | ||
| 84 | * - They make no Win32 call. | ||
| 85 | */ | ||
| 86 | namespace lifecycle_observability | ||
| 87 | { | ||
| 88 | static_assert( | ||
| 89 | std::atomic<std::size_t>::is_always_lock_free, | ||
| 90 | "lifecycle observability must stay lock-free on teardown paths" | ||
| 91 | ); | ||
| 92 | |||
| 93 | inline constinit std::atomic<std::size_t> s_reaper_started{0}; | ||
| 94 | inline constinit std::atomic<std::size_t> s_permanent_pins{0}; | ||
| 95 | inline constinit std::atomic<std::size_t> s_abandoned_owners{0}; | ||
| 96 | |||
| 97 | /// Records the process-lifetime reaper thread launch and its permanent module reference. | ||
| 98 | 10 | inline void record_reaper_started() noexcept | |
| 99 | { | ||
| 100 | s_reaper_started.fetch_add(1, std::memory_order_relaxed); | ||
| 101 | s_permanent_pins.fetch_add(1, std::memory_order_relaxed); | ||
| 102 | 10 | } | |
| 103 | |||
| 104 | /// Records a failed retirement that the reaper retains permanently. | ||
| 105 | 3 | inline void record_abandoned_owner() noexcept | |
| 106 | { | ||
| 107 | s_abandoned_owners.fetch_add(1, std::memory_order_relaxed); | ||
| 108 | 3 | } | |
| 109 | } // namespace lifecycle_observability | ||
| 110 | |||
| 111 | /** | ||
| 112 | * @brief Provides the outstanding-count storage behind @ref DetourModKit::diagnostics::module_pin_count. | ||
| 113 | * @details One relaxed atomic per @ref DetourModKit::diagnostics::ModulePinReason holds acquires minus releases. | ||
| 114 | * The recorders run on install, teardown, and loader-lock paths: | ||
| 115 | * - They allocate no memory. | ||
| 116 | * - They take no lock. | ||
| 117 | * - They make no Win32 call. | ||
| 118 | */ | ||
| 119 | namespace module_pin_observability | ||
| 120 | { | ||
| 121 | /// Mirrors ModulePinReason::Count. diagnostics.cpp static_asserts that the two values stay equal. | ||
| 122 | inline constexpr std::size_t MODULE_PIN_REASON_COUNT = 11; | ||
| 123 | |||
| 124 | static_assert( | ||
| 125 | std::atomic<std::size_t>::is_always_lock_free, | ||
| 126 | "module pin observability must stay lock-free on teardown paths" | ||
| 127 | ); | ||
| 128 | |||
| 129 | inline constinit std::array<std::atomic<std::size_t>, MODULE_PIN_REASON_COUNT> s_outstanding{}; | ||
| 130 | |||
| 131 | /// Records one counted module reference taken under @p reason. | ||
| 132 | 3407 | inline void note_acquired(DetourModKit::diagnostics::ModulePinReason reason) noexcept | |
| 133 | { | ||
| 134 | 3407 | const auto index = static_cast<std::size_t>(reason); | |
| 135 |
1/2✓ Branch 2 → 3 taken 3407 times.
✗ Branch 2 → 7 not taken.
|
3407 | if (index < MODULE_PIN_REASON_COUNT) |
| 136 | { | ||
| 137 | 3407 | s_outstanding[index].fetch_add(1, std::memory_order_relaxed); | |
| 138 | } | ||
| 139 | 3407 | } | |
| 140 | |||
| 141 | /** | ||
| 142 | * @brief Records the release of one counted module reference taken under @p reason. | ||
| 143 | * @details A release must pass the reason its acquire passed. | ||
| 144 | * An unmatched release wraps the unsigned count. | ||
| 145 | * The wrap exposes the imbalance. | ||
| 146 | */ | ||
| 147 | 3255 | inline void note_released(DetourModKit::diagnostics::ModulePinReason reason) noexcept | |
| 148 | { | ||
| 149 | 3255 | const auto index = static_cast<std::size_t>(reason); | |
| 150 |
1/2✓ Branch 2 → 3 taken 3255 times.
✗ Branch 2 → 7 not taken.
|
3255 | if (index < MODULE_PIN_REASON_COUNT) |
| 151 | { | ||
| 152 | 3255 | s_outstanding[index].fetch_sub(1, std::memory_order_relaxed); | |
| 153 | } | ||
| 154 | 3255 | } | |
| 155 | } // namespace module_pin_observability | ||
| 156 | } // namespace DetourModKit::detail | ||
| 157 | |||
| 158 | #endif // DETOURMODKIT_INTERNAL_DIAGNOSTICS_POPULATION_HPP | ||
| 159 |