3
0

ImGuiCpuProfiler.cpp 52 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230
  1. /*
  2. * Copyright (c) Contributors to the Open 3D Engine Project.
  3. * For complete copyright and license terms please see the LICENSE at the root of this distribution.
  4. *
  5. * SPDX-License-Identifier: Apache-2.0 OR MIT
  6. *
  7. */
  8. #if defined(IMGUI_ENABLED)
  9. #include <ImGuiCpuProfiler.h>
  10. #include <CpuProfiler.h>
  11. #include <AzCore/Debug/ProfilerBus.h>
  12. #include <AzCore/IO/FileIO.h>
  13. #include <AzCore/JSON/filereadstream.h>
  14. #include <AzCore/Math/MathUtils.h>
  15. #include <AzCore/Outcome/Outcome.h>
  16. #include <AzCore/Serialization/Json/JsonSerialization.h>
  17. #include <AzCore/Serialization/Json/JsonSerializationResult.h>
  18. #include <AzCore/Statistics/StatisticalProfilerProxy.h>
  19. #include <AzCore/std/limits.h>
  20. #include <AzCore/std/sort.h>
  21. #include <AzCore/std/string/conversions.h>
  22. #include <AzCore/std/time.h>
  23. namespace Profiler
  24. {
  25. constexpr AZStd::sys_time_t ProfilerViewEdgePadding = 5000;
  26. constexpr size_t InitialCpuTimingStatsAllocation = 8;
  27. constexpr int MinSavableFrameCount = 30; // 1 second @ 30 fps
  28. constexpr int MaxSavableFrameCount = 2000;
  29. constexpr int MaxUpdateFrequencyMs = 2000; // 2 seconds
  30. namespace CpuProfilerImGuiHelper
  31. {
  32. AZ::Outcome<CpuProfilingStatisticsSerializer, AZStd::string> LoadSavedCpuProfilingStatistics(const char* capturePath)
  33. {
  34. auto* base = AZ::IO::FileIOBase::GetInstance();
  35. char resolvedPath[AZ::IO::MaxPathLength];
  36. if (!base->ResolvePath(capturePath, resolvedPath, AZ::IO::MaxPathLength))
  37. {
  38. return AZ::Failure(AZStd::string::format("Could not resolve the path to file %s, is the path correct?", resolvedPath));
  39. }
  40. AZ::u64 captureSizeBytes;
  41. const AZ::IO::Result fileSizeResult = base->Size(resolvedPath, captureSizeBytes);
  42. if (!fileSizeResult)
  43. {
  44. return AZ::Failure(AZStd::string::format("Could not read the size of file %s, is the path correct?", resolvedPath));
  45. }
  46. // NOTE: this uses raw file pointers over the abstractions and utility functions provided by AZ::JsonSerializationUtils because
  47. // saved profiling captures can be upwards of 400 MB. This necessitates a buffered approach to avoid allocating huge chunks of memory.
  48. FILE* fp = nullptr;
  49. azfopen(&fp, resolvedPath, "rb");
  50. if (!fp)
  51. {
  52. return AZ::Failure(AZStd::string::format("Could not fopen file %s, is the path correct?\n", resolvedPath));
  53. }
  54. constexpr AZStd::size_t MaxBufSize = 65536;
  55. const AZStd::size_t bufSize = AZStd::min(MaxBufSize, aznumeric_cast<AZStd::size_t>(captureSizeBytes));
  56. char* buf = reinterpret_cast<char*>(azmalloc(bufSize));
  57. rapidjson::Document document;
  58. rapidjson::FileReadStream inputStream(fp, buf, bufSize);
  59. document.ParseStream(inputStream);
  60. azfree(buf);
  61. fclose(fp);
  62. if (document.HasParseError())
  63. {
  64. const auto pe = document.GetParseError();
  65. return AZ::Failure(AZStd::string::format(
  66. "Rapidjson could not parse the document with ParseErrorCode %u. See 3rdParty/rapidjson/error.h for definitions.\n", pe));
  67. }
  68. if (!document.IsObject() || !document.HasMember("ClassData"))
  69. {
  70. return AZ::Failure(AZStd::string::format(
  71. "Error in loading saved capture: top-level object does not have a ClassData field. Did the serialization format change recently?\n"));
  72. }
  73. AZ_TracePrintf("JsonUtils", "Successfully loaded JSON into memory.\n");
  74. const auto& root = document["ClassData"];
  75. CpuProfilingStatisticsSerializer serializer;
  76. const AZ::JsonSerializationResult::ResultCode deserializationResult = AZ::JsonSerialization::Load(serializer, root);
  77. if (deserializationResult.GetProcessing() == AZ::JsonSerializationResult::Processing::Halted
  78. || serializer.m_cpuProfilingStatisticsSerializerEntries.empty())
  79. {
  80. return AZ::Failure(AZStd::string::format("Error in deserializing document: %s\n", deserializationResult.ToString(capturePath).c_str()));
  81. }
  82. AZ_TracePrintf("JsonUtils", "Successfully loaded CPU profiling data with %zu profiling entries.\n",
  83. serializer.m_cpuProfilingStatisticsSerializerEntries.size());
  84. return AZ::Success(AZStd::move(serializer));
  85. }
  86. } // namespace CpuProfilerImGuiHelper
  87. ImGuiCpuProfiler::ImGuiCpuProfiler()
  88. {
  89. // thread IDs are hashed internally to unify display across platforms
  90. m_mainThreadId = AZStd::hash<AZStd::thread_id>{}(AZStd::this_thread::get_id());
  91. }
  92. float ImGuiCpuProfiler::TicksToMs(double ticks)
  93. {
  94. const AZStd::sys_time_t ticksPerSecond = m_ticksPerSecondFromFile > 0 ? m_ticksPerSecondFromFile : AZStd ::GetTimeTicksPerSecond();
  95. // Note: converting to microseconds integer before converting to milliseconds float
  96. AZ_Assert(ticksPerSecond >= 1000, "Error in converting ticks to ms, expected ticksPerSecond >= 1000");
  97. return static_cast<float>((ticks * 1000) / (ticksPerSecond / 1000)) / 1000.0f;
  98. }
  99. float ImGuiCpuProfiler::TicksToMs(AZStd::sys_time_t ticks)
  100. {
  101. return TicksToMs(static_cast<double>(ticks));
  102. }
  103. void ImGuiCpuProfiler::Draw(bool& keepDrawing)
  104. {
  105. // Cache the value to detect if it was changed by ImGui(user pressed 'x')
  106. const bool cachedShowCpuProfiler = keepDrawing;
  107. const ImVec2 windowSize(1280.0f, 720.0f);
  108. ImGui::SetNextWindowSize(windowSize, ImGuiCond_Once);
  109. if (ImGui::Begin("CPU Profiler", &keepDrawing, ImGuiWindowFlags_None))
  110. {
  111. // Collect the last frame's profiling data
  112. if (!m_paused)
  113. {
  114. // Update region map and cache the input cpu timing statistics when the profiling is not paused
  115. CacheCpuTimingStatistics();
  116. CollectFrameData();
  117. CullFrameData();
  118. // Only listen to system ticks when the profiler is active
  119. if (!AZ::SystemTickBus::Handler::BusIsConnected())
  120. {
  121. AZ::SystemTickBus::Handler::BusConnect();
  122. }
  123. }
  124. if (m_enableVisualizer)
  125. {
  126. DrawVisualizer();
  127. }
  128. else
  129. {
  130. DrawStatisticsView();
  131. }
  132. if (m_showFilePicker)
  133. {
  134. DrawFilePicker();
  135. }
  136. }
  137. ImGui::End();
  138. if (m_captureToFile)
  139. {
  140. AZ::Debug::ProfilerSystemInterface::Get()->CaptureFrame(GenerateOutputFile("single"));
  141. }
  142. m_captureToFile = false;
  143. // Toggle if the bool isn't the same as the cached value
  144. if (cachedShowCpuProfiler != keepDrawing)
  145. {
  146. AZ::Debug::ProfilerSystemInterface::Get()->SetActive(keepDrawing);
  147. }
  148. }
  149. void ImGuiCpuProfiler::DrawCommonHeader()
  150. {
  151. if (!m_lastCapturedFilePath.empty())
  152. {
  153. ImGui::Text("Saved: %s", m_lastCapturedFilePath.c_str());
  154. }
  155. if (ImGui::Button(m_enableVisualizer ? "Swap to statistics" : "Swap to visualizer"))
  156. {
  157. m_enableVisualizer = !m_enableVisualizer;
  158. }
  159. ImGui::SameLine();
  160. m_paused = !AZ::Debug::ProfilerSystemInterface::Get()->IsActive();
  161. if (ImGui::Button(m_paused ? "Resume" : "Pause"))
  162. {
  163. m_ticksPerSecondFromFile = 0;
  164. m_paused = !m_paused;
  165. AZ::Debug::ProfilerSystemInterface::Get()->SetActive(!m_paused);
  166. }
  167. ImGui::SameLine();
  168. if (ImGui::Button("Capture"))
  169. {
  170. m_captureToFile = true;
  171. }
  172. ImGui::SameLine();
  173. bool isInProgress = AZ::Debug::ProfilerSystemInterface::Get()->IsCaptureInProgress();
  174. if (ImGui::Button(isInProgress ? "End" : "Begin"))
  175. {
  176. auto profilerSystem = AZ::Debug::ProfilerSystemInterface::Get();
  177. if (isInProgress)
  178. {
  179. profilerSystem->EndCapture();
  180. m_paused = true;
  181. }
  182. else
  183. {
  184. profilerSystem->StartCapture(GenerateOutputFile("multi"));
  185. }
  186. }
  187. ImGui::SameLine();
  188. if (ImGui::Button("Load file"))
  189. {
  190. m_showFilePicker = true;
  191. // Only update the cached file list when opened so that we aren't making IO calls on every frame.
  192. m_cachedCapturePaths.clear();
  193. AZ::IO::FixedMaxPathString captureOutput = AZ::Debug::GetProfilerCaptureLocation();
  194. auto* base = AZ::IO::FileIOBase::GetInstance();
  195. base->FindFiles(captureOutput.c_str(), "*.json",
  196. [&paths = m_cachedCapturePaths](const char* path) -> bool
  197. {
  198. auto foundPath = AZ::IO::Path(path);
  199. paths.push_back(foundPath);
  200. return true;
  201. });
  202. // Sort by decreasing modification time (most recent at the top)
  203. AZStd::sort(m_cachedCapturePaths.begin(), m_cachedCapturePaths.end(),
  204. [&base](const AZ::IO::Path& lhs, const AZ::IO::Path& rhs)
  205. {
  206. return base->ModificationTime(lhs.c_str()) > base->ModificationTime(rhs.c_str());
  207. });
  208. }
  209. ImGui::SameLine();
  210. if (ImGui::Button("Reset All"))
  211. {
  212. if (auto statsProfiler = AZ::Interface<AZ::Statistics::StatisticalProfilerProxy>::Get(); statsProfiler)
  213. {
  214. statsProfiler->ResetAllStatistics();
  215. }
  216. ResetTable();
  217. }
  218. }
  219. void ImGuiCpuProfiler::DrawTable()
  220. {
  221. const auto flags =
  222. ImGuiTableFlags_Borders | ImGuiTableFlags_Sortable | ImGuiTableFlags_Resizable | ImGuiTableFlags_Reorderable;
  223. if (ImGui::BeginTable("FunctionStatisticsTable", 6, flags))
  224. {
  225. // Table header setup
  226. ImGui::TableSetupColumn("Group");
  227. ImGui::TableSetupColumn("Region");
  228. ImGui::TableSetupColumn("MTPC (ms)");
  229. ImGui::TableSetupColumn("Max (ms)");
  230. ImGui::TableSetupColumn("Invocations");
  231. ImGui::TableSetupColumn("Total (ms)");
  232. ImGui::TableHeadersRow();
  233. ImGui::TableNextColumn();
  234. ImGuiTableSortSpecs* sortSpecs = ImGui::TableGetSortSpecs();
  235. if (sortSpecs && sortSpecs->SpecsDirty)
  236. {
  237. SortTable(sortSpecs);
  238. }
  239. // Draw all of the rows held in the GroupRegionMap
  240. for (const auto* statistics : m_tableData)
  241. {
  242. if (!m_timedRegionFilter.PassFilter(statistics->m_groupName.c_str())
  243. && !m_timedRegionFilter.PassFilter(statistics->m_regionName.c_str()))
  244. {
  245. continue;
  246. }
  247. ImGui::Text("%s", statistics->m_groupName.c_str());
  248. const ImVec2 topLeftBound = ImGui::GetItemRectMin();
  249. ImGui::TableNextColumn();
  250. ImGui::Text("%s", statistics->m_regionName.c_str());
  251. ImGui::TableNextColumn();
  252. ImGui::Text("%.2f", TicksToMs(statistics->m_runningAverageTicks));
  253. ImGui::TableNextColumn();
  254. ImGui::Text("%.2f", TicksToMs(statistics->m_maxTicks));
  255. ImGui::TableNextColumn();
  256. ImGui::Text("%llu", statistics->m_invocationsLastFrame);
  257. ImGui::TableNextColumn();
  258. ImGui::Text("%.2f", TicksToMs(statistics->m_lastFrameTotalTicks));
  259. const ImVec2 botRightBound = ImGui::GetItemRectMax();
  260. ImGui::TableNextColumn();
  261. // NOTE: we are manually checking the bounds rather than using ImGui::IsItemHovered + Begin/EndGroup because
  262. // ImGui reports incorrect bounds when using Begin/End group in the Tables API.
  263. if (ImGui::IsWindowHovered() && ImGui::IsMouseHoveringRect(topLeftBound, botRightBound, false))
  264. {
  265. ImGui::BeginTooltip();
  266. ImGui::Text("%s", statistics->GetExecutingThreadsLabel().c_str());
  267. ImGui::EndTooltip();
  268. }
  269. }
  270. }
  271. ImGui::EndTable();
  272. }
  273. void ImGuiCpuProfiler::SortTable(ImGuiTableSortSpecs* sortSpecs)
  274. {
  275. const bool ascending = sortSpecs->Specs->SortDirection == ImGuiSortDirection_Ascending;
  276. const ImS16 columnToSort = sortSpecs->Specs->ColumnIndex;
  277. switch (columnToSort)
  278. {
  279. case (0): // Sort by group name
  280. AZStd::sort(m_tableData.begin(), m_tableData.end(), TableRow::TableRowCompareFunctor(&TableRow::m_groupName, ascending));
  281. break;
  282. case (1): // Sort by region name
  283. AZStd::sort(m_tableData.begin(), m_tableData.end(), TableRow::TableRowCompareFunctor(&TableRow::m_regionName, ascending));
  284. break;
  285. case (2): // Sort by average time
  286. AZStd::sort(m_tableData.begin(), m_tableData.end(), TableRow::TableRowCompareFunctor(&TableRow::m_runningAverageTicks, ascending));
  287. break;
  288. case (3): // Sort by max time
  289. AZStd::sort(m_tableData.begin(), m_tableData.end(), TableRow::TableRowCompareFunctor(&TableRow::m_maxTicks, ascending));
  290. break;
  291. case (4): // Sort by invocations
  292. AZStd::sort(m_tableData.begin(), m_tableData.end(), TableRow::TableRowCompareFunctor(&TableRow::m_invocationsLastFrame, ascending));
  293. break;
  294. case (5): // Sort by total time
  295. AZStd::sort(m_tableData.begin(), m_tableData.end(), TableRow::TableRowCompareFunctor(&TableRow::m_lastFrameTotalTicks, ascending));
  296. break;
  297. }
  298. sortSpecs->SpecsDirty = false;
  299. }
  300. void ImGuiCpuProfiler::ResetTable()
  301. {
  302. m_tableData.clear();
  303. m_groupRegionMap.clear();
  304. }
  305. void ImGuiCpuProfiler::DrawStatisticsView()
  306. {
  307. DrawCommonHeader();
  308. const auto ShowRow = [this](const char* regionLabel, double duration, double durationAverage)
  309. {
  310. ImGui::Text("%s", regionLabel);
  311. ImGui::TableNextColumn();
  312. ImGui::Text("%.2f", TicksToMs(duration));
  313. ImGui::TableNextColumn();
  314. ImGui::Text("%.2f", TicksToMs(durationAverage));
  315. ImGui::TableNextColumn();
  316. };
  317. if (ImGui::BeginChild("Statistics View", { 0, 0 }, true))
  318. {
  319. const auto flags = ImGuiTableFlags_Resizable;
  320. if (ImGui::BeginTable("General Timing Statistics", 3, flags))
  321. {
  322. // Table header setup
  323. ImGui::TableSetupColumn("");
  324. ImGui::TableSetupColumn("Current (ms)");
  325. ImGui::TableSetupColumn("Average (ms)");
  326. ImGui::TableHeadersRow();
  327. ImGui::TableNextColumn();
  328. for (const auto& queueStatistics : m_cpuTimingStatisticsWhenPause)
  329. {
  330. ShowRow(queueStatistics.m_name.c_str(), queueStatistics.m_executeDuration, queueStatistics.m_executeDurationAverage);
  331. }
  332. }
  333. ImGui::EndTable();
  334. ImGui::Separator();
  335. ImGui::Columns(1, "view", false);
  336. m_timedRegionFilter.Draw("Filter");
  337. ImGui::SameLine();
  338. if (ImGui::Button("Clear Filter"))
  339. {
  340. m_timedRegionFilter.Clear();
  341. }
  342. ImGui::SameLine();
  343. if (ImGui::Button("Reset Table"))
  344. {
  345. ResetTable();
  346. }
  347. DrawTable();
  348. }
  349. ImGui::EndChild();
  350. }
  351. void ImGuiCpuProfiler::DrawFilePicker()
  352. {
  353. ImGui::SetNextWindowSize({ 500, 200 }, ImGuiCond_Once);
  354. if (ImGui::Begin("File Picker", &m_showFilePicker))
  355. {
  356. if (ImGui::Button("Load selected"))
  357. {
  358. LoadFile();
  359. }
  360. auto getter = [](void* vectorPointer, int idx, const char** out_text) -> bool
  361. {
  362. const auto& pathVec = *static_cast<AZStd::vector<AZ::IO::Path>*>(vectorPointer);
  363. if (idx < 0 || idx >= pathVec.size())
  364. {
  365. return false;
  366. }
  367. *out_text = pathVec[idx].c_str();
  368. return true;
  369. };
  370. ImGui::SetNextItemWidth(ImGui::GetWindowContentRegionWidth());
  371. ImGui::ListBox("", &m_currentFileIndex, getter, &m_cachedCapturePaths, aznumeric_cast<int>(m_cachedCapturePaths.size()));
  372. }
  373. ImGui::End();
  374. }
  375. AZStd::string ImGuiCpuProfiler::GenerateOutputFile(const char* nameHint)
  376. {
  377. AZ::IO::FixedMaxPathString captureOutput = AZ::Debug::GetProfilerCaptureLocation();
  378. const AZ::IO::FixedMaxPathString frameDataFilePath =
  379. AZ::IO::FixedMaxPathString::format("%s/cpu_%s_%lld.json", captureOutput.c_str(), nameHint, AZStd::GetTimeNowSecond());
  380. AZ::IO::FileIOBase::GetInstance()->ResolvePath(m_lastCapturedFilePath, frameDataFilePath.c_str());
  381. return m_lastCapturedFilePath.String();
  382. }
  383. void ImGuiCpuProfiler::LoadFile()
  384. {
  385. const AZ::IO::Path& pathToLoad = m_cachedCapturePaths[m_currentFileIndex];
  386. auto loadResult = CpuProfilerImGuiHelper::LoadSavedCpuProfilingStatistics(pathToLoad.c_str());
  387. if (!loadResult.IsSuccess())
  388. {
  389. AZ_TracePrintf("ImGuiCpuProfiler", "%s", loadResult.GetError().c_str());
  390. return;
  391. }
  392. CpuProfilingStatisticsSerializer serializer = loadResult.TakeValue();
  393. AZStd::vector<CpuProfilingStatisticsSerializer::CpuProfilingStatisticsSerializerEntry>& deserializedData =
  394. serializer.m_cpuProfilingStatisticsSerializerEntries;
  395. // Clear visualizer and statistics view state
  396. m_savedRegionCount = deserializedData.size();
  397. m_savedData.clear();
  398. m_paused = true;
  399. AZ::Debug::ProfilerSystemInterface::Get()->SetActive(false);
  400. m_frameEndTicks.clear();
  401. m_tableData.clear();
  402. m_groupRegionMap.clear();
  403. m_ticksPerSecondFromFile = serializer.m_timeTicksPerSecond;
  404. // Since we don't serialize the frame boundaries, we will use "Component application tick" from
  405. // ComponentApplication::TickSystem as a heuristic.
  406. static const AZ::Name::Hash frameBoundaryHash = AZ::Name("Component application tick").GetHash();
  407. AZStd::sys_time_t frameTime = 0;
  408. for (const auto& entry : deserializedData)
  409. {
  410. const auto [groupNameItr, wasGroupNameInserted] = m_deserializedStringPool.emplace(entry.m_groupName.GetStringView());
  411. const auto [regionNameItr, wasRegionNameInserted] = m_deserializedStringPool.emplace(entry.m_regionName.GetStringView());
  412. const auto [groupRegionNameItr, wasGroupRegionNameInserted] =
  413. m_deserializedGroupRegionNamePool.emplace(groupNameItr->c_str(), regionNameItr->c_str());
  414. const CachedTimeRegion newRegion(*groupRegionNameItr, entry.m_stackDepth, entry.m_startTick, entry.m_endTick);
  415. m_savedData[entry.m_threadId].push_back(newRegion);
  416. if (entry.m_regionName.GetHash() == frameBoundaryHash)
  417. {
  418. if (!m_frameEndTicks.empty())
  419. {
  420. frameTime = entry.m_endTick - m_frameEndTicks.back();
  421. }
  422. else
  423. {
  424. frameTime = entry.m_endTick - entry.m_startTick;
  425. }
  426. m_frameEndTicks.push_back(entry.m_endTick);
  427. }
  428. // Update running statistics
  429. if (!m_groupRegionMap[*groupNameItr].contains(*regionNameItr))
  430. {
  431. m_groupRegionMap[*groupNameItr][*regionNameItr].m_groupName = *groupNameItr;
  432. m_groupRegionMap[*groupNameItr][*regionNameItr].m_regionName = *regionNameItr;
  433. m_tableData.push_back(&m_groupRegionMap[*groupNameItr][*regionNameItr]);
  434. }
  435. m_groupRegionMap[*groupNameItr][*regionNameItr].RecordRegion(newRegion, entry.m_threadId);
  436. }
  437. // Update viewport bounds to the estimated final frame time with some padding
  438. m_viewportStartTick = m_frameEndTicks.back() - frameTime - ProfilerViewEdgePadding;
  439. m_viewportEndTick = m_frameEndTicks.back() + ProfilerViewEdgePadding;
  440. // Invariant: each vector in m_savedData must be sorted so that we can efficiently cull region data.
  441. for (auto& [threadId, singleThreadData] : m_savedData)
  442. {
  443. AZStd::sort(singleThreadData.begin(), singleThreadData.end(),
  444. [](const TimeRegion& lhs, const TimeRegion& rhs)
  445. {
  446. return lhs.m_startTick < rhs.m_startTick;
  447. });
  448. }
  449. }
  450. // -- CPU Visualizer --
  451. void ImGuiCpuProfiler::DrawVisualizer()
  452. {
  453. DrawCommonHeader();
  454. // Options & Statistics
  455. if (ImGui::BeginChild("Options and Statistics", { 0, 0 }, true))
  456. {
  457. ImGui::Columns(3, "Options", true);
  458. ImGui::SliderInt("Update Freq. (ms)", &m_updateFrequencyMs, 0, MaxUpdateFrequencyMs, "%d", ImGuiSliderFlags_AlwaysClamp);
  459. ImGui::SliderInt("Saved Frames", &m_framesToCollect, MinSavableFrameCount, MaxSavableFrameCount, "%d", ImGuiSliderFlags_AlwaysClamp | ImGuiSliderFlags_Logarithmic);
  460. m_visualizerHighlightFilter.Draw("Find Region");
  461. // estimate the number of frames required to fulfill the update frequency
  462. const AZ::TimeMs deltaMs = AZ::TimeUsToMs(AZ::GetRealTickDeltaTimeUs());
  463. const int estimatedFrameCountPadding = 5; // padding is necessary to prevent flashes of blank frames
  464. const int estimatedFrameCount = aznumeric_cast<int >(AZ::TimeMs{ m_updateFrequencyMs } / deltaMs) + estimatedFrameCountPadding;
  465. // bump the number of saved frames to the update frequency estimate to prevent periods of empty data
  466. m_framesToCollect = AZStd::max(m_framesToCollect, estimatedFrameCount);
  467. ImGui::NextColumn();
  468. ImGui::Text("Viewport width: %.3f ms", TicksToMs(GetViewportTickWidth()));
  469. ImGui::Text("Ticks [%lld , %lld]", m_viewportStartTick, m_viewportEndTick);
  470. ImGui::Text("Recording %zu threads", m_savedData.size());
  471. ImGui::Text("%llu profiling events saved", m_savedRegionCount);
  472. ImGui::NextColumn();
  473. ImGui::TextWrapped(
  474. "Hold the right mouse button to move around. Zoom by scrolling the mouse wheel while holding <ctrl>.");
  475. }
  476. ImGui::Columns(1, "FrameTimeColumn", true);
  477. if (ImGui::BeginChild("FrameTimeHistogram", { 0, 50 }, true, ImGuiWindowFlags_NoScrollbar))
  478. {
  479. DrawFrameTimeHistogram();
  480. }
  481. ImGui::EndChild();
  482. ImGui::Columns(1, "RulerColumn", true);
  483. // Ruler
  484. if (ImGui::BeginChild("Ruler", { 0, 30 }, true, ImGuiWindowFlags_NoNavFocus))
  485. {
  486. DrawRuler();
  487. }
  488. ImGui::EndChild();
  489. ImGui::Columns(1, "TimelineColumn", true);
  490. // Timeline
  491. if (ImGui::BeginChild("Timeline", { 0, 0 }, true, ImGuiWindowFlags_AlwaysVerticalScrollbar))
  492. {
  493. // Find the next frame boundary after the viewport's right bound and draw until that tick
  494. auto nextFrameBoundaryItr = AZStd::lower_bound(m_frameEndTicks.begin(), m_frameEndTicks.end(), m_viewportEndTick);
  495. if (nextFrameBoundaryItr == m_frameEndTicks.end() && m_frameEndTicks.size() != 0)
  496. {
  497. --nextFrameBoundaryItr;
  498. }
  499. const AZStd::sys_time_t nextFrameBoundary = *nextFrameBoundaryItr;
  500. // Find the start tick of the leftmost frame, which may be offscreen.
  501. auto startTickItr = AZStd::lower_bound(m_frameEndTicks.begin(), m_frameEndTicks.end(), m_viewportStartTick);
  502. const AZStd::sys_time_t prevFrameBoundary = startTickItr != m_frameEndTicks.begin() ? *AZStd::prev(startTickItr) : 0;
  503. // Main draw loop
  504. AZ::u64 baseRow = 0;
  505. auto drawThreadDataFunc = [&](size_t threadId, const AZStd::vector<TimeRegion>& threadData)
  506. {
  507. // Find the first TimeRegion that we should draw
  508. auto regionItr = AZStd::lower_bound(
  509. threadData.begin(), threadData.end(), prevFrameBoundary,
  510. [](const TimeRegion& wrapper, AZStd::sys_time_t target)
  511. {
  512. return wrapper.m_startTick < target;
  513. });
  514. if (regionItr == threadData.end())
  515. {
  516. return;
  517. }
  518. // Draw all of the blocks for a given thread/row
  519. AZ::u64 maxDepth = 0;
  520. while (regionItr != threadData.end())
  521. {
  522. const TimeRegion& region = *regionItr;
  523. // Early out if we have drawn all the onscreen regions
  524. if (region.m_startTick > nextFrameBoundary)
  525. {
  526. break;
  527. }
  528. AZ::u64 targetRow = region.m_stackDepth + baseRow;
  529. maxDepth = AZStd::max(aznumeric_cast<AZ::u64>(region.m_stackDepth), maxDepth);
  530. DrawBlock(region, targetRow);
  531. ++regionItr;
  532. }
  533. // Draw UI details
  534. DrawThreadLabel(baseRow, threadId);
  535. DrawThreadSeparator(baseRow, maxDepth);
  536. baseRow += maxDepth + 1; // Next draw loop should start one row down
  537. };
  538. // keep the main thread at the top
  539. drawThreadDataFunc(m_mainThreadId, m_savedData[m_mainThreadId]);
  540. for (const auto& [threadId, threadData] : m_savedData)
  541. {
  542. if (threadId != m_mainThreadId)
  543. {
  544. drawThreadDataFunc(threadId, threadData);
  545. }
  546. }
  547. DrawFrameBoundaries();
  548. // Draw an invisible button to capture inputs and make sure it has a non-zero height
  549. ImGui::InvisibleButton("Timeline Input",
  550. { ImGui::GetWindowContentRegionWidth(), AZ::GetMax(baseRow, decltype(baseRow){1}) * RowHeight });
  551. // Controls
  552. ImGuiIO& io = ImGui::GetIO();
  553. if (ImGui::IsWindowFocused() && ImGui::IsItemHovered())
  554. {
  555. io.WantCaptureMouse = true;
  556. if (ImGui::IsMouseDragging(ImGuiMouseButton_Right)) // Scrolling
  557. {
  558. const auto [deltaX, deltaY] = io.MouseDelta;
  559. if (deltaX != 0 || deltaY != 0)
  560. {
  561. // We want to maintain uniformity in scrolling (a click and drag should leave the cursor at the same spot
  562. // relative to the objects on screen)
  563. const float pixelDeltaNormalized = deltaX / ImGui::GetWindowWidth();
  564. auto tickDelta = aznumeric_cast<AZStd::sys_time_t>(-1 * pixelDeltaNormalized * GetViewportTickWidth());
  565. m_viewportStartTick += tickDelta;
  566. m_viewportEndTick += tickDelta;
  567. ImGui::SetScrollY(ImGui::GetScrollY() + deltaY * -1);
  568. }
  569. }
  570. else if (io.MouseWheel != 0 && io.KeyCtrl) // Zooming
  571. {
  572. // We want zooming to be relative to the mouse's current position
  573. const float mouseX = ImGui::GetMousePos().x;
  574. // Find the normalized position of the cursor relative to the window
  575. const float percentWindow = (mouseX - ImGui::GetWindowPos().x) / ImGui::GetWindowWidth();
  576. const auto overallTickDelta = aznumeric_cast<AZStd::sys_time_t>(0.05 * io.MouseWheel * GetViewportTickWidth());
  577. // Split the overall delta between the two bounds depending on mouse pos
  578. const auto newStartTick = m_viewportStartTick + aznumeric_cast<AZStd::sys_time_t>(percentWindow * overallTickDelta);
  579. const auto newEndTick = m_viewportEndTick - aznumeric_cast<AZStd::sys_time_t>((1-percentWindow) * overallTickDelta);
  580. // Avoid zooming too much, start tick should always be less than end tick
  581. if (newStartTick < newEndTick)
  582. {
  583. m_viewportStartTick = newStartTick;
  584. m_viewportEndTick = newEndTick;
  585. }
  586. }
  587. }
  588. }
  589. ImGui::EndChild(); // "Timeline"
  590. ImGui::EndChild(); // "Options and Statistics"
  591. }
  592. void ImGuiCpuProfiler::CacheCpuTimingStatistics()
  593. {
  594. using namespace AZ::Statistics;
  595. m_cpuTimingStatisticsWhenPause.clear();
  596. if (auto statsProfiler = AZ::Interface<StatisticalProfilerProxy>::Get(); statsProfiler)
  597. {
  598. AZStd::vector<NamedRunningStatistic*> statistics;
  599. statistics.reserve(InitialCpuTimingStatsAllocation);
  600. statsProfiler->GetAllStatisticsOfUnits(statistics, "clocks");
  601. for (NamedRunningStatistic* stat : statistics)
  602. {
  603. m_cpuTimingStatisticsWhenPause.push_back({ stat->GetName(), stat->GetMostRecentSample(), stat->GetAverage() });
  604. }
  605. }
  606. }
  607. void ImGuiCpuProfiler::CollectFrameData()
  608. {
  609. // We maintain separate datastores for the visualizer and the statistical view because they require different
  610. // data formats - one grouped by thread ID versus the other organized by group + region. Since the statistical
  611. // view is only holding data from the last frame, the memory overhead is minimal and gives us a faster redraw
  612. // compared to if we needed to transform the visualizer's data into the statistical format every frame.
  613. // Get the latest TimeRegionMap
  614. auto profilerInterface = AZ::Interface<AZ::Debug::Profiler>::Get();
  615. auto cpuProfiler = azrtti_cast<CpuProfiler*>(profilerInterface);
  616. const TimeRegionMap& timeRegionMap = cpuProfiler->GetTimeRegionMap();
  617. AZ::s64 viewportStartTick = AZStd::numeric_limits<AZ::s64>::max();
  618. AZ::s64 viewportEndTick = AZStd::numeric_limits<AZ::s64>::lowest();
  619. // Iterate through the entire TimeRegionMap and copy the data since it will get deleted on the next frame
  620. for (const auto& [threadId, singleThreadRegionMap] : timeRegionMap)
  621. {
  622. const size_t threadIdHashed = AZStd::hash<AZStd::thread_id>{}(threadId);
  623. // The profiler can sometime return threads without any profiling events when dropping threads, FIXME(ATOM-15949)
  624. if (singleThreadRegionMap.size() == 0)
  625. {
  626. continue;
  627. }
  628. // Now focus on just the data for the current thread
  629. AZStd::vector<TimeRegion> newVisualizerData;
  630. newVisualizerData.reserve(singleThreadRegionMap.size()); // Avoids reallocation in the normal case when each region only has one invocation
  631. for (const auto& [regionName, regionVec] : singleThreadRegionMap)
  632. {
  633. for (const TimeRegion& region : regionVec)
  634. {
  635. newVisualizerData.push_back(region); // Copies
  636. // Also update the statistical view's data
  637. const AZStd::string& groupName = region.m_groupRegionName.m_groupName;
  638. if (!m_groupRegionMap[groupName].contains(regionName))
  639. {
  640. m_groupRegionMap[groupName][regionName].m_groupName = groupName;
  641. m_groupRegionMap[groupName][regionName].m_regionName = regionName;
  642. m_tableData.push_back(&m_groupRegionMap[groupName][regionName]);
  643. }
  644. m_groupRegionMap[groupName][regionName].RecordRegion(region, threadIdHashed);
  645. }
  646. }
  647. // Sorting by start tick allows us to speed up some other processes (ex. finding the first block to draw)
  648. // since we can binary search by start tick.
  649. AZStd::sort(
  650. newVisualizerData.begin(), newVisualizerData.end(),
  651. [](const TimeRegion& lhs, const TimeRegion& rhs)
  652. {
  653. return lhs.m_startTick < rhs.m_startTick;
  654. });
  655. // Use the latest frame's data as the new bounds of the viewport
  656. viewportStartTick = AZStd::min(newVisualizerData.front().m_startTick, viewportStartTick);
  657. viewportEndTick = AZStd::max(newVisualizerData.back().m_endTick, viewportEndTick);
  658. m_savedRegionCount += newVisualizerData.size();
  659. // Move onto the end of the current thread's saved data, sorted order maintained
  660. AZStd::vector<TimeRegion>& savedDataVec = m_savedData[threadIdHashed];
  661. savedDataVec.insert(
  662. savedDataVec.end(), AZStd::make_move_iterator(newVisualizerData.begin()), AZStd::make_move_iterator(newVisualizerData.end()));
  663. }
  664. // only update the viewport bounds at the specified frequency
  665. m_currentUpdateTimeMs += AZ::TimeUsToMs(AZ::GetRealTickDeltaTimeUs());
  666. if (m_currentUpdateTimeMs >= static_cast<AZ::TimeMs>(m_updateFrequencyMs))
  667. {
  668. m_currentUpdateTimeMs = AZ::TimeMs{ 0 };
  669. m_viewportStartTick = viewportStartTick;
  670. m_viewportEndTick = viewportEndTick;
  671. }
  672. }
  673. void ImGuiCpuProfiler::CullFrameData()
  674. {
  675. const AZ::TimeUs delta = AZ::GetRealTickDeltaTimeUs();
  676. const float deltaTimeInSeconds = AZ::TimeUsToSeconds(delta);
  677. const AZStd::sys_time_t frameToFrameTime = static_cast<AZStd::sys_time_t>(deltaTimeInSeconds * AZStd::GetTimeTicksPerSecond());
  678. const AZStd::sys_time_t deleteBeforeTick = AZStd::GetTimeNowTicks() - frameToFrameTime * m_framesToCollect;
  679. // Remove old frame boundary data
  680. auto firstBoundaryToKeepItr = AZStd::upper_bound(m_frameEndTicks.begin(), m_frameEndTicks.end(), deleteBeforeTick);
  681. m_frameEndTicks.erase(m_frameEndTicks.begin(), firstBoundaryToKeepItr);
  682. // Remove old region data for each thread
  683. for (auto& [threadId, savedRegions] : m_savedData)
  684. {
  685. AZStd::size_t sizeBeforeRemove = savedRegions.size();
  686. // Early out to avoid the linear erase_if call
  687. if (savedRegions.size() >= 1 && savedRegions.at(0).m_startTick > deleteBeforeTick)
  688. {
  689. continue;
  690. }
  691. // Use erase_if over plain upper_bound + erase to avoid repeated shifts. erase requires a shift of all elements to the right
  692. // for each element that is erased, while erase_if squashes all removes into a single shift which significantly improves perf.
  693. AZStd::erase_if(
  694. savedRegions,
  695. [deleteBeforeTick](const TimeRegion& region)
  696. {
  697. return region.m_startTick < deleteBeforeTick;
  698. });
  699. m_savedRegionCount -= sizeBeforeRemove - savedRegions.size();
  700. }
  701. // Remove any threads from the top-level map that no longer hold data
  702. AZStd::erase_if(
  703. m_savedData,
  704. [](const auto& singleThreadDataEntry)
  705. {
  706. return singleThreadDataEntry.second.empty();
  707. });
  708. }
  709. void ImGuiCpuProfiler::DrawBlock(const TimeRegion& block, AZ::u64 targetRow)
  710. {
  711. // Don't draw anything if the user is searching for regions and this block doesn't pass the filter
  712. if (!m_visualizerHighlightFilter.PassFilter(block.m_groupRegionName.m_regionName.GetCStr()))
  713. {
  714. return;
  715. }
  716. float wy = ImGui::GetWindowPos().y - ImGui::GetScrollY();
  717. ImDrawList* drawList = ImGui::GetWindowDrawList();
  718. const float startPixel = ConvertTickToPixelSpace(block.m_startTick, m_viewportStartTick, m_viewportEndTick);
  719. const float endPixel = ConvertTickToPixelSpace(block.m_endTick, m_viewportStartTick, m_viewportEndTick);
  720. if (endPixel - startPixel < 0.5f)
  721. {
  722. return;
  723. }
  724. const ImVec2 startPoint = { startPixel, wy + targetRow * RowHeight + 1};
  725. const ImVec2 endPoint = { endPixel, wy + (targetRow + 1) * RowHeight };
  726. const ImU32 blockColor = GetBlockColor(block);
  727. drawList->AddRectFilled(startPoint, endPoint, blockColor, 0);
  728. drawList->AddLine(startPoint, { endPixel, startPoint.y }, IM_COL32_BLACK, 0.5f);
  729. drawList->AddLine({ startPixel, endPoint.y }, endPoint, IM_COL32_BLACK, 0.5f);
  730. // Draw the region name if possible
  731. // If the block's current width is too small, we skip drawing the label.
  732. const float regionPixelWidth = endPixel - startPixel;
  733. const float maxCharWidth = ImGui::CalcTextSize("M").x; // M is usually the largest character in most fonts (see CSS em)
  734. if (regionPixelWidth > maxCharWidth) // We can draw at least one character
  735. {
  736. const AZStd::string label =
  737. AZStd::string::format("%s/ %s", block.m_groupRegionName.m_groupName, block.m_groupRegionName.m_regionName.GetCStr());
  738. const float textWidth = ImGui::CalcTextSize(label.c_str()).x;
  739. if (regionPixelWidth < textWidth) // Not enough space in the block to draw the whole name, draw clipped text.
  740. {
  741. const ImVec4 clipRect = { startPoint.x, startPoint.y, endPoint.x - maxCharWidth, endPoint.y };
  742. // NOTE: RenderText calls do not automatically account for the global scale (which is modified at high DPI)
  743. // so we must adjust for the scale manually.
  744. const float scaleFactor = ImGui::GetIO().FontGlobalScale;
  745. const float fontSize = ImGui::GetFont()->FontSize * scaleFactor;
  746. ImGui::GetFont()->RenderText(drawList, fontSize, startPoint, IM_COL32_WHITE, clipRect, label.c_str(), 0);
  747. }
  748. else // We have enough space to draw the entire label, draw and center text.
  749. {
  750. const float remainingWidth = regionPixelWidth - textWidth;
  751. const float offset = remainingWidth * .5f;
  752. drawList->AddText({ startPoint.x + offset, startPoint.y }, IM_COL32_WHITE, label.c_str());
  753. }
  754. }
  755. // Tooltip and block highlighting
  756. if (ImGui::IsMouseHoveringRect(startPoint, endPoint) && ImGui::IsWindowHovered())
  757. {
  758. // Go to the statistics view when a region is clicked
  759. if (ImGui::IsMouseClicked(ImGuiMouseButton_Left))
  760. {
  761. m_enableVisualizer = false;
  762. m_timedRegionFilter = ImGuiTextFilter(block.m_groupRegionName.m_regionName.GetCStr());
  763. m_timedRegionFilter.Build();
  764. }
  765. // Hovering outline
  766. drawList->AddRect(startPoint, endPoint, ImGui::GetColorU32({ 1, 1, 1, 1 }), 0.0, 0, 1.5);
  767. ImGui::BeginTooltip();
  768. ImGui::Text("%s::%s", block.m_groupRegionName.m_groupName, block.m_groupRegionName.m_regionName.GetCStr());
  769. ImGui::Text("Execution time: %.3f ms", TicksToMs(block.m_endTick - block.m_startTick));
  770. ImGui::Text("Ticks %lld => %lld", block.m_startTick, block.m_endTick);
  771. ImGui::EndTooltip();
  772. }
  773. }
  774. ImU32 ImGuiCpuProfiler::GetBlockColor(const TimeRegion& block)
  775. {
  776. // Use the GroupRegionName pointer a key into the cache, equal regions will have equal pointers
  777. const GroupRegionName& key = block.m_groupRegionName;
  778. if (auto iter = m_regionColorMap.find(key); iter != m_regionColorMap.end()) // Cache hit
  779. {
  780. return ImGui::GetColorU32(iter->second);
  781. }
  782. // Cache miss, generate a new random color
  783. AZ::SimpleLcgRandom rand(aznumeric_cast<AZ::u64>(AZStd::GetTimeNowTicks()));
  784. const float r = AZStd::clamp(rand.GetRandomFloat(), .1f, .9f);
  785. const float g = AZStd::clamp(rand.GetRandomFloat(), .1f, .9f);
  786. const float b = AZStd::clamp(rand.GetRandomFloat(), .1f, .9f);
  787. const ImVec4 randomColor = {r, g, b, .8};
  788. m_regionColorMap.emplace(key, randomColor);
  789. return ImGui::GetColorU32(randomColor);
  790. }
  791. void ImGuiCpuProfiler::DrawThreadSeparator(AZ::u64 baseRow, AZ::u64 maxDepth)
  792. {
  793. const ImU32 red = ImGui::GetColorU32({ 1, 0, 0, 1 });
  794. auto [wx, wy] = ImGui::GetWindowPos();
  795. wy -= ImGui::GetScrollY();
  796. const float windowWidth = ImGui::GetWindowWidth();
  797. const float boundaryY = wy + (baseRow + maxDepth + 1) * RowHeight;
  798. ImGui::GetWindowDrawList()->AddLine({ wx, boundaryY }, { wx + windowWidth, boundaryY }, red, 1.0f);
  799. }
  800. void ImGuiCpuProfiler::DrawThreadLabel(AZ::u64 baseRow, size_t threadId)
  801. {
  802. auto [wx, wy] = ImGui::GetWindowPos();
  803. wy -= ImGui::GetScrollY();
  804. const AZStd::string threadIdText = AZStd::string::format("Thread: %zu", threadId);
  805. ImGui::GetWindowDrawList()->AddText({ wx + 10, wy + baseRow * RowHeight}, IM_COL32_WHITE, threadIdText.c_str());
  806. }
  807. void ImGuiCpuProfiler::DrawFrameBoundaries()
  808. {
  809. ImDrawList* drawList = ImGui::GetWindowDrawList();
  810. const float wy = ImGui::GetWindowPos().y;
  811. const float windowHeight = ImGui::GetWindowHeight();
  812. const ImU32 red = ImGui::GetColorU32({ 1, 0, 0, 1 });
  813. // End ticks are sorted in increasing order, find the first frame bound to draw
  814. auto endTickItr = AZStd::lower_bound(m_frameEndTicks.begin(), m_frameEndTicks.end(), m_viewportStartTick);
  815. while (endTickItr != m_frameEndTicks.end() && *endTickItr < m_viewportEndTick)
  816. {
  817. const float horizontalPixel = ConvertTickToPixelSpace(*endTickItr, m_viewportStartTick, m_viewportEndTick);
  818. drawList->AddLine({ horizontalPixel, wy }, { horizontalPixel, wy + windowHeight }, red);
  819. ++endTickItr;
  820. }
  821. }
  822. void ImGuiCpuProfiler::DrawRuler()
  823. {
  824. // Use a pair of iterators to go through all saved frame boundaries and draw ruler lines
  825. auto lastFrameBoundaryItr = AZStd::lower_bound(m_frameEndTicks.begin(), m_frameEndTicks.end(), m_viewportStartTick);
  826. auto nextFrameBoundaryItr = lastFrameBoundaryItr;
  827. if (lastFrameBoundaryItr != m_frameEndTicks.begin())
  828. {
  829. --lastFrameBoundaryItr;
  830. }
  831. const auto [wx, wy] = ImGui::GetWindowPos();
  832. ImDrawList* drawList = ImGui::GetWindowDrawList();
  833. while (nextFrameBoundaryItr != m_frameEndTicks.end() && *lastFrameBoundaryItr <= m_viewportEndTick)
  834. {
  835. const AZStd::sys_time_t lastFrameBoundaryTick = *lastFrameBoundaryItr;
  836. const AZStd::sys_time_t nextFrameBoundaryTick = *nextFrameBoundaryItr;
  837. if (lastFrameBoundaryTick > m_viewportEndTick)
  838. {
  839. break;
  840. }
  841. const float lastFrameBoundaryPixel = ConvertTickToPixelSpace(lastFrameBoundaryTick, m_viewportStartTick, m_viewportEndTick);
  842. const float nextFrameBoundaryPixel = ConvertTickToPixelSpace(nextFrameBoundaryTick, m_viewportStartTick, m_viewportEndTick);
  843. const AZStd::string label =
  844. AZStd::string::format("%.2f ms", TicksToMs(nextFrameBoundaryTick - lastFrameBoundaryTick));
  845. const float labelWidth = ImGui::CalcTextSize(label.c_str()).x;
  846. // The label can fit between the two boundaries, center it and draw
  847. if (labelWidth <= nextFrameBoundaryPixel - lastFrameBoundaryPixel)
  848. {
  849. const float offset = (nextFrameBoundaryPixel - lastFrameBoundaryPixel - labelWidth) /2;
  850. const float textBeginPixel = lastFrameBoundaryPixel + offset;
  851. const float textEndPixel = textBeginPixel + labelWidth;
  852. const float verticalOffset = (ImGui::GetWindowHeight() - ImGui::GetFontSize()) / 2;
  853. // Execution time label
  854. drawList->AddText({ textBeginPixel, wy + verticalOffset }, IM_COL32_WHITE, label.c_str());
  855. // Left side
  856. drawList->AddLine(
  857. { lastFrameBoundaryPixel, wy + ImGui::GetWindowHeight() / 2 },
  858. { textBeginPixel - 5, wy + ImGui::GetWindowHeight() / 2},
  859. IM_COL32_WHITE);
  860. // Right side
  861. drawList->AddLine(
  862. { textEndPixel, wy + ImGui::GetWindowHeight()/2 },
  863. { nextFrameBoundaryPixel, wy + ImGui::GetWindowHeight()/2 },
  864. IM_COL32_WHITE);
  865. }
  866. else // Cannot fit inside, just draw a line between the two boundaries
  867. {
  868. drawList->AddLine(
  869. { lastFrameBoundaryPixel, wy + ImGui::GetWindowHeight() / 2 },
  870. { nextFrameBoundaryPixel, wy + ImGui::GetWindowHeight() / 2 },
  871. IM_COL32_WHITE);
  872. }
  873. // Left bound
  874. drawList->AddLine(
  875. { lastFrameBoundaryPixel, wy },
  876. { lastFrameBoundaryPixel, wy + ImGui::GetWindowHeight() },
  877. IM_COL32_WHITE);
  878. // Right bound
  879. drawList->AddLine(
  880. { nextFrameBoundaryPixel, wy },
  881. { nextFrameBoundaryPixel, wy + ImGui::GetWindowHeight() },
  882. IM_COL32_WHITE);
  883. lastFrameBoundaryItr = nextFrameBoundaryItr;
  884. ++nextFrameBoundaryItr;
  885. }
  886. }
  887. void ImGuiCpuProfiler::DrawFrameTimeHistogram()
  888. {
  889. ImDrawList* drawList = ImGui::GetWindowDrawList();
  890. const auto [wx, wy] = ImGui::GetWindowPos();
  891. const ImU32 orange = ImGui::GetColorU32({ 1, .7, 0, 1 });
  892. const ImU32 red = ImGui::GetColorU32({ 1, 0, 0, 1 });
  893. const AZStd::sys_time_t ticksPerSecond = AZStd::GetTimeTicksPerSecond();
  894. const AZStd::sys_time_t viewportCenter = m_viewportEndTick - (m_viewportEndTick - m_viewportStartTick) / 2;
  895. const AZStd::sys_time_t leftHistogramBound = viewportCenter - ticksPerSecond;
  896. const AZStd::sys_time_t rightHistogramBound = viewportCenter + ticksPerSecond;
  897. // Draw frame limit lines
  898. drawList->AddLine(
  899. { wx, wy + ImGui::GetWindowHeight() - MediumFrameTimeLimit },
  900. { wx + ImGui::GetWindowWidth(), wy + ImGui::GetWindowHeight() - MediumFrameTimeLimit },
  901. orange);
  902. drawList->AddLine(
  903. { wx, wy + ImGui::GetWindowHeight() - HighFrameTimeLimit },
  904. { wx + ImGui::GetWindowWidth(), wy + ImGui::GetWindowHeight() - HighFrameTimeLimit },
  905. red);
  906. // Draw viewport bound rectangle
  907. const float leftViewportPixel = ConvertTickToPixelSpace(m_viewportStartTick, leftHistogramBound, rightHistogramBound);
  908. const float rightViewportPixel = ConvertTickToPixelSpace(m_viewportEndTick, leftHistogramBound, rightHistogramBound);
  909. const ImVec2 topLeftPos = { leftViewportPixel, wy };
  910. const ImVec2 botRightPos = { rightViewportPixel, wy + ImGui::GetWindowHeight() };
  911. const ImU32 gray = ImGui::GetColorU32({ 1, 1, 1, .3 });
  912. drawList->AddRectFilled(topLeftPos, botRightPos, gray);
  913. // Find the first onscreen frame execution time
  914. auto frameEndTickItr = AZStd::lower_bound(m_frameEndTicks.begin(), m_frameEndTicks.end(), leftHistogramBound);
  915. if (frameEndTickItr != m_frameEndTicks.begin())
  916. {
  917. --frameEndTickItr;
  918. }
  919. // Since we only store the frame end ticks, we must calculate the execution times on the fly by comparing pairs of elements.
  920. AZStd::sys_time_t lastFrameEndTick = *frameEndTickItr;
  921. while (*frameEndTickItr < rightHistogramBound && ++frameEndTickItr != m_frameEndTicks.end())
  922. {
  923. const AZStd::sys_time_t frameEndTick = *frameEndTickItr;
  924. const float framePixelPos = ConvertTickToPixelSpace(frameEndTick, leftHistogramBound, rightHistogramBound);
  925. const float frameTimeMs = TicksToMs(frameEndTick - lastFrameEndTick);
  926. const ImVec2 lineBottom = { framePixelPos, ImGui::GetWindowHeight() + wy };
  927. const ImVec2 lineTop = { framePixelPos, ImGui::GetWindowHeight() + wy - frameTimeMs };
  928. ImU32 lineColor = ImGui::GetColorU32({ .3, .3, .3, 1 }); // Gray
  929. if (frameTimeMs > HighFrameTimeLimit)
  930. {
  931. lineColor = ImGui::GetColorU32({1, 0, 0, 1}); // Red
  932. }
  933. else if (frameTimeMs > MediumFrameTimeLimit)
  934. {
  935. lineColor = ImGui::GetColorU32({1, .7, 0, 1}); // Orange
  936. }
  937. drawList->AddLine(lineBottom, lineTop, lineColor, 3.0);
  938. lastFrameEndTick = frameEndTick;
  939. }
  940. // Handle input
  941. ImGui::InvisibleButton("HistogramInputCapture", { ImGui::GetWindowWidth(), ImGui::GetWindowHeight() });
  942. ImGuiIO& io = ImGui::GetIO();
  943. if (ImGui::IsItemClicked(ImGuiMouseButton_Left))
  944. {
  945. const float mousePixelX = io.MousePos.x;
  946. const float percentWindow = (mousePixelX - wx) / ImGui::GetWindowWidth();
  947. const AZStd::sys_time_t newViewportCenterTick = leftHistogramBound +
  948. aznumeric_cast<AZStd::sys_time_t>((rightHistogramBound - leftHistogramBound) * percentWindow);
  949. const AZStd::sys_time_t viewportWidth = GetViewportTickWidth();
  950. m_viewportEndTick = newViewportCenterTick + viewportWidth / 2;
  951. m_viewportStartTick = newViewportCenterTick - viewportWidth / 2;
  952. }
  953. }
  954. AZStd::sys_time_t ImGuiCpuProfiler::GetViewportTickWidth() const
  955. {
  956. return m_viewportEndTick - m_viewportStartTick;
  957. }
  958. float ImGuiCpuProfiler::ConvertTickToPixelSpace(AZStd::sys_time_t tick, AZStd::sys_time_t leftBound, AZStd::sys_time_t rightBound) const
  959. {
  960. const float wx = ImGui::GetWindowPos().x;
  961. const float tickSpaceShifted = aznumeric_cast<float>(tick - leftBound); // This will be close to zero, so FP inaccuracy should not be too bad
  962. const float tickSpaceNormalized = tickSpaceShifted / (rightBound - leftBound);
  963. const float pixelSpace = tickSpaceNormalized * ImGui::GetWindowWidth() + wx;
  964. return pixelSpace;
  965. }
  966. // System tick bus overrides
  967. void ImGuiCpuProfiler::OnSystemTick()
  968. {
  969. if (m_paused)
  970. {
  971. AZ::SystemTickBus::Handler::BusDisconnect();
  972. }
  973. else
  974. {
  975. m_frameEndTicks.push_back(AZStd::GetTimeNowTicks());
  976. for (auto& [groupName, regionMap] : m_groupRegionMap)
  977. {
  978. for (auto& [regionName, row] : regionMap)
  979. {
  980. row.ResetPerFrameStatistics();
  981. }
  982. }
  983. }
  984. }
  985. // ---- TableRow impl ----
  986. void TableRow::RecordRegion(const CachedTimeRegion& region, size_t threadId)
  987. {
  988. const AZStd::sys_time_t deltaTime = region.m_endTick - region.m_startTick;
  989. // Update per frame statistics
  990. ++m_invocationsLastFrame;
  991. m_executingThreads.insert(threadId);
  992. m_lastFrameTotalTicks += deltaTime;
  993. m_maxTicks = AZStd::max(m_maxTicks, deltaTime);
  994. // Update aggregate statistics
  995. m_runningAverageTicks =
  996. aznumeric_cast<AZStd::sys_time_t>((1.0 * (deltaTime + m_invocationsTotal * m_runningAverageTicks)) / (m_invocationsTotal + 1));
  997. ++m_invocationsTotal;
  998. }
  999. void TableRow::ResetPerFrameStatistics()
  1000. {
  1001. m_invocationsLastFrame = 0;
  1002. m_executingThreads.clear();
  1003. m_lastFrameTotalTicks = 0;
  1004. m_maxTicks = 0;
  1005. }
  1006. AZStd::string TableRow::GetExecutingThreadsLabel() const
  1007. {
  1008. auto threadString = AZStd::string::format("Executed in %zu threads\n", m_executingThreads.size());
  1009. for (const auto& threadId : m_executingThreads)
  1010. {
  1011. threadString.append(AZStd::string::format("Thread: %zu\n", threadId));
  1012. }
  1013. return threadString;
  1014. }
  1015. } // namespace Profiler
  1016. #endif // defined(IMGUI_ENABLED)