morph 0.1.0
Asynchronous bridge between GUI thread and business models
Loading...
Searching...
No Matches
profiler.hpp
Go to the documentation of this file.
1// SPDX-License-Identifier: Apache-2.0
2
3#pragma once
4
5/// @file
6/// @brief Compile-time profiler instrumentation: Tracy zones, plots, messages
7/// and thread names, behind morph's own macro names.
8///
9/// Configure with `-DMORPH_ENABLE_TRACY=ON` and the macros below forward to
10/// Tracy (`MORPH_TRACY_ENABLED` is then defined on every target that links
11/// `morph::morph`). Otherwise each macro is a stub that names its arguments
12/// only inside `decltype`, an unevaluated operand: a disabled build evaluates
13/// nothing, warns about no unused argument, and needs no Tracy headers.
14///
15/// @par Why morph's names and not Tracy's
16/// Lightweight defines stubs under Tracy's own names (`ZoneScoped`,
17/// `ZoneScopedN`, `TracyPlot`, ...) when its own Tracy option is off, and an
18/// application may include Lightweight and morph in one translation unit. Were
19/// morph to do the same, a build with one library's Tracy on and the other's
20/// off would have one header's stubs redefine the other's real macros. Every
21/// macro here is spelled `MORPH_*` and none of Tracy's names is defined.
22///
23/// @par One zone, one phase, one thread
24/// A Tracy zone must begin and end on the same thread. A dispatch crosses
25/// threads -- the caller, the model's strand, the callback executor -- so each
26/// phase is its own zone on the thread that runs it, and the phases of one
27/// call are linked by writing `session::Context::requestId` as zone text
28/// (`MORPH_ZONE_TEXT`). No zone spans a hand-off, and none stays open across a
29/// `co_await`: a suspended coroutine's thread goes on to run other work, whose
30/// zones would then close out of order with the open one.
31///
32/// @par Every translation unit must agree
33/// morph is header-only, so these macros change the bodies of inline
34/// functions. A program in which one translation unit sees
35/// `MORPH_TRACY_ENABLED` and another does not holds two different definitions
36/// of the same inline function, which is an ODR violation: the linker keeps
37/// one of them, silently. CMake consumers get agreement by construction,
38/// because the definition is an `INTERFACE` property of `morph::morph`. A
39/// build that does not use morph's CMake package must define
40/// `MORPH_TRACY_ENABLED` for all of its translation units or for none, and
41/// link exactly one `TracyClient`. MSVC and clang-cl turn a mismatch into a
42/// link error (`detect_mismatch` below); other toolchains do not detect it.
43
44// NOLINTBEGIN(cppcoreguidelines-macro-usage) — the macros are the API: a zone
45// is a scoped local the call site's scope has to own, and a disabled argument
46// must not be evaluated, neither of which a function can do.
47
48#ifdef MORPH_TRACY_ENABLED
49
50#include <string_view>
51#include <tracy/Tracy.hpp>
52
53namespace morph::profiler::detail {
54
55/// Attaches @p text to @p zone, skipping an empty one so a zone without a
56/// request id carries no blank annotation.
57inline void zoneText(::tracy::ScopedZone& zone, std::string_view text) {
58 if (!text.empty()) {
59 zone.Text(text.data(), text.size());
60 }
61}
62
63/// Sends @p text to the timeline as a message.
64inline void message(std::string_view text) { TracyMessage(text.data(), text.size()); }
65
66} // namespace morph::profiler::detail
67
68// Every zone is a local named `morphProfilerZone`, so MORPH_ZONE_TEXT finds
69// the innermost one; a nested zone therefore shadows the outer one on
70// purpose, and the warning is silenced around the declaration. Not Tracy's
71// own `___tracy_scoped_zone`: spelled in this header, a name with a double
72// underscore is a reserved identifier at every call site.
73// Not Tracy's ZoneScopedN: whether that expansion ends in a ';' depends on
74// the compiler, and the one the call site writes would then be an empty
75// statement. The trailing static_assert takes the call site's ';' instead.
76#if defined(__clang__)
77#define MORPH_ZONE(name) \
78 _Pragma("clang diagnostic push") _Pragma("clang diagnostic ignored \"-Wshadow\"") \
79 ZoneNamedN(morphProfilerZone, name, true); \
80 _Pragma("clang diagnostic pop") static_assert(true)
81#elif defined(__GNUC__)
82#define MORPH_ZONE(name) \
83 _Pragma("GCC diagnostic push") _Pragma("GCC diagnostic ignored \"-Wshadow\"") \
84 ZoneNamedN(morphProfilerZone, name, true); \
85 _Pragma("GCC diagnostic pop") static_assert(true)
86#elif defined(_MSC_VER)
87#define MORPH_ZONE(name) \
88 _Pragma("warning(push)") _Pragma("warning(disable : 4456)") ZoneNamedN(morphProfilerZone, name, true); \
89 _Pragma("warning(pop)") static_assert(true)
90#else
91#define MORPH_ZONE(name) ZoneNamedN(morphProfilerZone, name, true)
92#endif
93#define MORPH_ZONE_TEXT(text) ::morph::profiler::detail::zoneText(morphProfilerZone, (text))
94#define MORPH_PLOT(name, value) TracyPlot(name, static_cast<double>(value))
95#define MORPH_THREAD_NAME(name) ::tracy::SetThreadName(name)
96#define MORPH_MESSAGE(text) ::morph::profiler::detail::message((text))
97
98#ifdef _MSC_VER
99#pragma detect_mismatch("morph_tracy_enabled", "1")
100#endif
101
102#else
103
104namespace morph::profiler::detail {
105
106/// What a disabled macro builds from its arguments' types: naming an argument
107/// in `decltype` uses it without evaluating it, and an empty object costs
108/// nothing to discard.
109template <typename... Args>
110struct Unevaluated {};
111
112} // namespace morph::profiler::detail
113
114/// @brief Opens a profiler zone named @p name that ends with the enclosing
115/// scope. At most one per scope.
116/// @param name A string literal: Tracy keeps a pointer to it for the life of
117/// the process.
118#define MORPH_ZONE(name) static_cast<void>(::morph::profiler::detail::Unevaluated<decltype(name)>{})
119
120/// @brief Attaches @p text to the enclosing scope's `MORPH_ZONE`; an empty
121/// text attaches nothing.
122///
123/// Only valid after a `MORPH_ZONE` in the same or an enclosing scope.
124/// @param text Anything convertible to `std::string_view`; Tracy copies it.
125/// Not evaluated in a build without Tracy.
126#define MORPH_ZONE_TEXT(text) static_cast<void>(::morph::profiler::detail::Unevaluated<decltype(text)>{})
127
128/// @brief Records @p value on the plot named @p name.
129/// @param name A string literal, the plot's identity in the capture.
130/// @param value An arithmetic value, recorded as a `double`. Not evaluated in
131/// a build without Tracy.
132#define MORPH_PLOT(name, value) \
133 static_cast<void>(::morph::profiler::detail::Unevaluated<decltype(name), decltype(value)>{})
134
135/// @brief Names the calling thread in the capture.
136/// @param name A NUL-terminated string; Tracy copies it.
137#define MORPH_THREAD_NAME(name) static_cast<void>(::morph::profiler::detail::Unevaluated<decltype(name)>{})
138
139/// @brief Sends @p text to the capture's timeline as a message.
140/// @param text Anything convertible to `std::string_view`; Tracy copies it.
141/// Not evaluated in a build without Tracy.
142#define MORPH_MESSAGE(text) static_cast<void>(::morph::profiler::detail::Unevaluated<decltype(text)>{})
143
144#ifdef _MSC_VER
145#pragma detect_mismatch("morph_tracy_enabled", "0")
146#endif
147
148#endif
149
150// NOLINTEND(cppcoreguidelines-macro-usage)
Node text(TextOptions const &options)
A text node.
Definition author.hpp:3126