GCC Code Coverage Report


Directory: ./
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 100.0% 11 / 0 / 11
Functions: 100.0% 5 / 0 / 5
Branches: -% 0 / 0 / 0

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 Provides zero-overhead profiling when disabled at compile time. When enabled via DMK_ENABLE_PROFILING,
9 * records scoped timing samples into a lock-free ring buffer and exports to Chrome Tracing JSON format
10 * (viewable in chrome://tracing or https://ui.perfetto.dev).
11 *
12 * **Compile-time control:**
13 * - Define DMK_ENABLE_PROFILING before including this header, or
14 * - Pass -DDMK_ENABLE_PROFILING=ON to CMake.
15 *
16 * **Performance characteristics (when enabled):**
17 * - ~50 ns per scoped measurement (two QPC calls + one atomic store)
18 * - Fixed-size ring buffer (no heap allocations on the hot path)
19 * - Lock-free recording from multiple threads
20 *
21 * **Usage:**
22 * @code
23 * void on_camera_update(void* camera_ptr) {
24 * DMK_PROFILE_SCOPE("camera_update");
25 * // ... hook logic ...
26 * }
27 *
28 * // Export after a profiling session
29 * DMKProfiler::get_instance().export_to_file("profile.json");
30 * @endcode
31 */
32
33 #include <atomic>
34 #include <cstddef>
35 #include <cstdint>
36 #include <memory>
37 #include <string>
38 #include <string_view>
39
40 #ifdef DMK_ENABLE_PROFILING
41
42 // Two-level indirection so __LINE__ expands before token pasting.
43 #define DMK_CONCAT_IMPL(a, b) a##b
44 #define DMK_CONCAT(a, b) DMK_CONCAT_IMPL(a, b)
45
46 // Scoped timing measurement. The `name` argument must refer to storage that outlives the process, because the pointer
47 // is stored unchanged in the ring buffer and read asynchronously by export methods. String literals satisfy this
48 // automatically.
49 //
50 // The ScopedProfile(const char (&)[N]) constructor rejects decayed `const char *` / `char *` sources (see
51 // static_asserts in test_profiler.cpp), but array-reference binding accepts any array, including function-local `char
52 // buf[N]`. Callers remain responsible for static-storage lifetime. Prefer string literals or namespace-scope `static
53 // constexpr char` arrays.
54 #define DMK_PROFILE_SCOPE(name) \
55 ::DetourModKit::ScopedProfile DMK_CONCAT(dmk_scoped_profile_, __LINE__) \
56 { \
57 name \
58 }
59
60 // Scoped timing using the enclosing function name. `__func__` is a static-storage array per [dcl.fct.def.general]/8, so
61 // it binds to the array-reference constructor and the stored pointer remains valid for the lifetime of the process.
62 #define DMK_PROFILE_FUNCTION() \
63 ::DetourModKit::ScopedProfile DMK_CONCAT(dmk_scoped_profile_func_, __LINE__) \
64 { \
65 __func__ \
66 }
67
68 #else
69
70 #define DMK_PROFILE_SCOPE(name) ((void)0)
71 #define DMK_PROFILE_FUNCTION() ((void)0)
72
73 #endif // DMK_ENABLE_PROFILING
74
75 namespace DetourModKit
76 {
77 /**
78 * @brief A single timing sample recorded by the profiler.
79 * @details The sequence field uses odd/even protocol to detect in-flight
80 * writes: record() stores an odd sequence before writing fields
81 * and an even sequence after. Readers skip samples with odd sequence values (torn/in-progress writes).
82 */
83 struct ProfileSample
84 {
85 /// Odd = write in progress, even = committed.
86 std::atomic<uint32_t> sequence{0};
87 /**
88 * @brief Non-owning pointer to the sample name.
89 * @note Caller must ensure the pointed-to string outlives the process (e.g. a string literal or a
90 * namespace-scope `static constexpr char` array). The ScopedProfile
91 * array-reference constructor only rejects pointer decay;
92 * it does NOT verify static-storage.
93 */
94 const char *name{nullptr};
95 /// QPC tick count at scope entry.
96 int64_t start_ticks{0};
97 /// Duration in microseconds (max ~71 minutes).
98 uint32_t duration_us{0};
99 /// Win32 thread ID of the recording thread.
100 uint32_t thread_id{0};
101
102 1835008 ProfileSample() noexcept = default;
103 ProfileSample(const ProfileSample &) = delete;
104 ProfileSample &operator=(const ProfileSample &) = delete;
105 ProfileSample(ProfileSample &&) = delete;
106 ProfileSample &operator=(ProfileSample &&) = delete;
107 };
108
109 /**
110 * @brief Lock-free ring buffer profiler with Chrome Tracing JSON export.
111 *
112 * @details Uses a fixed-capacity power-of-2 ring buffer. Recording is lock-free via a single atomic fetch_add on
113 * the write position. When the buffer wraps, oldest samples are silently overwritten (no allocation, no
114 * lock).
115 *
116 * The profiler is a singleton. All public methods are safe to call from multiple threads. Export methods
117 * take a consistent snapshot by reading the current write position and walking backwards.
118 *
119 * **Thread safety:**
120 * - `record()`: lock-free (atomic fetch_add + sequence counter)
121 * - `reset()`: safe when no concurrent `record()` calls are in flight
122 * - `export_chrome_json()` / `export_to_file()`: safe to call concurrently
123 * with `record()`. Uses odd/even sequence protocol to skip in-flight writes, preventing torn reads in the
124 * exported data
125 */
126 class Profiler
127 {
128 public:
129 /// Default ring buffer capacity (must be a power of 2).
130 static constexpr size_t DEFAULT_CAPACITY{65536};
131
132 Profiler(const Profiler &) = delete;
133 Profiler &operator=(const Profiler &) = delete;
134 Profiler(Profiler &&) = delete;
135 Profiler &operator=(Profiler &&) = delete;
136
137 /// Returns the global profiler singleton.
138 [[nodiscard]] static Profiler &get_instance() noexcept;
139
140 /**
141 * @brief Records a completed timing sample.
142 * @param name Non-owning pointer that must outlive the process. The pointer is stored as-is in the ring buffer
143 * and read asynchronously by export methods. Passing a pointer whose storage is released before
144 * process exit (std::string::c_str(), heap buffers, function-local arrays) is undefined behavior.
145 * Neither this entry point nor the ScopedProfile(const char (&)[N]) constructor enforces
146 * static-storage at compile time; array-reference binding accepts any array, so callers remain
147 * responsible for lifetime. Safe sources: string literals, `static constexpr char` arrays at
148 * namespace scope, and `__func__` (see [dcl.fct.def.general]/8).
149 * @param start_ticks QPC tick count at scope entry.
150 * @param end_ticks QPC tick count at scope exit.
151 * @param thread_id Win32 thread ID of the recording thread.
152 * @note Lock-free. Safe to call from any thread at any time.
153 */
154 void record(const char *name, int64_t start_ticks, int64_t end_ticks, uint32_t thread_id) noexcept;
155
156 /**
157 * @brief Resets the profiler, discarding all recorded samples.
158 * @note Not safe to call while other threads are calling record(). Intended for use between profiling sessions.
159 */
160 void reset() noexcept;
161
162 /**
163 * @brief Exports recorded samples as a Chrome Tracing JSON string.
164 * @details Output conforms to the Chrome Trace Event Format (array form). Open the result in chrome://tracing
165 * or https://ui.perfetto.dev.
166 * @return JSON string containing all recorded samples.
167 */
168 [[nodiscard]] std::string export_chrome_json() const;
169
170 /**
171 * @brief Exports recorded samples to a JSON file on disk.
172 * @param path File path to write (created or overwritten).
173 * @return true on success, false on I/O failure.
174 */
175 [[nodiscard]] bool export_to_file(std::string_view path) const;
176
177 /// Returns the number of samples recorded (may exceed capacity due to wrapping).
178 [[nodiscard]] size_t total_samples_recorded() const noexcept;
179
180 /// Returns the number of valid samples available for export (min of recorded, capacity).
181 [[nodiscard]] size_t available_samples() const noexcept;
182
183 /// Returns the ring buffer capacity.
184 [[nodiscard]] size_t capacity() const noexcept;
185
186 /// Returns the QPC frequency (ticks per second) used for timing.
187 [[nodiscard]] int64_t qpc_frequency() const noexcept;
188
189 private:
190 Profiler();
191 28 ~Profiler() noexcept = default;
192
193 // m_write_pos first to avoid 40 bytes of padding (alignas(64) requirement). This placement ensures cache-line
194 // alignment for the lock-free ring buffer.
195 alignas(64) std::atomic<size_t> m_write_pos{0};
196 std::unique_ptr<ProfileSample[]> m_buffer;
197 size_t m_capacity;
198 size_t m_mask; // m_capacity - 1 for power-of-2 index wrapping
199 int64_t m_qpc_frequency{0};
200 };
201
202 /**
203 * @brief RAII scoped profiler that records timing on destruction.
204 *
205 * @details Captures QPC tick count and thread ID in the constructor. On destruction, computes duration and records
206 * the sample in the global Profiler ring buffer.
207 *
208 * This class is only active when DMK_ENABLE_PROFILING is defined. Use the DMK_PROFILE_SCOPE() macro
209 * instead of constructing directly.
210 */
211 class ScopedProfile
212 {
213 public:
214 /**
215 * @brief Begins a profiling scope.
216 * @tparam N Deduced length of the bound array (including the trailing null terminator when the source is a
217 * string literal).
218 * @param name Reference to a `const char` array. The array-reference
219 * parameter rejects decayed pointer sources (`std::string::
220 * c_str()`, `const char *` function arguments, `char *` buffers) at compile time, so those fail to bind
221 * and produce a compile error. However, C++ reference binding also accepts arrays with automatic storage
222 * (e.g. `char buf[N] = "...";` inside a function), which decays to a dangling pointer once the enclosing
223 * scope exits. This overload does NOT prove static storage; callers must still ensure the bound array
224 * outlives the process. Safe sources: string literals, namespace-scope `static constexpr char` arrays,
225 * and `__func__` (static-storage per [dcl.fct.def.general]/8).
226 * @note Hot-path cost: two pointer-sized stores (name pointer and thread id) plus the QPC read; the
227 * array-reference overload adds no runtime overhead over a raw `const char *` parameter.
228 */
229 template <size_t N>
230 1755 explicit ScopedProfile(const char (&name)[N]) noexcept
231 1755 : ScopedProfile(static_cast<const char *>(name), literal_tag{})
232 {
233 1794 }
234 ~ScopedProfile() noexcept;
235
236 ScopedProfile(const ScopedProfile &) = delete;
237 ScopedProfile &operator=(const ScopedProfile &) = delete;
238 ScopedProfile(ScopedProfile &&) = delete;
239 ScopedProfile &operator=(ScopedProfile &&) = delete;
240
241 private:
242 struct literal_tag
243 {
244 };
245
246 ScopedProfile(const char *name, literal_tag) noexcept;
247
248 const char *m_name;
249 int64_t m_start_ticks;
250 uint32_t m_thread_id;
251 };
252
253 } // namespace DetourModKit
254
255 #endif // DETOURMODKIT_PROFILER_HPP
256