util/profiler.h
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | // SPDX-License-Identifier: BSD-3-Clause | ||
| 2 | /// @file profiler.h | ||
| 3 | /// @brief Ernie's in-process CPU and GPU zone profiler, its zone macros, and averaged reports. | ||
| 4 | /// | ||
| 5 | /// Records `ERNIE_ZONE` frame by frame for the Profiler panel, and with -DERNIE_TRACY=ON for | ||
| 6 | /// Tracy too; `ERNIE_GPU_ZONE` times commands with timestamp queries the device context reads | ||
| 7 | /// back into `AddGpuZone`, and reaches Tracy too. | ||
| 8 | /// Off until `SetEnabled`; a zone then costs one atomic load. CPU zones belong to the frame they | ||
| 9 | /// end in; GPU zones to the frame that recorded their commands. Reports average frames into zone | ||
| 10 | /// trees, modelled on Omniverse Kit's profiler window, and export text, JSON and Chrome traces. GPU | ||
| 11 | /// zones are in device/profile.h. | ||
| 12 | /// | ||
| 13 | /// // The rest of the scope, one per scope; a string literal. | ||
| 14 | /// ERNIE_ZONE("Sync"); | ||
| 15 | /// // Text attached to the scope's zone (Tracy only). | ||
| 16 | /// ERNIE_ZONE_TEXT(path.String()); | ||
| 17 | /// // A value plotted over time (Tracy only). | ||
| 18 | /// ERNIE_PLOT("views", 256); | ||
| 19 | /// // The end of a frame of a named frame set (Tracy only). | ||
| 20 | /// ERNIE_FRAME_MARK("Step"); | ||
| 21 | /// | ||
| 22 | /// References: | ||
| 23 | /// - B. Taudul, "Tracy Profiler". https://github.com/wolfpld/tracy | ||
| 24 | /// - NVIDIA, "Profiling", Omniverse Kit SDK manual (the Profiler window: GPU and CPU zone trees, | ||
| 25 | /// depths, averaging, exports). | ||
| 26 | /// https://docs.omniverse.nvidia.com/kit/docs/kit-manual/latest/guide/profiling.html | ||
| 27 | /// - Google, "Trace Event Format" (complete events, "ph": "X", and thread-name metadata), read by | ||
| 28 | /// Perfetto and chrome://tracing. | ||
| 29 | /// https://docs.google.com/document/d/1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU | ||
| 30 | #pragma once | ||
| 31 | |||
| 32 | #include "ernie/export.h" | ||
| 33 | |||
| 34 | #include <cstdint> | ||
| 35 | #include <span> | ||
| 36 | #include <string> | ||
| 37 | #include <string_view> | ||
| 38 | #include <vector> | ||
| 39 | |||
| 40 | namespace ernie::profiler { | ||
| 41 | |||
| 42 | /// @brief A timed span on a CPU thread or the GPU. | ||
| 43 | struct Zone { | ||
| 44 | /// A string literal; zones keep the pointer. | ||
| 45 | const char* name = ""; | ||
| 46 | /// Start, in nanoseconds. | ||
| 47 | std::uint64_t start = 0; | ||
| 48 | /// End, in nanoseconds. | ||
| 49 | std::uint64_t end = 0; | ||
| 50 | /// The CPU thread's index; unused for GPU zones. | ||
| 51 | std::uint32_t thread = 0; | ||
| 52 | /// How many zones of the same thread or recording enclose it. | ||
| 53 | std::uint32_t depth = 0; | ||
| 54 | }; | ||
| 55 | |||
| 56 | /// @brief One frame's zones. | ||
| 57 | /// CPU times use the steady clock (`Now`). GPU times are converted to it: calibrated where the | ||
| 58 | /// device reads its clock with the host's, else aligned to when the first GPU zone was recorded. | ||
| 59 | 41 | struct Frame { | |
| 60 | /// The frame's index. | ||
| 61 | std::uint64_t index = 0; | ||
| 62 | /// Start on the steady clock, in nanoseconds. | ||
| 63 | std::uint64_t start = 0; | ||
| 64 | /// End on the steady clock, in nanoseconds. | ||
| 65 | std::uint64_t end = 0; | ||
| 66 | /// CPU zones. | ||
| 67 | std::vector<Zone> cpu; | ||
| 68 | /// GPU zones. | ||
| 69 | std::vector<Zone> gpu; | ||
| 70 | }; | ||
| 71 | |||
| 72 | /// Newest frames whose GPU zones may still be arriving. | ||
| 73 | inline constexpr std::size_t kFramesInFlight = 2; | ||
| 74 | |||
| 75 | /// @brief Starts or stops recording both CPU and GPU zones. | ||
| 76 | /// Stopping keeps the frames recorded so far. | ||
| 77 | /// @param enabled Whether to record. | ||
| 78 | ERNIE_API void SetEnabled(bool enabled); | ||
| 79 | /// @return Whether CPU or GPU zones are recording. | ||
| 80 | ERNIE_API bool Enabled(); | ||
| 81 | |||
| 82 | /// @brief Starts or stops recording CPU zones (`ERNIE_ZONE`). | ||
| 83 | /// @param enabled Whether to record. | ||
| 84 | ERNIE_API void SetCpuEnabled(bool enabled); | ||
| 85 | /// @return Whether CPU zones are recording. | ||
| 86 | ERNIE_API bool CpuEnabled(); | ||
| 87 | /// @brief Starts or stops recording GPU zones (`ERNIE_GPU_ZONE`). | ||
| 88 | /// @param enabled Whether to record. | ||
| 89 | ERNIE_API void SetGpuEnabled(bool enabled); | ||
| 90 | /// @return Whether GPU zones are recording. | ||
| 91 | ERNIE_API bool GpuEnabled(); | ||
| 92 | |||
| 93 | /// @return Nanoseconds on the steady clock. | ||
| 94 | ERNIE_API std::uint64_t Now(); | ||
| 95 | |||
| 96 | /// @brief Ends the current frame and starts the next. | ||
| 97 | /// The ended frame is kept even when not recording, so frame times | ||
| 98 | /// show before any zones. | ||
| 99 | ERNIE_API void BeginFrame(); | ||
| 100 | |||
| 101 | /// @return The index of the frame being recorded. | ||
| 102 | ERNIE_API std::uint64_t CurrentFrame(); | ||
| 103 | |||
| 104 | /// @return The kept finished frames, oldest first, at most | ||
| 105 | /// `History`. | ||
| 106 | ERNIE_API std::vector<Frame> Frames(); | ||
| 107 | /// @brief Sets how many finished frames are kept. | ||
| 108 | /// @param frames The frame count. | ||
| 109 | ERNIE_API void SetHistory(std::size_t frames); | ||
| 110 | /// @return How many finished frames are kept. | ||
| 111 | ERNIE_API std::size_t History(); | ||
| 112 | /// @brief Drops all kept frames. | ||
| 113 | ERNIE_API void Clear(); | ||
| 114 | |||
| 115 | /// @return The calling thread's index, as in `Zone::thread`. | ||
| 116 | ERNIE_API std::uint32_t ThreadIndex(); | ||
| 117 | /// @brief Names the calling thread. | ||
| 118 | /// @param name The name; unnamed threads are "Thread <index>". | ||
| 119 | ERNIE_API void SetThreadName(std::string name); | ||
| 120 | /// @return Thread names by index. | ||
| 121 | ERNIE_API std::vector<std::string> ThreadNames(); | ||
| 122 | |||
| 123 | /// @brief Copies a run-time name to process-lifetime storage, since | ||
| 124 | /// zones keep their name's pointer. | ||
| 125 | /// @param name The name. | ||
| 126 | /// @return The copy; the same pointer for the same name. | ||
| 127 | ERNIE_API const char* Intern(std::string_view name); | ||
| 128 | |||
| 129 | /// @brief Adds a GPU zone to the frame that recorded it. | ||
| 130 | /// @param frame The frame index; dropped if no longer kept. | ||
| 131 | /// @param zone The zone. | ||
| 132 | ERNIE_API void AddGpuZone(std::uint64_t frame, const Zone& zone); | ||
| 133 | |||
| 134 | /// @brief Times the rest of the enclosing scope as a CPU zone. | ||
| 135 | class ERNIE_API ScopedZone { | ||
| 136 | public: | ||
| 137 | /// @brief Starts the zone if recording. | ||
| 138 | /// @param name A string literal naming the zone. | ||
| 139 | explicit ScopedZone(const char* name); | ||
| 140 | ~ScopedZone(); | ||
| 141 | ScopedZone(const ScopedZone&) = delete; | ||
| 142 | ScopedZone& operator=(const ScopedZone&) = delete; | ||
| 143 | |||
| 144 | private: | ||
| 145 | const char* name_; | ||
| 146 | std::uint64_t start_ = 0; | ||
| 147 | bool active_ = false; | ||
| 148 | }; | ||
| 149 | |||
| 150 | // ---------------------------------------------------------------------------------------------- | ||
| 151 | // Reports | ||
| 152 | |||
| 153 | /// @brief A zone averaged over frames. | ||
| 154 | /// Zones of the same name under the same parent merge. | ||
| 155 | 56 | struct ReportNode { | |
| 156 | /// The zone's name. | ||
| 157 | std::string name; | ||
| 158 | /// Mean time per frame, in milliseconds. | ||
| 159 | double ms = 0.0; | ||
| 160 | /// Mean runs per frame. | ||
| 161 | double calls = 0.0; | ||
| 162 | /// Share of its section's total, in percent. | ||
| 163 | double percent = 0.0; | ||
| 164 | /// Nested zones, in the order they first ran. | ||
| 165 | std::vector<ReportNode> children; | ||
| 166 | }; | ||
| 167 | |||
| 168 | /// @brief GPU and CPU zone trees averaged over frames. | ||
| 169 | 12 | struct Report { | |
| 170 | /// The GPU's zones. | ||
| 171 | std::vector<ReportNode> gpu; | ||
| 172 | /// The CPU's zones, under a node per thread. | ||
| 173 | std::vector<ReportNode> cpu; | ||
| 174 | /// Mean GPU time of the outermost zones, in milliseconds. | ||
| 175 | double gpuMs = 0.0; | ||
| 176 | /// Mean host frame time, in milliseconds. | ||
| 177 | double cpuMs = 0.0; | ||
| 178 | /// Frames whose GPU zones were averaged. | ||
| 179 | std::size_t gpuFrames = 0; | ||
| 180 | /// Frames averaged. | ||
| 181 | std::size_t cpuFrames = 0; | ||
| 182 | }; | ||
| 183 | |||
| 184 | /// @brief Averages frames' zones into a report. | ||
| 185 | /// @param frames The frames; GPU zones count only once arrived. | ||
| 186 | /// @param threadNames CPU thread names by index (`ThreadNames`). | ||
| 187 | /// @return The report. | ||
| 188 | ERNIE_API Report Summarize(std::span<const Frame> frames, | ||
| 189 | const std::vector<std::string>& threadNames); | ||
| 190 | |||
| 191 | /// @brief Which zones of a report to list. | ||
| 192 | struct ReportFilter { | ||
| 193 | /// Deepest nesting shown; 0 lists the outermost alone. | ||
| 194 | std::uint32_t depth = 8; | ||
| 195 | /// Shortest zone shown, in milliseconds. | ||
| 196 | double minMs = 0.0; | ||
| 197 | /// Sort siblings longest first instead of by run order. | ||
| 198 | bool byTime = false; | ||
| 199 | }; | ||
| 200 | |||
| 201 | /// @brief A listed zone and how deep it is shown. | ||
| 202 | struct ReportRow { | ||
| 203 | /// The zone. | ||
| 204 | const ReportNode* node = nullptr; | ||
| 205 | /// Its nesting depth. | ||
| 206 | std::uint32_t depth = 0; | ||
| 207 | /// Whether it has listed children. | ||
| 208 | bool parent = false; | ||
| 209 | }; | ||
| 210 | |||
| 211 | /// @brief Flattens a zone tree into listed rows. | ||
| 212 | /// @param nodes The tree. | ||
| 213 | /// @param filter Which zones to list. | ||
| 214 | /// @return The rows, parents before their children. | ||
| 215 | ERNIE_API std::vector<ReportRow> Rows(const std::vector<ReportNode>& nodes, | ||
| 216 | const ReportFilter& filter); | ||
| 217 | |||
| 218 | /// @brief Formats a report as indented names, times and shares. | ||
| 219 | /// @param report The report. | ||
| 220 | /// @param filter Which zones to list. | ||
| 221 | /// @return The text. | ||
| 222 | ERNIE_API std::string ReportText(const Report& report, const ReportFilter& filter); | ||
| 223 | |||
| 224 | /// @brief Formats a report as JSON. | ||
| 225 | /// The shape is {"gpu": {"ms", "zones": [...]}, "cpu": ...}, each | ||
| 226 | /// zone {"name", "ms", "calls", "percent", "children"}. | ||
| 227 | /// @param report The report. | ||
| 228 | /// @return The JSON text. | ||
| 229 | ERNIE_API std::string ReportJson(const Report& report); | ||
| 230 | |||
| 231 | /// @brief Exports frames in the Trace Event Format. | ||
| 232 | /// CPU threads are threads of process 1, GPU zones process 2; times | ||
| 233 | /// are microseconds from the first frame. GPU zones are placed from | ||
| 234 | /// their frame's start since the GPU clock differs from the host's. | ||
| 235 | /// @param frames The frames. | ||
| 236 | /// @param threadNames CPU thread names by index. | ||
| 237 | /// @return The trace JSON. | ||
| 238 | ERNIE_API std::string TraceJson(std::span<const Frame> frames, | ||
| 239 | const std::vector<std::string>& threadNames); | ||
| 240 | |||
| 241 | } // namespace ernie::profiler | ||
| 242 | |||
| 243 | #define ERNIE_PROFILE_CONCAT_(a, b) a##b | ||
| 244 | #define ERNIE_PROFILE_CONCAT(a, b) ERNIE_PROFILE_CONCAT_(a, b) | ||
| 245 | #define ERNIE_OWN_ZONE(name) \ | ||
| 246 | const ::ernie::profiler::ScopedZone ERNIE_PROFILE_CONCAT(ernieZone, __LINE__)(name) | ||
| 247 | |||
| 248 | #if ERNIE_TRACY | ||
| 249 | #include <tracy/Tracy.hpp> | ||
| 250 | |||
| 251 | #define ERNIE_ZONE(name) \ | ||
| 252 | ZoneScopedN(name); \ | ||
| 253 | ERNIE_OWN_ZONE(name) | ||
| 254 | #define ERNIE_ZONE_TEXT(text) \ | ||
| 255 | do { \ | ||
| 256 | const auto& ernieZoneText = (text); \ | ||
| 257 | ZoneText(ernieZoneText.data(), ernieZoneText.size()); \ | ||
| 258 | } while (false) | ||
| 259 | #define ERNIE_PLOT(name, value) TracyPlot(name, static_cast<double>(value)) | ||
| 260 | #define ERNIE_FRAME_MARK(name) FrameMarkNamed(name) | ||
| 261 | #else | ||
| 262 | #define ERNIE_ZONE(name) ERNIE_OWN_ZONE(name) | ||
| 263 | #define ERNIE_ZONE_TEXT(text) | ||
| 264 | #define ERNIE_PLOT(name, value) | ||
| 265 | #define ERNIE_FRAME_MARK(name) | ||
| 266 | #endif | ||
| 267 |