diff --git a/Gems/Atom/Utils/Code/Include/Atom/Utils/ImGuiCpuProfiler.h b/Gems/Atom/Utils/Code/Include/Atom/Utils/ImGuiCpuProfiler.h index d21783a801..a4f5763c99 100644 --- a/Gems/Atom/Utils/Code/Include/Atom/Utils/ImGuiCpuProfiler.h +++ b/Gems/Atom/Utils/Code/Include/Atom/Utils/ImGuiCpuProfiler.h @@ -10,7 +10,6 @@ #include #include -#include #include #include @@ -25,41 +24,50 @@ namespace AZ namespace Render { - struct ThreadRegionEntry - { - AZStd::thread_id m_threadId; - AZStd::sys_time_t m_startTick = 0; - AZStd::sys_time_t m_endTick = 0; - }; - + //! Stores all the data associated with a row in the table. struct TableRow { + // Update running statistics with new region data void RecordRegion(const AZ::RHI::CachedTimeRegion& region, AZStd::thread_id threadId); - double GetAverageInvocationsPerFrame() const; + + void ResetPerFrameStatistics(); + + // Get a string of all threads that this region executed in during the last frame AZStd::string TableRow::GetExecutingThreadsLabel() const; AZStd::string m_groupName; AZStd::string m_regionName; - AZStd::sys_time_t m_maxTicks; - AZStd::sys_time_t m_runningAverageTicks; - u64 m_invocations; + // --- Per frame statistics --- + + u64 m_invocationsLastFrame = 0; + + // NOTE: set over unordered_set so the threads can be shown in increasing order in tooltip. AZStd::set m_executingThreads; + + AZStd::sys_time_t m_lastFrameTotalTicks = 0; + + // Maximum execution time of a region in the last frame. + AZStd::sys_time_t m_maxTicks = 0; + + // --- Aggregate statistics --- + + u64 m_invocationsTotal = 0; + + // Running average of Mean Time Per Call + AZStd::sys_time_t m_runningAverageTicks = 0; }; - //! Visual profiler for Cpu statistics. - //! It uses ImGui as the library for displaying the Attachments and Heaps. - //! It shows all heaps that are being used by the RHI and how the FIXME - //! resources are allocated in each heap. + //! ImGui widget for examining Atom CPU Profiling instrumentation. + //! Offers both a statistical view (with sorting and searching capability) and a visualizer + //! similar to RAD and other profiling tools. class ImGuiCpuProfiler : SystemTickBus::Handler { - friend struct TableRow; - - // Region Name -> Array of ThreadRegion entries - using RegionEntryMap = AZStd::map; - // Group Name -> RegionEntryMap - using GroupRegionMap = AZStd::map; + // Region Name -> statistical view row data + using RegionRowMap = AZStd::map; + // Group Name -> RegionRowMap + using GroupRegionMap = AZStd::map; using TimeRegion = AZ::RHI::CachedTimeRegion; using GroupRegionName = AZ::RHI::CachedTimeRegion::GroupRegionName; @@ -71,63 +79,34 @@ namespace AZ //! Draws the overall CPU profiling window, defaults to the statistical view void Draw(bool& keepDrawing, const AZ::RHI::CpuTimingStatistics& cpuTimingStatistics); - //! Draws the statistical view of the CPU profiling data - void DrawStatisticsView(); - - //! Draws the CPU profiling visualizer in a new window. - void DrawVisualizer(); - private: static constexpr float RowHeight = 50.0; static constexpr int DefaultFramesToCollect = 50; static constexpr float MediumFrameTimeLimit = 16.6; // 60 fps static constexpr float HighFrameTimeLimit = 33.3; // 30 fps - static u64 ms_framesActive; + //! Draws the statistical view of the CPU profiling data. + void DrawStatisticsView(); - // Draw the shared header between the two windows + //! Draws the CPU profiling visualizer. + void DrawVisualizer(); + + // Draw the shared header between the two windows. void DrawCommonHeader(); - // Draw the region statstics table in the order specified by the pointers in m_tableData + // Draw the region statistics table in the order specified by the pointers in m_tableData. void DrawTable(); - // Sort the table by a given column, rearranges the pointers in m_tableData + // Sort the table by a given column, rearranges the pointers in m_tableData. void SortTable(ImGuiTableSortSpecs* sortSpecs); - // ImGui filter used to filter TimedRegions. - ImGuiTextFilter m_timedRegionFilter; - - // Saves statistical view data organized by group name -> region name -> row data - GroupRegionMap m_groupRegionMap; - - // Saves pointers to objects in m_groupRegionMap, order reflects table ordering - AZStd::vector m_tableData; - - // Pause cpu profiling. The profiler will show the statistics of the last frame before pause - bool m_paused = false; - - // Export the profiling data from a single frame to a local file - bool m_captureToFile = false; - - // Toggle between the normal statistical view and the visual profiling view - bool m_enableVisualizer = false; - - // Total frames need to be saved - int m_captureFrameCount = 1; - - AZ::RHI::CpuTimingStatistics m_cpuTimingStatisticsWhenPause; - - AZStd::string m_lastCapturedFilePath; - - // Visualizer methods - // Get the profiling data from the last frame, only called when the profiler is not paused. void CollectFrameData(); // Cull old data from internal storage, only called when profiler is not paused. void CullFrameData(const AZ::RHI::CpuTimingStatistics& currentCpuTimingStatistics); - // Draws a single block onto the timeline + // Draws a single block onto the timeline into the specified row void DrawBlock(const TimeRegion& block, u64 targetRow); // Draw horizontal lines between threads in the timeline @@ -150,14 +129,14 @@ namespace AZ AZStd::sys_time_t GetViewportTickWidth() const; - // Gets the color for a block using the GroupRegionName as a key into the cache - // Generates a random ImU32 if the block does not yet have a color + // Gets the color for a block using the GroupRegionName as a key into the cache. + // Generates a random ImU32 if the block does not yet have a color. ImU32 GetBlockColor(const TimeRegion& block); // System tick bus overrides virtual void OnSystemTick() override; - // Visualizer state + // --- Visualizer Members --- int m_framesToCollect = DefaultFramesToCollect; @@ -179,6 +158,32 @@ namespace AZ // Filter for highlighting regions on the visualizer ImGuiTextFilter m_visualizerHighlightFilter; + + // --- Tabular view members --- + + // ImGui filter used to filter TimedRegions. + ImGuiTextFilter m_timedRegionFilter; + + // Saves statistical view data organized by group name -> region name -> row data + GroupRegionMap m_groupRegionMap; + + // Saves pointers to objects in m_groupRegionMap, order reflects table ordering. + // Non-owning, will be cleared when m_groupRegionMap is cleared. + AZStd::vector m_tableData; + + // Pause cpu profiling. The profiler will show the statistics of the last frame before pause. + bool m_paused = false; + + // Export the profiling data from a single frame to a local file. + bool m_captureToFile = false; + + // Toggle between the normal statistical view and the visual profiling view. + bool m_enableVisualizer = false; + + // Last captured CPU timing statistics + AZ::RHI::CpuTimingStatistics m_cpuTimingStatisticsWhenPause; + + AZStd::string m_lastCapturedFilePath; }; } // namespace Render } // namespace AZ diff --git a/Gems/Atom/Utils/Code/Include/Atom/Utils/ImGuiCpuProfiler.inl b/Gems/Atom/Utils/Code/Include/Atom/Utils/ImGuiCpuProfiler.inl index 124e48af13..d89fed449a 100644 --- a/Gems/Atom/Utils/Code/Include/Atom/Utils/ImGuiCpuProfiler.inl +++ b/Gems/Atom/Utils/Code/Include/Atom/Utils/ImGuiCpuProfiler.inl @@ -18,16 +18,11 @@ #include #include -#pragma optimize("", off) - -#include "../../../Gems/ImGui/External/ImGui/v1.82/imgui/imgui.h" namespace AZ { namespace Render { - inline u64 ImGuiCpuProfiler::ms_framesActive = 0; - namespace CpuProfilerImGuiHelper { // NOTE: Fix build error in case AZStd::thread_id is not of an arithmetic type, and instead a pointer @@ -145,14 +140,15 @@ namespace AZ { const auto flags = ImGuiTableFlags_Borders | ImGuiTableFlags_Sortable | ImGuiTableFlags_Resizable | ImGuiTableFlags_Reorderable; - if (ImGui::BeginTable("FunctionStatisticsTable", 5, flags)) + if (ImGui::BeginTable("FunctionStatisticsTable", 6, flags)) { // Table header setup ImGui::TableSetupColumn("Group"); ImGui::TableSetupColumn("Region"); ImGui::TableSetupColumn("MTPC (ms)"); ImGui::TableSetupColumn("Max (ms)"); - ImGui::TableSetupColumn("Invocations/frame"); + ImGui::TableSetupColumn("Invocations"); + ImGui::TableSetupColumn("Total (ms)"); ImGui::TableHeadersRow(); ImGui::TableNextColumn(); @@ -184,13 +180,16 @@ namespace AZ ImGui::Text("%.2f", CpuProfilerImGuiHelper::TicksToMs(statistics->m_maxTicks)); ImGui::TableNextColumn(); - ImGui::Text("%.1f", statistics->GetAverageInvocationsPerFrame()); + ImGui::Text("%ld", statistics->m_invocationsLastFrame); + ImGui::TableNextColumn(); + + ImGui::Text("%.2f", CpuProfilerImGuiHelper::TicksToMs(statistics->m_lastFrameTotalTicks)); const ImVec2 botRightBound = ImGui::GetItemRectMax(); ImGui::TableNextColumn(); // NOTE: we are manually checking the bounds rather than using ImGui::IsItemHovered + Begin/EndGroup because // ImGui reports incorrect bounds when using Begin/End group in the Tables API. - if (ImGui::IsMouseHoveringRect(topLeftBound, botRightBound, false)) + if (ImGui::IsWindowHovered() && ImGui::IsMouseHoveringRect(topLeftBound, botRightBound, false)) { ImGui::BeginTooltip(); ImGui::Text(statistics->GetExecutingThreadsLabel().c_str()); @@ -215,23 +214,32 @@ namespace AZ break; case (1): // Sort by region name AZStd::sort(m_tableData.begin(), m_tableData.end(),[ascending](const TableRow* lhs, const TableRow* rhs){ - return ascending ? lhs->m_regionName < rhs->m_regionName : lhs->m_regionName > rhs->m_regionName; + return ascending ? lhs->m_regionName < rhs->m_regionName + : lhs->m_regionName > rhs->m_regionName; }); break; case (2): // Sort by average time AZStd::sort(m_tableData.begin(), m_tableData.end(), [ascending](const TableRow* lhs, const TableRow* rhs){ return ascending ? lhs->m_runningAverageTicks < rhs->m_runningAverageTicks - : lhs->m_runningAverageTicks > rhs->m_runningAverageTicks; + : lhs->m_runningAverageTicks > rhs->m_runningAverageTicks; }); break; case (3): // Sort by max time AZStd::sort(m_tableData.begin(), m_tableData.end(), [ascending](const TableRow* lhs, const TableRow* rhs){ - return ascending ? lhs->m_maxTicks < rhs->m_maxTicks : lhs->m_maxTicks > rhs->m_maxTicks; + return ascending ? lhs->m_maxTicks < rhs->m_maxTicks + : lhs->m_maxTicks > rhs->m_maxTicks; }); break; case (4): // Sort by invocations AZStd::sort(m_tableData.begin(), m_tableData.end(), [ascending](const TableRow* lhs, const TableRow* rhs){ - return ascending ? lhs->m_invocations < rhs->m_invocations : lhs->m_invocations > rhs->m_invocations; + return ascending ? lhs->m_invocationsLastFrame < rhs->m_invocationsLastFrame + : lhs->m_invocationsLastFrame > rhs->m_invocationsLastFrame; + }); + break; + case (5): // Sort by total time + AZStd::sort(m_tableData.begin(), m_tableData.end(), [ascending](const TableRow* lhs, const TableRow* rhs){ + return ascending ? lhs->m_lastFrameTotalTicks < rhs->m_lastFrameTotalTicks + : lhs->m_lastFrameTotalTicks > rhs->m_lastFrameTotalTicks; }); break; } @@ -288,7 +296,6 @@ namespace AZ { m_tableData.clear(); m_groupRegionMap.clear(); - ImGuiCpuProfiler::ms_framesActive = 0; } DrawTable(); @@ -855,7 +862,14 @@ namespace AZ else { m_frameEndTicks.push_back(AZStd::GetTimeNowTicks()); - ImGuiCpuProfiler::ms_framesActive++; + + for (auto& [groupName, regionMap] : m_groupRegionMap) + { + for (auto& [regionName, row] : regionMap) + { + row.ResetPerFrameStatistics(); + } + } } } @@ -863,28 +877,34 @@ namespace AZ inline void TableRow::RecordRegion(const AZ::RHI::CachedTimeRegion& region, AZStd::thread_id threadId) { - m_invocations++; - const AZStd::sys_time_t deltaTime = AZStd::abs(region.m_endTick - region.m_startTick); + const AZStd::sys_time_t deltaTime = region.m_endTick - region.m_startTick; + + // Update per frame statistics + m_invocationsLastFrame++; + m_executingThreads.insert(threadId); + m_lastFrameTotalTicks += deltaTime; m_maxTicks = AZStd::max(m_maxTicks, deltaTime); - // Standard running average algorithm - const auto newMean = m_runningAverageTicks + aznumeric_cast((deltaTime - m_runningAverageTicks) * 1.0 / m_invocations); - m_runningAverageTicks = newMean; - - m_executingThreads.insert(threadId); + // Update aggregate statistics + m_runningAverageTicks = + aznumeric_cast((1.0 * (deltaTime + m_invocationsTotal * m_runningAverageTicks)) / (m_invocationsTotal + 1)); + ++m_invocationsTotal; } - inline double TableRow::GetAverageInvocationsPerFrame() const + inline void TableRow::ResetPerFrameStatistics() { - return 1.0 * m_invocations / ImGuiCpuProfiler::ms_framesActive; + m_invocationsLastFrame = 0; + m_executingThreads.clear(); + m_lastFrameTotalTicks = 0; + m_maxTicks = 0; } inline AZStd::string TableRow::GetExecutingThreadsLabel() const { - AZStd::string threadString; + auto threadString = AZStd::string::format("Executed in %zu threads\n", m_executingThreads.size()); for (const auto& threadId : m_executingThreads) { - threadString.append(CpuProfilerImGuiHelper::TextThreadId(threadId.m_id) + ", "); + threadString.append(CpuProfilerImGuiHelper::TextThreadId(threadId.m_id) + "\n"); } return threadString; }