GCC Code Coverage Report


Directory: ./
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 100.0% 18 / 0 / 18
Functions: 100.0% 6 / 0 / 6
Branches: 50.0% 6 / 0 / 12

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/12
DetourModKit::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