Visualizer: Implement total time + cleanup

Signed-off-by: Jacob Hilliard <jhlliar@amazon.com>
This commit is contained in:
Jacob Hilliard
2021-07-21 16:14:08 -07:00
parent bd00867fe6
commit 11a001946f
2 changed files with 114 additions and 89 deletions
@@ -10,7 +10,6 @@
#include <AzCore/Component/TickBus.h>
#include <AzCore/Math/Random.h>
#include <AzCore/std/containers/set.h>
#include <Atom/RHI.Reflect/CpuTimingStatistics.h>
#include <Atom/RHI/CpuProfiler.h>
@@ -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<AZStd::thread_id> 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<AZStd::string, TableRow>;
// Group Name -> RegionEntryMap
using GroupRegionMap = AZStd::map<AZStd::string, RegionEntryMap>;
// Region Name -> statistical view row data
using RegionRowMap = AZStd::map<AZStd::string, TableRow>;
// Group Name -> RegionRowMap
using GroupRegionMap = AZStd::map<AZStd::string, RegionRowMap>;
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<TableRow*> 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<TableRow*> 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
@@ -18,16 +18,11 @@
#include <AzCore/std/sort.h>
#include <AzCore/std/time.h>
#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<AZStd::sys_time_t>((deltaTime - m_runningAverageTicks) * 1.0 / m_invocations);
m_runningAverageTicks = newMean;
m_executingThreads.insert(threadId);
// Update aggregate statistics
m_runningAverageTicks =
aznumeric_cast<AZStd::sys_time_t>((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;
}