./
Coverage:
low: ≥ 0%
medium: ≥ 75.0%
high: ≥ 90.0%
Lines:
18 / 18
100.0%
Branches:
6 / 12
50.0%
Functions:
6 / 6
100.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 timing instrumentation with a lock-free ring and Chrome Tracing JSON export.
7 * @details DMK_PROFILE_SCOPE and DMK_PROFILE_FUNCTION expand to a no-op unless DMK_ENABLE_PROFILING is defined. The
8 * CMake option DMK_ENABLE_PROFILING=ON defines it for every target that links DetourModKit.
9 * @warning `[B-100]` Run first use of Profiler::get_instance() and the export routes outside the loader lock. First
10 * use allocates the ring. The export routes can allocate, and export_to_file() writes a file. A warm record()
11 * is allocation-free from any thread. `ProfilerLoaderBoundary.*` pins the boundary.
12 */
13
14 #include "DetourModKit/detail/profile_ring.hpp"
15
16 #include <cstddef>
17 #include <cstdint>
18 #include <string>
19 #include <string_view>
20
21 #ifdef DMK_ENABLE_PROFILING
22
23 // Two-level indirection so __LINE__ expands before token pasting.
24 #define DMK_CONCAT_IMPL(a, b) a##b
25 #define DMK_CONCAT(a, b) DMK_CONCAT_IMPL(a, b)
26
27 // Scoped timer. ScopedProfile owns the label lifetime and extent contract for `name`.
28 #define DMK_PROFILE_SCOPE(name) \
29 ::DetourModKit::ScopedProfile DMK_CONCAT(dmk_scoped_profile_, __LINE__) \
30 { \
31 name \
32 }
33
34 // Scoped timer for the current function. `__func__` has static storage per [dcl.fct.def.general]/8.
35 #define DMK_PROFILE_FUNCTION() \
36 ::DetourModKit::ScopedProfile DMK_CONCAT(dmk_scoped_profile_func_, __LINE__) \
37 { \
38 __func__ \
39 }
40
41 #else
42
43 #define DMK_PROFILE_SCOPE(name) ((void)0)
44 #define DMK_PROFILE_FUNCTION() ((void)0)
45
46 #endif // DMK_ENABLE_PROFILING
47
48 namespace DetourModKit
49 {
50 /**
51 * @brief Lock-free ring buffer profiler with Chrome Tracing JSON export.
52 * @details Each record() call claims one slot of a fixed ring, and new samples overwrite the oldest. A claim on a
53 * slot that another writer still owns is refused, so export never sees a torn sample. Export is safe
54 * concurrently with record(). The instance is never destroyed, so a ScopedProfile that outlives static
55 * teardown still records safely. Each linked DMK instance has its own profiler.
56 */
57 class Profiler
58 {
59 public:
60 /// Default ring buffer capacity, a power of two.
61 static constexpr size_t DEFAULT_CAPACITY{65536};
62
63 Profiler(const Profiler &) = delete;
64 Profiler &operator=(const Profiler &) = delete;
65 Profiler(Profiler &&) = delete;
66 Profiler &operator=(Profiler &&) = delete;
67
68 /**
69 * @brief Returns the profiler singleton.
70 * @details If first use cannot allocate the ring, it publishes a disabled profiler for the life of this DMK
71 * instance. A disabled profiler exports an empty trace and resets safely. First use never terminates.
72 */
73 [[nodiscard]] static Profiler &get_instance() noexcept;
74
75 /**
76 * @brief Records a completed profile sample whose label is null-terminated.
77 * @param name A label that export reads later. Its storage must stay readable until process exit, or the
78 * behavior is undefined. A non-null @p name must be null-terminated. A null @p name records a
79 * sample that export skips. @ref ScopedProfile lists safe label sources.
80 * @param start_ticks QPC tick count at scope entry.
81 * @param end_ticks QPC tick count at scope exit. An interval with `end_ticks <= start_ticks` records zero. A
82 * long interval saturates and never overflows.
83 * @param thread_id Win32 thread ID of the source thread.
84 * @note Lock-free, with no allocation or system call. Safe to call from any thread, but not during reset().
85 */
86 void record(const char *name, int64_t start_ticks, int64_t end_ticks, uint32_t thread_id) noexcept;
87
88 /**
89 * @brief Records a completed profile sample whose label extent the caller supplies. The other parameters
90 * follow the null-terminated overload.
91 * @param name If @p name is not null, @p name_length bytes must stay readable until process exit. No terminator
92 * is required. A null @p name records a sample that export skips.
93 * @param name_length Export reads exactly this many bytes. A value above `UINT32_MAX` saturates.
94 * @note Lock-free, with no allocation or system call. Safe to call from any thread, but not during reset().
95 */
96 void record(
97 const char *name,
98 size_t name_length,
99 int64_t start_ticks,
100 int64_t end_ticks,
101 uint32_t thread_id
102 ) noexcept;
103
104 /** @brief Discards all samples and counters. Requires that no record() call and no export is in flight. */
105 void reset() noexcept;
106
107 /** @brief Exports the resident samples as a Chrome Tracing JSON array, or "[]" if none are resident. */
108 [[nodiscard]] std::string export_chrome_json() const;
109
110 /**
111 * @brief Writes the Chrome Tracing JSON to the UTF-8 @p path, which it creates or overwrites.
112 * @return true on success, or false on ill-formed UTF-8, an embedded NUL, or an I/O failure.
113 */
114 [[nodiscard]] bool export_to_file(std::string_view path) const;
115
116 /// Returns the number of record() calls. The count can exceed the capacity after the ring wraps.
117 [[nodiscard]] size_t total_samples_recorded() const noexcept;
118
119 /// Returns the number of resident samples. Export skips the null-label samples among them.
120 [[nodiscard]] size_t available_samples() const noexcept;
121
122 /// Returns the number of record() calls refused because the ring is disabled or another writer took their slot.
123 [[nodiscard]] size_t dropped_samples() const noexcept;
124
125 /// Returns the ring buffer capacity, or 0 for a disabled profiler.
126 [[nodiscard]] size_t capacity() const noexcept;
127
128 /// Returns the QPC frequency (ticks per second) used for timing.
129 [[nodiscard]] int64_t qpc_frequency() const noexcept;
130
131 private:
132 Profiler() noexcept;
133 ~Profiler() noexcept = default;
134
135 detail::ProfileRing m_ring;
136 int64_t m_qpc_frequency{0};
137 };
138
139 /**
140 * @brief Captures the start tick and thread ID at construction and records one sample at destruction. The profiling
141 * macros create one only when DMK_ENABLE_PROFILING is defined, but direct construction always records.
142 */
143 class ScopedProfile
144 {
145 public:
146 /**
147 * @brief Begins a profiling scope.
148 * @param name An array that must stay readable until process exit. An automatic array compiles but dangles.
149 * Safe sources are string literals, namespace-scope `static constexpr char` arrays, and `__func__`.
150 * @details The label extent is `N` without one final null. An array with no terminator exports all its bytes.
151 */
152 template <size_t N>
153 1754 explicit ScopedProfile(const char (&name)[N]) noexcept
154
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 1747 times.
✗ Branch 2 → 4 not taken.
DetourModKit::ScopedProfile::ScopedProfile<21ull>(char const (&) [21ull]):
✓ Branch 2 → 3 taken 8 times.
✗ Branch 2 → 4 not taken.
1754 : ScopedProfile(static_cast<const char *>(name), name[N - 1] == '\0' ? N - 1 : N, LiteralTag{})
155 {
156 1781 }
157 ~ScopedProfile() noexcept;
158
159 ScopedProfile(const ScopedProfile &) = delete;
160 ScopedProfile &operator=(const ScopedProfile &) = delete;
161 ScopedProfile(ScopedProfile &&) = delete;
162 ScopedProfile &operator=(ScopedProfile &&) = delete;
163
164 private:
165 struct LiteralTag
166 {
167 };
168
169 ScopedProfile(const char *name, size_t name_length, LiteralTag) noexcept;
170
171 const char *m_name;
172 size_t m_name_length;
173 int64_t m_start_ticks;
174 uint32_t m_thread_id;
175 };
176
177 } // namespace DetourModKit
178
179 #endif // DETOURMODKIT_PROFILER_HPP
180