include/DetourModKit/profiler.hpp
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | #ifndef DETOURMODKIT_PROFILER_HPP | ||
| 2 | #define DETOURMODKIT_PROFILER_HPP | ||
| 3 | |||
| 4 | /** | ||
| 5 | * @file profiler.hpp | ||
| 6 | * @brief Opt-in profiling instrumentation for measuring hook and subsystem timing. | ||
| 7 | * | ||
| 8 | * @details Compiles to nothing unless DMK_ENABLE_PROFILING is defined (directly, or through -DDMK_ENABLE_PROFILING=ON). | ||
| 9 | * When enabled, scoped measurements land in a fixed-capacity lock-free ring and export as Chrome Tracing JSON | ||
| 10 | * (chrome://tracing, https://ui.perfetto.dev). | ||
| 11 | * @warning `[B-100]` Run first use of Profiler::get_instance() and the export routes outside the loader lock. First | ||
| 12 | * use allocates the ring. The export routes can allocate, and export_to_file() writes a file. A warm record() | ||
| 13 | * is allocation-free from any thread. `ProfilerLoaderBoundary.*` pins the boundary. | ||
| 14 | */ | ||
| 15 | |||
| 16 | #include "DetourModKit/detail/profile_ring.hpp" | ||
| 17 | |||
| 18 | #include <cstddef> | ||
| 19 | #include <cstdint> | ||
| 20 | #include <string> | ||
| 21 | #include <string_view> | ||
| 22 | |||
| 23 | #ifdef DMK_ENABLE_PROFILING | ||
| 24 | |||
| 25 | // Two-level indirection so __LINE__ expands before token pasting. | ||
| 26 | #define DMK_CONCAT_IMPL(a, b) a##b | ||
| 27 | #define DMK_CONCAT(a, b) DMK_CONCAT_IMPL(a, b) | ||
| 28 | |||
| 29 | // Scoped timer. ScopedProfile owns the label lifetime and extent contract for `name`. | ||
| 30 | #define DMK_PROFILE_SCOPE(name) \ | ||
| 31 | ::DetourModKit::ScopedProfile DMK_CONCAT(dmk_scoped_profile_, __LINE__) \ | ||
| 32 | { \ | ||
| 33 | name \ | ||
| 34 | } | ||
| 35 | |||
| 36 | // Scoped timer for the current function. `__func__` has static storage per [dcl.fct.def.general]/8. | ||
| 37 | #define DMK_PROFILE_FUNCTION() \ | ||
| 38 | ::DetourModKit::ScopedProfile DMK_CONCAT(dmk_scoped_profile_func_, __LINE__) \ | ||
| 39 | { \ | ||
| 40 | __func__ \ | ||
| 41 | } | ||
| 42 | |||
| 43 | #else | ||
| 44 | |||
| 45 | #define DMK_PROFILE_SCOPE(name) ((void)0) | ||
| 46 | #define DMK_PROFILE_FUNCTION() ((void)0) | ||
| 47 | |||
| 48 | #endif // DMK_ENABLE_PROFILING | ||
| 49 | |||
| 50 | namespace DetourModKit | ||
| 51 | { | ||
| 52 | /** | ||
| 53 | * @brief Lock-free ring buffer profiler with Chrome Tracing JSON export. | ||
| 54 | * | ||
| 55 | * @details Recording claims one slot of a fixed power-of-two ring and publishes into it; when the ring wraps, the | ||
| 56 | * oldest samples are overwritten. A claim whose slot another writer still owns is refused and counted by | ||
| 57 | * @ref dropped_samples rather than overwriting it, so the exporter never observes a torn sample. No | ||
| 58 | * allocation, lock, or system call occurs on the recording path. | ||
| 59 | * | ||
| 60 | * The instance is process-lifetime and is never destroyed, so a ScopedProfile that outlives ordinary | ||
| 61 | * static teardown still records safely. It is per linked DMK instance, not per process: two modules that | ||
| 62 | * each link the static library each get their own profiler. | ||
| 63 | * | ||
| 64 | * **Thread safety:** | ||
| 65 | * - `record()`: lock-free, callable from any thread | ||
| 66 | * - `reset()`: safe only when no `record()` call is in flight | ||
| 67 | * - `export_chrome_json()` / `export_to_file()`: safe concurrently with `record()` | ||
| 68 | */ | ||
| 69 | class Profiler | ||
| 70 | { | ||
| 71 | public: | ||
| 72 | /// Default ring buffer capacity (must be a power of 2). | ||
| 73 | static constexpr size_t DEFAULT_CAPACITY{65536}; | ||
| 74 | |||
| 75 | Profiler(const Profiler &) = delete; | ||
| 76 | Profiler &operator=(const Profiler &) = delete; | ||
| 77 | Profiler(Profiler &&) = delete; | ||
| 78 | Profiler &operator=(Profiler &&) = delete; | ||
| 79 | |||
| 80 | /** | ||
| 81 | * @brief Returns the profiler singleton. | ||
| 82 | * @details First use publishes either a complete profiler or, if its ring cannot be allocated, a disabled one | ||
| 83 | * whose @ref capacity is 0. Recording then fails closed and increments @ref dropped_samples, export | ||
| 84 | * returns an empty trace, and reset remains safe. It never terminates or publishes a partially | ||
| 85 | * constructed profiler; a first-use failure latches for the lifetime of this linked instance. | ||
| 86 | */ | ||
| 87 | [[nodiscard]] static Profiler &get_instance() noexcept; | ||
| 88 | |||
| 89 | /** | ||
| 90 | * @brief Records a completed profile sample whose label is null-terminated. | ||
| 91 | * @param name Pointer to storage that must outlive the process. | ||
| 92 | * The ring stores it as-is, and export reads it later. A pointer whose storage is released before | ||
| 93 | * process exit is undefined behavior. A non-null @p name must be null-terminated because this | ||
| 94 | * overload measures the label here. A null @p name records a sample that export skips. Safe | ||
| 95 | * sources include string literals, namespace-scope `static constexpr char` arrays with a | ||
| 96 | * terminator, and `__func__`. | ||
| 97 | * @param start_ticks QPC tick count at scope entry. | ||
| 98 | * @param end_ticks QPC tick count at scope exit. | ||
| 99 | * Any order and magnitude is accepted. An interval with `end_ticks <= start_ticks` records | ||
| 100 | * zero. A long interval saturates. Neither case overflows. | ||
| 101 | * @param thread_id Win32 thread ID for the source thread. | ||
| 102 | * @note Lock-free. Safe to call from any thread at any time. | ||
| 103 | */ | ||
| 104 | void record(const char *name, int64_t start_ticks, int64_t end_ticks, uint32_t thread_id) noexcept; | ||
| 105 | |||
| 106 | /** | ||
| 107 | * @brief Records a completed profile sample whose label extent the caller supplies. | ||
| 108 | * @param name Pointer to label storage. | ||
| 109 | * For a non-null pointer, at least @p name_length bytes must remain readable until process exit. | ||
| 110 | * No terminator is required. A null pointer records a sample that export skips. | ||
| 111 | * @param name_length Label byte count. | ||
| 112 | * Export reads exactly this many bytes. A value above `UINT32_MAX` saturates. | ||
| 113 | * @param start_ticks QPC tick count at scope entry. | ||
| 114 | * @param end_ticks QPC tick count at scope exit. | ||
| 115 | * The null-terminated overload defines its range behavior. | ||
| 116 | * @param thread_id Win32 thread ID for the source thread. | ||
| 117 | * @details This is the bounded route. | ||
| 118 | * @ref ScopedProfile uses it for every array label. A `static constexpr char label[3]{'f','p','s'}` | ||
| 119 | * therefore exports as `fps`. Publication stores the pointer and extent together without allocation. | ||
| 120 | * @note Lock-free. Safe to call from any thread at any time. | ||
| 121 | */ | ||
| 122 | void record( | ||
| 123 | const char *name, | ||
| 124 | size_t name_length, | ||
| 125 | int64_t start_ticks, | ||
| 126 | int64_t end_ticks, | ||
| 127 | uint32_t thread_id | ||
| 128 | ) noexcept; | ||
| 129 | |||
| 130 | /** | ||
| 131 | * @brief Resets the profiler, discarding all recorded samples and counters. | ||
| 132 | * @note Not safe to call while other threads are calling record(). Intended for use between profiling sessions. | ||
| 133 | */ | ||
| 134 | void reset() noexcept; | ||
| 135 | |||
| 136 | /** | ||
| 137 | * @brief Exports recorded samples as a Chrome Tracing JSON string (array form). | ||
| 138 | * @return JSON string containing all recorded samples, or "[]" when none are resident. | ||
| 139 | */ | ||
| 140 | [[nodiscard]] std::string export_chrome_json() const; | ||
| 141 | |||
| 142 | /** | ||
| 143 | * @brief Exports recorded samples to a JSON file on disk. | ||
| 144 | * @param path File path to write (created or overwritten). | ||
| 145 | * @return true on success, false on I/O failure. | ||
| 146 | */ | ||
| 147 | [[nodiscard]] bool export_to_file(std::string_view path) const; | ||
| 148 | |||
| 149 | /// Returns the number of record() calls made (may exceed capacity due to wrapping). | ||
| 150 | [[nodiscard]] size_t total_samples_recorded() const noexcept; | ||
| 151 | |||
| 152 | /// Returns the number of committed samples available for export. | ||
| 153 | [[nodiscard]] size_t available_samples() const noexcept; | ||
| 154 | |||
| 155 | /// Returns the number of record() calls refused because their slot was still owned, or the ring is disabled. | ||
| 156 | [[nodiscard]] size_t dropped_samples() const noexcept; | ||
| 157 | |||
| 158 | /// Returns the ring buffer capacity, or 0 for a disabled profiler. | ||
| 159 | [[nodiscard]] size_t capacity() const noexcept; | ||
| 160 | |||
| 161 | /// Returns the QPC frequency (ticks per second) used for timing. | ||
| 162 | [[nodiscard]] int64_t qpc_frequency() const noexcept; | ||
| 163 | |||
| 164 | private: | ||
| 165 | Profiler() noexcept; | ||
| 166 | ~Profiler() noexcept = default; | ||
| 167 | |||
| 168 | detail::ProfileRing m_ring; | ||
| 169 | int64_t m_qpc_frequency{0}; | ||
| 170 | }; | ||
| 171 | |||
| 172 | /** | ||
| 173 | * @brief RAII scoped profiler that records timing on destruction. | ||
| 174 | * | ||
| 175 | * @details Captures the QPC tick count and thread ID on construction and records the completed sample on | ||
| 176 | * destruction. The profiling macros instantiate this class only when DMK_ENABLE_PROFILING is defined; | ||
| 177 | * direct construction always records and is intended for specialized instrumentation. | ||
| 178 | */ | ||
| 179 | class ScopedProfile | ||
| 180 | { | ||
| 181 | public: | ||
| 182 | /** | ||
| 183 | * @brief Begins a profiling scope. | ||
| 184 | * @tparam N Deduced extent of the bound array. | ||
| 185 | * For a string literal, this includes the final null. | ||
| 186 | * @param name Reference to a `const char` array. The array-reference parameter rejects decayed pointer sources | ||
| 187 | * (`std::string::c_str()`, `const char *` function arguments, `char *` buffers) at compile time. | ||
| 188 | * Reference binding still accepts an array with automatic storage, which dangles once its scope exits, | ||
| 189 | * so this does NOT prove static storage; callers must ensure the bound array outlives the process. Safe | ||
| 190 | * sources: string literals, namespace-scope `static constexpr char` arrays, and `__func__` | ||
| 191 | * (static-storage per [dcl.fct.def.general]/8). | ||
| 192 | * @details The label extent is `N` less one final null. | ||
| 193 | * The bounded @ref Profiler::record overload receives that extent and the pointer. An array with no | ||
| 194 | * terminator exports its own bytes. For a null-padded array, only the final null is removed. | ||
| 195 | */ | ||
| 196 | template <size_t N> | ||
| 197 | 1640 | explicit ScopedProfile(const char (&name)[N]) noexcept | |
| 198 |
6/12DetourModKit::ScopedProfile::ScopedProfile<10ull>(char const (&) [10ull]):
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 1 time.
DetourModKit::ScopedProfile::ScopedProfile<12ull>(char const (&) [12ull]):
✓ Branch 2 → 3 taken 1 time.
✗ Branch 2 → 4 not taken.
DetourModKit::ScopedProfile::ScopedProfile<14ull>(char const (&) [14ull]):
✓ Branch 2 → 3 taken 2 times.
✗ Branch 2 → 4 not taken.
DetourModKit::ScopedProfile::ScopedProfile<15ull>(char const (&) [15ull]):
✓ Branch 2 → 3 taken 2 times.
✗ Branch 2 → 4 not taken.
DetourModKit::ScopedProfile::ScopedProfile<17ull>(char const (&) [17ull]):
✓ Branch 2 → 3 taken 1650 times.
✗ Branch 2 → 4 not taken.
DetourModKit::ScopedProfile::ScopedProfile<21ull>(char const (&) [21ull]):
✓ Branch 2 → 3 taken 8 times.
✗ Branch 2 → 4 not taken.
|
1640 | : ScopedProfile(static_cast<const char *>(name), name[N - 1] == '\0' ? N - 1 : N, LiteralTag{}) |
| 199 | { | ||
| 200 | 1715 | } | |
| 201 | ~ScopedProfile() noexcept; | ||
| 202 | |||
| 203 | ScopedProfile(const ScopedProfile &) = delete; | ||
| 204 | ScopedProfile &operator=(const ScopedProfile &) = delete; | ||
| 205 | ScopedProfile(ScopedProfile &&) = delete; | ||
| 206 | ScopedProfile &operator=(ScopedProfile &&) = delete; | ||
| 207 | |||
| 208 | private: | ||
| 209 | struct LiteralTag | ||
| 210 | { | ||
| 211 | }; | ||
| 212 | |||
| 213 | ScopedProfile(const char *name, size_t name_length, LiteralTag) noexcept; | ||
| 214 | |||
| 215 | const char *m_name; | ||
| 216 | size_t m_name_length; | ||
| 217 | int64_t m_start_ticks; | ||
| 218 | uint32_t m_thread_id; | ||
| 219 | }; | ||
| 220 | |||
| 221 | } // namespace DetourModKit | ||
| 222 | |||
| 223 | #endif // DETOURMODKIT_PROFILER_HPP | ||
| 224 |