GCC Code Coverage Report


Directory: ./
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 94.0% 109 / 0 / 116
Functions: 94.1% 16 / 0 / 17
Branches: 61.2% 60 / 0 / 98

src/profiler.cpp
Line Branch Exec Source
1 /**
2 * @file profiler.cpp
3 * @brief Implementation of the lock-free ring buffer profiler with Chrome Tracing export.
4 */
5
6 #include "DetourModKit/profiler.hpp"
7
8 #include <windows.h>
9 #include <cstdio>
10 #include <cstring>
11 #include <format>
12 #include <memory>
13 #include <new>
14 #include <string>
15 #include <string_view>
16
17 namespace DetourModKit
18 {
19 namespace
20 {
21 /**
22 * @brief Escapes @p input for use in a JSON value, without the outer quotes.
23 * @details Forward slash is left alone. RFC 8259 permits that form.
24 */
25 59384 std::string escape_json_string(std::string_view input)
26 {
27 59384 std::string out;
28
1/2
✓ Branch 4 → 5 taken 59384 times.
✗ Branch 4 → 33 not taken.
59384 out.reserve(input.size());
29
2/2
✓ Branch 29 → 7 taken 994308 times.
✓ Branch 29 → 30 taken 59384 times.
1053692 for (const char c : input)
30 {
31
8/8
✓ Branch 7 → 8 taken 1 time.
✓ Branch 7 → 10 taken 1 time.
✓ Branch 7 → 12 taken 1 time.
✓ Branch 7 → 14 taken 1 time.
✓ Branch 7 → 16 taken 1 time.
✓ Branch 7 → 18 taken 1 time.
✓ Branch 7 → 20 taken 1 time.
✓ Branch 7 → 22 taken 994301 times.
994308 switch (c)
32 {
33 1 case '"':
34
1/2
✓ Branch 8 → 9 taken 1 time.
✗ Branch 8 → 33 not taken.
1 out += "\\\"";
35 1 break;
36 1 case '\\':
37
1/2
✓ Branch 10 → 11 taken 1 time.
✗ Branch 10 → 33 not taken.
1 out += "\\\\";
38 1 break;
39 1 case '\b':
40
1/2
✓ Branch 12 → 13 taken 1 time.
✗ Branch 12 → 33 not taken.
1 out += "\\b";
41 1 break;
42 1 case '\f':
43
1/2
✓ Branch 14 → 15 taken 1 time.
✗ Branch 14 → 33 not taken.
1 out += "\\f";
44 1 break;
45 1 case '\n':
46
1/2
✓ Branch 16 → 17 taken 1 time.
✗ Branch 16 → 33 not taken.
1 out += "\\n";
47 1 break;
48 1 case '\r':
49
1/2
✓ Branch 18 → 19 taken 1 time.
✗ Branch 18 → 33 not taken.
1 out += "\\r";
50 1 break;
51 1 case '\t':
52
1/2
✓ Branch 20 → 21 taken 1 time.
✗ Branch 20 → 33 not taken.
1 out += "\\t";
53 1 break;
54 994301 default:
55
2/2
✓ Branch 22 → 23 taken 1 time.
✓ Branch 22 → 26 taken 994300 times.
994301 if (static_cast<unsigned char>(c) < 0x20)
56 {
57 char buf[8];
58 1 std::snprintf(
59 buf,
60 sizeof(buf),
61 "\\u%04x",
62
1/2
✓ Branch 23 → 24 taken 1 time.
✗ Branch 23 → 32 not taken.
1 static_cast<unsigned int>(static_cast<unsigned char>(c))
63 );
64
1/2
✓ Branch 24 → 25 taken 1 time.
✗ Branch 24 → 32 not taken.
1 out += buf;
65 }
66 else
67 {
68
1/2
✓ Branch 26 → 27 taken 994300 times.
✗ Branch 26 → 33 not taken.
994300 out += c;
69 }
70 994301 break;
71 }
72 }
73 59384 return out;
74 }
75 } // namespace
76
77 41 Profiler::Profiler() noexcept : m_ring(DEFAULT_CAPACITY)
78 {
79 LARGE_INTEGER freq;
80 // QueryPerformanceFrequency cannot fail and is always non-zero on Windows XP and later, but guard regardless: a
81 // zero frequency would make every conversion degenerate. Fall back to a 10 MHz tick so durations stay finite if
82 // the API ever misbehaves.
83
3/6
✓ Branch 4 → 5 taken 41 times.
✗ Branch 4 → 7 not taken.
✓ Branch 5 → 6 taken 41 times.
✗ Branch 5 → 7 not taken.
✓ Branch 8 → 9 taken 41 times.
✗ Branch 8 → 10 not taken.
41 if (QueryPerformanceFrequency(&freq) && freq.QuadPart > 0)
84 {
85 41 m_qpc_frequency = freq.QuadPart;
86 }
87 else
88 {
89 m_qpc_frequency = 10'000'000;
90 }
91 41 }
92
93 1833 Profiler &Profiler::get_instance() noexcept
94 {
95 // Constructed once into function-local static storage and never destroyed, mirroring StringPool::instance().
96 // A Meyers singleton (`static Profiler instance;`) registers a static destructor that frees the ring at
97 // static-teardown time. A ScopedProfile whose own destructor runs after that (a static or thread_local
98 // ScopedProfile, or one on a thread still alive at process teardown) then records through this accessor
99 // into freed storage. Placement-new into raw static storage keeps the object alive for the whole process; the
100 // single fixed-size ring is reclaimed by the OS at exit.
101 //
102 // Construction cannot throw: the ring allocates with nothrow new and reports capacity 0 if that fails, so the
103 // worst first-use outcome is a disabled profiler rather than a terminate out of this noexcept accessor.
104 alignas(Profiler) static unsigned char storage[sizeof(Profiler)];
105
5/6
✓ Branch 2 → 3 taken 44 times.
✓ Branch 2 → 10 taken 1789 times.
✓ Branch 4 → 5 taken 41 times.
✓ Branch 4 → 10 taken 3 times.
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 41 times.
1833 static Profiler *const instance = ::new (static_cast<void *>(storage)) Profiler();
106 1840 return *instance;
107 }
108
109 389049 void Profiler::record(const char *name, int64_t start_ticks, int64_t end_ticks, uint32_t thread_id) noexcept
110 {
111
1/2
✓ Branch 2 → 3 taken 392702 times.
✗ Branch 2 → 4 not taken.
389049 record(name, name != nullptr ? std::strlen(name) : 0, start_ticks, end_ticks, thread_id);
112 392189 }
113
114 387428 void Profiler::record(
115 const char *name,
116 size_t name_length,
117 int64_t start_ticks,
118 int64_t end_ticks,
119 uint32_t thread_id
120 ) noexcept
121 {
122 387428 const auto claim = m_ring.claim();
123
2/2
✓ Branch 3 → 4 taken 5 times.
✓ Branch 3 → 5 taken 393281 times.
393286 if (!claim.owned)
124 {
125 5 return;
126 }
127 393281 const size_t bounded_length = name_length < UINT32_MAX ? name_length : UINT32_MAX;
128 393281 m_ring.publish(
129 claim,
130 name,
131 static_cast<uint32_t>(bounded_length),
132 start_ticks,
133 detail::ticks_to_microseconds(start_ticks, end_ticks, m_qpc_frequency),
134 thread_id
135 );
136 }
137
138 // Caller must ensure no concurrent record() calls are in flight. There is no runtime guard because an atomic
139 // "recording active" counter would penalize every record() call on the hot path for a contract that only matters at
140 // session boundaries.
141 71 void Profiler::reset() noexcept
142 {
143 71 m_ring.reset();
144 71 }
145
146 30 std::string Profiler::export_chrome_json() const
147 {
148 30 std::string json;
149 30 const auto resident = static_cast<size_t>(m_ring.resident());
150
2/2
✓ Branch 4 → 5 taken 6 times.
✓ Branch 4 → 10 taken 24 times.
30 if (resident == 0)
151 {
152
1/2
✓ Branch 7 → 8 taken 6 times.
✗ Branch 7 → 24 not taken.
12 return "[]";
153 }
154
155 // ~120 bytes per JSON event is a reasonable estimate.
156
1/2
✓ Branch 10 → 11 taken 24 times.
✗ Branch 10 → 31 not taken.
24 json.reserve(resident * 120 + 4);
157
1/2
✓ Branch 11 → 12 taken 24 times.
✗ Branch 11 → 31 not taken.
24 json += "[\n";
158
159 24 const double ticks_to_us = 1'000'000.0 / static_cast<double>(m_qpc_frequency);
160 24 bool first = true;
161 24 m_ring.visit_committed(
162
1/2
✓ Branch 12 → 13 taken 24 times.
✗ Branch 12 → 27 not taken.
24 [&](const char *name, uint32_t name_length, int64_t start_ticks, uint32_t duration_us, uint32_t thread_id)
163 {
164
2/2
✓ Branch 2 → 3 taken 59360 times.
✓ Branch 2 → 4 taken 24 times.
59384 if (!first)
165 {
166
1/2
✓ Branch 3 → 4 taken 59360 times.
✗ Branch 3 → 19 not taken.
59360 json += ",\n";
167 }
168 59384 first = false;
169
170 // Chrome Trace Event Format: "X" = complete event (has duration). The name is escaped so a caller's
171 // quotes or backslashes still produce valid JSON. The published extent bounds the read, so a source
172 // array with no terminator cannot cause an over-read.
173 59384 const double ts = static_cast<double>(start_ticks) * ticks_to_us;
174
1/2
✓ Branch 6 → 7 taken 59384 times.
✗ Branch 6 → 13 not taken.
118768 json += std::format(
175 R"({{"name":"{}","ph":"X","ts":{:.1f},"dur":{},"pid":1,"tid":{}}})",
176
1/2
✓ Branch 5 → 6 taken 59384 times.
✗ Branch 5 → 16 not taken.
118768 escape_json_string(std::string_view{name, name_length}),
177 ts,
178 duration_us,
179 thread_id
180
1/2
✓ Branch 7 → 8 taken 59384 times.
✗ Branch 7 → 11 not taken.
59384 );
181 59384 }
182 );
183
184
1/2
✗ Branch 13 → 14 not taken.
✓ Branch 13 → 19 taken 24 times.
24 if (first)
185 {
186 return "[]";
187 }
188
1/2
✓ Branch 19 → 20 taken 24 times.
✗ Branch 19 → 31 not taken.
24 json += "\n]";
189 24 return json;
190 30 }
191
192 5 bool Profiler::export_to_file(std::string_view path) const
193 {
194
1/2
✓ Branch 2 → 3 taken 5 times.
✗ Branch 2 → 43 not taken.
5 const std::string json = export_chrome_json();
195
1/2
✓ Branch 5 → 6 taken 5 times.
✗ Branch 5 → 34 not taken.
5 const std::string path_str(path);
196
197 const auto closer = [](std::FILE *f) { std::fclose(f); };
198 5 std::FILE *file_ptr = nullptr;
199
200
1/2
✓ Branch 8 → 9 taken 5 times.
✗ Branch 8 → 39 not taken.
5 const errno_t err = fopen_s(&file_ptr, path_str.c_str(), "wb");
201
3/4
✓ Branch 9 → 10 taken 4 times.
✓ Branch 9 → 11 taken 1 time.
✗ Branch 10 → 11 not taken.
✓ Branch 10 → 12 taken 4 times.
5 if (err != 0 || file_ptr == nullptr)
202 {
203 1 return false;
204 }
205
206 4 std::unique_ptr<std::FILE, decltype(closer)> fp(file_ptr, closer);
207
1/2
✓ Branch 16 → 17 taken 4 times.
✗ Branch 16 → 37 not taken.
4 const size_t written = std::fwrite(json.data(), 1, json.size(), fp.get());
208
1/2
✗ Branch 18 → 19 not taken.
✓ Branch 18 → 20 taken 4 times.
4 if (written != json.size())
209 {
210 return false;
211 }
212
2/4
✓ Branch 21 → 22 taken 4 times.
✗ Branch 21 → 37 not taken.
✗ Branch 22 → 23 not taken.
✓ Branch 22 → 24 taken 4 times.
4 if (std::fflush(fp.get()) != 0)
213 {
214 return false;
215 }
216 // Release the pointer so unique_ptr does not double-close.
217
2/4
✓ Branch 25 → 26 taken 4 times.
✗ Branch 25 → 37 not taken.
✗ Branch 26 → 27 not taken.
✓ Branch 26 → 28 taken 4 times.
4 if (std::fclose(fp.release()) != 0)
218 {
219 return false;
220 }
221 4 return true;
222 5 }
223
224 38 size_t Profiler::total_samples_recorded() const noexcept
225 {
226 38 return static_cast<size_t>(m_ring.claims());
227 }
228
229 13 size_t Profiler::available_samples() const noexcept
230 {
231 13 return static_cast<size_t>(m_ring.resident());
232 }
233
234 6 size_t Profiler::dropped_samples() const noexcept
235 {
236 6 return static_cast<size_t>(m_ring.dropped());
237 }
238
239 12 size_t Profiler::capacity() const noexcept
240 {
241 12 return m_ring.capacity();
242 }
243
244 5 int64_t Profiler::qpc_frequency() const noexcept
245 {
246 5 return m_qpc_frequency;
247 }
248
249 1629 ScopedProfile::ScopedProfile(const char *name, size_t name_length, LiteralTag) noexcept
250 1629 : m_name(name), m_name_length(name_length), m_thread_id(GetCurrentThreadId())
251 {
252 LARGE_INTEGER ticks;
253 1617 QueryPerformanceCounter(&ticks);
254 1698 m_start_ticks = ticks.QuadPart;
255 1698 }
256
257 1661 ScopedProfile::~ScopedProfile() noexcept
258 {
259 LARGE_INTEGER ticks;
260 1661 QueryPerformanceCounter(&ticks);
261 1725 Profiler::get_instance().record(m_name, m_name_length, m_start_ticks, ticks.QuadPart, m_thread_id);
262 1653 }
263
264 } // namespace DetourModKit
265