[ATOM-15682] Initial CPU Visualizer widget (#1836)
* Visualizer: implement basic performance widget with controls Signed-off-by: Jacob Hilliard <jhlliar@amazon.com> * Visualizer: implement basic function statistics - Fix floating point bug in drawing logic - Implement color picker for regions - Aggregate invocations and average time across frames Signed-off-by: Jacob Hilliard <jhlliar@amazon.com> * Visualizer: drawing execution time labels Signed-off-by: Jacob Hilliard <jhlliar@amazon.com> * Visualizer: fix fstring type errors Signed-off-by: Jacob Hilliard <jhlliar@amazon.com> * Visualizer: fix remaining fstring errors Signed-off-by: Jacob Hilliard <jhlliar@amazon.com> * Visualizer: implement cursor-relative zooming Signed-off-by: Jacob Hilliard <jhlliar@amazon.com> * Visualizer: try to update AR status Signed-off-by: Jacob Hilliard <jhlliar@amazon.com> * Visualizer: address PR comments + cleanup Signed-off-by: Jacob Hilliard <jhlliar@amazon.com> * Visualizer: address more PR comments Signed-off-by: Jacob Hilliard <jhlliar@amazon.com> * Visualizer: address more PR comments Signed-off-by: Jacob Hilliard <jhlliar@amazon.com>
This commit is contained in:
@@ -229,6 +229,7 @@ namespace AZ
|
||||
{
|
||||
m_clearContainers = false;
|
||||
|
||||
m_stackLevel = 0;
|
||||
m_cachedTimeRegionMap.clear();
|
||||
m_timeRegionStack.clear();
|
||||
m_cachedTimeRegions.clear();
|
||||
|
||||
@@ -7,8 +7,12 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <Atom/RHI/CpuProfiler.h>
|
||||
#include <AzCore/Component/TickBus.h>
|
||||
#include <AzCore/Math/Random.h>
|
||||
|
||||
#include <Atom/RHI.Reflect/CpuTimingStatistics.h>
|
||||
#include <Atom/RHI/CpuProfiler.h>
|
||||
|
||||
|
||||
namespace AZ
|
||||
{
|
||||
@@ -19,24 +23,41 @@ namespace AZ
|
||||
|
||||
namespace Render
|
||||
{
|
||||
struct ThreadRegionEntry
|
||||
struct ThreadRegionEntry
|
||||
{
|
||||
AZStd::thread_id m_threadId;
|
||||
AZStd::sys_time_t m_startTick = 0;
|
||||
AZStd::sys_time_t m_endTick = 0;
|
||||
};
|
||||
|
||||
// Stores data about a region that is agreggated from all collected frames
|
||||
// Data collection can be toggled on and off through m_record.
|
||||
struct RegionStatistics
|
||||
{
|
||||
float CalcAverageTimeMs() const;
|
||||
void RecordRegion(const AZ::RHI::CachedTimeRegion& region);
|
||||
|
||||
bool m_draw = false;
|
||||
bool m_record = true;
|
||||
u64 m_invocations = 0;
|
||||
AZStd::sys_time_t m_totalTicks = 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
|
||||
//! resources are allocated in each heap.
|
||||
class ImGuiCpuProfiler
|
||||
: SystemTickBus::Handler
|
||||
{
|
||||
// Region Name -> Array of ThreadRegion entries
|
||||
using RegionEntryMap = AZStd::map<AZStd::string, AZStd::vector<ThreadRegionEntry>>;
|
||||
// Group Name -> RegionEntryMap
|
||||
using GroupRegionMap = AZStd::map<AZStd::string, RegionEntryMap>;
|
||||
|
||||
using TimeRegion = AZ::RHI::CachedTimeRegion;
|
||||
using GroupRegionName = AZ::RHI::CachedTimeRegion::GroupRegionName;
|
||||
|
||||
public:
|
||||
ImGuiCpuProfiler() = default;
|
||||
~ImGuiCpuProfiler() = default;
|
||||
@@ -44,7 +65,13 @@ namespace AZ
|
||||
//! Draws the provided Cpu statistics.
|
||||
void Draw(bool& keepDrawing, const AZ::RHI::CpuTimingStatistics& cpuTimingStatistics);
|
||||
|
||||
//! Draws the CPU profiling visualizer in a new window.
|
||||
void DrawVisualizer(bool& keepDrawing, const AZ::RHI::CpuTimingStatistics& currentCpuTimingStatistics);
|
||||
|
||||
private:
|
||||
static constexpr float RowHeight = 50.0;
|
||||
static constexpr int DefaultFramesToCollect = 50;
|
||||
|
||||
// Update the GroupRegionMap with the latest cached time regions
|
||||
void UpdateGroupRegionMap();
|
||||
|
||||
@@ -62,8 +89,73 @@ namespace AZ
|
||||
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
|
||||
void DrawBlock(const TimeRegion& block, u64 targetRow);
|
||||
|
||||
// Draw horizontal lines between threads in the timeline
|
||||
void DrawThreadSeparator(u64 threadBoundary, u64 maxDepth);
|
||||
|
||||
// Draw the "Thread XXXXX" label onto the viewport
|
||||
void DrawThreadLabel(u64 baseRow, AZStd::thread_id threadId);
|
||||
|
||||
// Draws all active function statistics windows
|
||||
void DrawRegionStatistics();
|
||||
|
||||
// Draw the vertical lines separating frames in the timeline
|
||||
void DrawFrameBoundaries();
|
||||
|
||||
// Draw the ruler with frame time labels
|
||||
void DrawRuler();
|
||||
|
||||
// Converts raw ticks to a pixel value suitable to give to ImDrawList, handles window scrolling
|
||||
float ConvertTickToPixelSpace(AZStd::sys_time_t tick) const;
|
||||
|
||||
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
|
||||
ImU32 GetBlockColor(const TimeRegion& block);
|
||||
|
||||
// System tick bus overrides
|
||||
virtual void OnSystemTick() override;
|
||||
|
||||
// Visualizer state
|
||||
|
||||
bool m_showVisualizer = false;
|
||||
|
||||
int m_framesToCollect = DefaultFramesToCollect;
|
||||
|
||||
// Tally of the number of saved profiling events so far
|
||||
u64 m_savedRegionCount = 0;
|
||||
|
||||
// Viewport tick bounds, these are used to convert tick space -> screen space and cull so we only draw onscreen objects
|
||||
AZStd::sys_time_t m_viewportStartTick;
|
||||
AZStd::sys_time_t m_viewportEndTick;
|
||||
|
||||
// Map to store each thread's TimeRegions, individual vectors are sorted by start tick
|
||||
AZStd::unordered_map<AZStd::thread_id, AZStd::vector<TimeRegion>> m_savedData;
|
||||
|
||||
// Region color cache
|
||||
AZStd::unordered_map<const GroupRegionName*, ImVec4> m_regionColorMap;
|
||||
|
||||
// Tracks the frame boundaries
|
||||
AZStd::vector<AZStd::sys_time_t> m_frameEndTicks = { INT64_MIN };
|
||||
|
||||
// Main data structure for storing function statistics to be shown in the popup windows.
|
||||
// For now we default allocate for all regions on the first render frame and then use RegionStatistics.m_draw to determine
|
||||
// if we should draw the window or not. FIXME(ATOM-15948) this should be changed once RegionStatistics gets heavier.
|
||||
AZStd::unordered_map<const GroupRegionName*, RegionStatistics> m_regionStatisticsMap;
|
||||
};
|
||||
} // namespace Render
|
||||
}
|
||||
} // namespace AZ
|
||||
|
||||
#include "ImGuiCpuProfiler.inl"
|
||||
|
||||
@@ -6,10 +6,16 @@
|
||||
*/
|
||||
|
||||
#include <Atom/Feature/Utils/ProfilingCaptureBus.h>
|
||||
#include <Atom/RHI.Reflect/CpuTimingStatistics.h>
|
||||
#include <Atom/RHI/CpuProfiler.h>
|
||||
#include <Atom/RPI.Public/RPISystemInterface.h>
|
||||
|
||||
#include <AzCore/IO/Path/Path_fwd.h>
|
||||
#include <AzCore/std/time.h>
|
||||
#include <AzCore/std/containers/map.h>
|
||||
#include <AzCore/std/containers/set.h>
|
||||
#include <AzCore/std/sort.h>
|
||||
#include <AzCore/std/time.h>
|
||||
|
||||
|
||||
namespace AZ
|
||||
{
|
||||
@@ -38,14 +44,14 @@ namespace AZ
|
||||
AZ_Assert(ticksPerSecond >= 1000, "Error in converting ticks to ms, expected ticksPerSecond >= 1000");
|
||||
return static_cast<float>((ticks * 1000) / (ticksPerSecond / 1000)) / 1000.0f;
|
||||
}
|
||||
}
|
||||
} // namespace CpuProfilerImGuiHelper
|
||||
|
||||
inline void ImGuiCpuProfiler::Draw(bool& keepDrawing, const AZ::RHI::CpuTimingStatistics& currentCpuTimingStatistics)
|
||||
{
|
||||
// Cache the value to detect if it was changed by ImGui(user pressed 'x')
|
||||
const bool cachedShowCpuProfiler = keepDrawing;
|
||||
|
||||
const ImVec2 windowSize(640.0f, 480.0f);
|
||||
const ImVec2 windowSize(900.0f, 600.0f);
|
||||
ImGui::SetNextWindowSize(windowSize, ImGuiCond_Once);
|
||||
bool captureToFile = false;
|
||||
if (ImGui::Begin("Cpu Profiler", &keepDrawing, ImGuiWindowFlags_None))
|
||||
@@ -114,9 +120,9 @@ namespace AZ
|
||||
}
|
||||
};
|
||||
|
||||
const auto ShowRegionRow = [ticksPerSecond, &DrawRegionHoverMarker, &ShowTimeInMs](const char* regionLabel,
|
||||
AZStd::vector<ThreadRegionEntry> regions,
|
||||
AZStd::sys_time_t duration)
|
||||
const auto ShowRegionRow =
|
||||
[ticksPerSecond, &DrawRegionHoverMarker,
|
||||
&ShowTimeInMs](const char* regionLabel, AZStd::vector<ThreadRegionEntry> regions, AZStd::sys_time_t duration)
|
||||
{
|
||||
// Draw the region label
|
||||
ImGui::Text(regionLabel);
|
||||
@@ -142,14 +148,15 @@ namespace AZ
|
||||
ImGui::NextColumn();
|
||||
|
||||
// Draw the time labels (max and then total)
|
||||
const AZStd::string timeLabel =
|
||||
AZStd::string::format("%.2f ms max, %.2f ms total",
|
||||
CpuProfilerImGuiHelper::TicksToMs(duration),
|
||||
const AZStd::string timeLabel = AZStd::string::format(
|
||||
"%.2f ms max, %.2f ms total", CpuProfilerImGuiHelper::TicksToMs(duration),
|
||||
CpuProfilerImGuiHelper::TicksToMs(totalTime));
|
||||
ImGui::Text(timeLabel.c_str());
|
||||
ImGui::NextColumn();
|
||||
};
|
||||
|
||||
ImGui::Checkbox("Enable Visualizer", &m_showVisualizer);
|
||||
|
||||
// Set column settings.
|
||||
ImGui::Columns(2, "view", false);
|
||||
ImGui::SetColumnWidth(0, 660.0f);
|
||||
@@ -216,8 +223,8 @@ namespace AZ
|
||||
char resolvedPath[AZ::IO::MaxPathLength];
|
||||
AZ::IO::FileIOBase::GetInstance()->ResolvePath(frameDataFilePath.c_str(), resolvedPath, AZ::IO::MaxPathLength);
|
||||
m_lastCapturedFilePath = resolvedPath;
|
||||
AZ::Render::ProfilingCaptureRequestBus::Broadcast(&AZ::Render::ProfilingCaptureRequestBus::Events::CaptureCpuProfilingStatistics,
|
||||
frameDataFilePath);
|
||||
AZ::Render::ProfilingCaptureRequestBus::Broadcast(
|
||||
&AZ::Render::ProfilingCaptureRequestBus::Events::CaptureCpuProfilingStatistics, frameDataFilePath);
|
||||
}
|
||||
|
||||
// Toggle if the bool isn't the same as the cached value
|
||||
@@ -225,6 +232,11 @@ namespace AZ
|
||||
{
|
||||
AZ::RHI::CpuProfiler::Get()->SetProfilerEnabled(keepDrawing);
|
||||
}
|
||||
|
||||
if (m_showVisualizer)
|
||||
{
|
||||
DrawVisualizer(m_showVisualizer, currentCpuTimingStatistics);
|
||||
}
|
||||
}
|
||||
|
||||
inline void ImGuiCpuProfiler::UpdateGroupRegionMap()
|
||||
@@ -251,5 +263,519 @@ namespace AZ
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -- CPU Visualizer --
|
||||
inline void ImGuiCpuProfiler::DrawVisualizer(bool& keepDrawing, const AZ::RHI::CpuTimingStatistics& currentCpuTimingStatistics)
|
||||
{
|
||||
ImGui::SetNextWindowSize({ 900, 600 }, ImGuiCond_Once);
|
||||
if (ImGui::Begin("CPU Visualizer", &keepDrawing, ImGuiWindowFlags_None))
|
||||
{
|
||||
// Get the instrumentation data for the last frame if active
|
||||
if (!m_paused && m_groupRegionMap.size() != 0)
|
||||
{
|
||||
CollectFrameData(); // Also updates viewport bounds
|
||||
|
||||
CullFrameData(currentCpuTimingStatistics); // Trim data if necessary
|
||||
|
||||
if (!SystemTickBus::Handler::BusIsConnected())
|
||||
{
|
||||
SystemTickBus::Handler::BusConnect();
|
||||
}
|
||||
}
|
||||
|
||||
// Options & Statistics
|
||||
if (ImGui::BeginChild("Options and Statistics", { 0, 0 }, true))
|
||||
{
|
||||
ImGui::Columns(3, "Options", true);
|
||||
ImGui::Text("Frames To Collect:");
|
||||
ImGui::SliderInt("", &m_framesToCollect, 10, 100, "%d", ImGuiSliderFlags_AlwaysClamp | ImGuiSliderFlags_Logarithmic);
|
||||
|
||||
ImGui::NextColumn();
|
||||
|
||||
ImGui::Text("Viewport width: %.3f ms", CpuProfilerImGuiHelper::TicksToMs(GetViewportTickWidth()));
|
||||
ImGui::Text("Ticks [%lld , %lld]", m_viewportStartTick, m_viewportEndTick);
|
||||
ImGui::Text("Recording %ld threads", RHI::CpuProfiler::Get()->GetTimeRegionMap().size());
|
||||
ImGui::Text("%llu profiling events saved", m_savedRegionCount);
|
||||
|
||||
ImGui::NextColumn();
|
||||
|
||||
ImGui::TextWrapped(
|
||||
"Hold the right mouse button to move around. Zoom by scrolling the mouse wheel while holding <ctrl>.");
|
||||
}
|
||||
|
||||
|
||||
ImGui::Columns(1, "RulerColumn", true);
|
||||
|
||||
// Ruler
|
||||
if (ImGui::BeginChild("Ruler", { 0, 30 }, true, ImGuiWindowFlags_NoNavFocus))
|
||||
{
|
||||
DrawRuler();
|
||||
}
|
||||
ImGui::EndChild();
|
||||
|
||||
|
||||
ImGui::Columns(1, "TimelineColumn", true);
|
||||
|
||||
// Timeline
|
||||
if (ImGui::BeginChild(
|
||||
"Timeline", { 0, 0 }, true, ImGuiWindowFlags_AlwaysVerticalScrollbar | ImGuiWindowFlags_NoScrollWithMouse))
|
||||
{
|
||||
// Find the next frame boundary after the viewport's right bound and draw until that tick
|
||||
auto nextFrameBoundaryItr = AZStd::lower_bound(m_frameEndTicks.begin(), m_frameEndTicks.end(), m_viewportEndTick);
|
||||
if (nextFrameBoundaryItr == m_frameEndTicks.end() &&
|
||||
m_frameEndTicks.size() != 0) // lower_bound returns end() if not found
|
||||
{
|
||||
nextFrameBoundaryItr--;
|
||||
}
|
||||
const AZStd::sys_time_t nextFrameBoundary = *nextFrameBoundaryItr;
|
||||
|
||||
// Find the start tick of the leftmost frame, which may be offscreen.
|
||||
auto startTickItr = AZStd::lower_bound(m_frameEndTicks.begin(), m_frameEndTicks.end(), m_viewportStartTick);
|
||||
if (startTickItr != m_frameEndTicks.begin())
|
||||
{
|
||||
startTickItr--;
|
||||
}
|
||||
|
||||
// Main draw loop
|
||||
u64 baseRow = 0;
|
||||
for (const auto& [currentThreadId, singleThreadData] : m_savedData)
|
||||
{
|
||||
// Find the first TimeRegion that we should draw
|
||||
auto regionItr = AZStd::lower_bound(
|
||||
singleThreadData.begin(), singleThreadData.end(), *startTickItr,
|
||||
[](const TimeRegion& wrapper, AZStd::sys_time_t target)
|
||||
{
|
||||
return wrapper.m_startTick < target;
|
||||
});
|
||||
|
||||
// Draw all of the blocks for a given thread/row
|
||||
u64 maxDepth = 0;
|
||||
while (regionItr != singleThreadData.end())
|
||||
{
|
||||
const TimeRegion& region = *regionItr;
|
||||
|
||||
// Early out if we have drawn all the onscreen regions
|
||||
if (region.m_startTick > nextFrameBoundary)
|
||||
{
|
||||
break;
|
||||
}
|
||||
u64 targetRow = region.m_stackDepth + baseRow;
|
||||
maxDepth = AZStd::max(aznumeric_cast<u64>(region.m_stackDepth), maxDepth);
|
||||
|
||||
DrawBlock(region, targetRow);
|
||||
|
||||
regionItr++;
|
||||
}
|
||||
|
||||
// Draw UI details
|
||||
DrawThreadLabel(baseRow, currentThreadId);
|
||||
DrawThreadSeparator(baseRow, maxDepth);
|
||||
|
||||
baseRow += maxDepth + 1; // Next draw loop should start one row down
|
||||
}
|
||||
|
||||
DrawRegionStatistics();
|
||||
DrawFrameBoundaries();
|
||||
|
||||
// Draw an invisible button to capture inputs
|
||||
ImGui::InvisibleButton("Timeline Input", { ImGui::GetWindowContentRegionWidth(), baseRow * RowHeight });
|
||||
|
||||
// Controls
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
if (ImGui::IsWindowFocused() && ImGui::IsItemHovered())
|
||||
{
|
||||
io.WantCaptureMouse = true;
|
||||
if (ImGui::IsMouseDragging(ImGuiMouseButton_Right)) // Scrolling
|
||||
{
|
||||
auto [deltaX, deltaY] = io.MouseDelta;
|
||||
if (deltaX != 0 || deltaY != 0)
|
||||
{
|
||||
// We want to maintain uniformity in scrolling (a click and drag should leave the cursor at the same spot
|
||||
// relative to the objects on screen)
|
||||
const float pixelDeltaNormalized = deltaX / ImGui::GetWindowWidth();
|
||||
auto tickDelta = aznumeric_cast<AZStd::sys_time_t>(-1 * pixelDeltaNormalized * GetViewportTickWidth());
|
||||
m_viewportStartTick += tickDelta;
|
||||
m_viewportEndTick += tickDelta;
|
||||
|
||||
ImGui::SetScrollY(ImGui::GetScrollY() + deltaY * -1);
|
||||
}
|
||||
}
|
||||
else if (io.MouseWheel != 0 && io.KeyCtrl) // Zooming
|
||||
{
|
||||
// We want zooming to be relative to the mouse's current position
|
||||
const float mouseVel = io.MouseWheel;
|
||||
const float mouseX = ImGui::GetMousePos().x;
|
||||
|
||||
// Find the normalized position of the cursor relative to the window
|
||||
const float percentWindow = (mouseX - ImGui::GetWindowPos().x) / ImGui::GetWindowWidth();
|
||||
|
||||
const auto overallTickDelta = aznumeric_cast<AZStd::sys_time_t>(0.05 * io.MouseWheel * GetViewportTickWidth());
|
||||
|
||||
// Split the overall delta between the two bounds depending on mouse pos
|
||||
const auto newStartTick = m_viewportStartTick + aznumeric_cast<AZStd::sys_time_t>(percentWindow * overallTickDelta);
|
||||
const auto newEndTick = m_viewportEndTick - aznumeric_cast<AZStd::sys_time_t>((1-percentWindow) * overallTickDelta);
|
||||
|
||||
// Avoid zooming too much, start tick should always be less than end tick
|
||||
if (newStartTick < newEndTick)
|
||||
{
|
||||
m_viewportStartTick = newStartTick;
|
||||
m_viewportEndTick = newEndTick;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ImGui::EndChild();
|
||||
}
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
inline void ImGuiCpuProfiler::CollectFrameData()
|
||||
{
|
||||
const RHI::CpuProfiler::TimeRegionMap& timeRegionMap = RHI::CpuProfiler::Get()->GetTimeRegionMap();
|
||||
|
||||
m_viewportStartTick = INT64_MAX;
|
||||
m_viewportEndTick = INT64_MIN;
|
||||
|
||||
// Iterate through the entire TimeRegionMap and copy the data since it will get deleted on the next frame
|
||||
for (const auto& [threadId, singleThreadRegionMap] : timeRegionMap)
|
||||
{
|
||||
// The profiler can sometime return threads without any profiling events when dropping threads, FIXME(ATOM-15949)
|
||||
if (singleThreadRegionMap.size() == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Now focus on just the data for the current thread
|
||||
AZStd::vector<TimeRegion> newData;
|
||||
newData.reserve(singleThreadRegionMap.size()); // Avoids reallocation in the normal case when each region only has one invocation
|
||||
for (const auto& [regionName, regionVec] : singleThreadRegionMap)
|
||||
{
|
||||
for (const TimeRegion& region : regionVec)
|
||||
{
|
||||
newData.push_back(region); // Copies
|
||||
|
||||
// Update running statistics if we want to record this region's data
|
||||
if (m_regionStatisticsMap[region.m_groupRegionName].m_record)
|
||||
{
|
||||
m_regionStatisticsMap[region.m_groupRegionName].RecordRegion(region);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sorting by start tick allows us to speed up some other processes (ex. finding the first block to draw)
|
||||
// since we can binary search by start tick.
|
||||
AZStd::sort(
|
||||
newData.begin(), newData.end(),
|
||||
[](const TimeRegion& lhs, const TimeRegion& rhs)
|
||||
{
|
||||
return lhs.m_startTick < rhs.m_startTick;
|
||||
});
|
||||
|
||||
// Use the latest frame's data as the new bounds of the viewport
|
||||
m_viewportStartTick = AZStd::min(newData.front().m_startTick, m_viewportStartTick);
|
||||
m_viewportEndTick = AZStd::max(newData.back().m_endTick, m_viewportEndTick);
|
||||
|
||||
m_savedRegionCount += newData.size();
|
||||
|
||||
// Move onto the end of the current thread's saved data, sorted order maintained
|
||||
AZStd::vector<TimeRegion>& savedDataVec = m_savedData[threadId];
|
||||
savedDataVec.insert(
|
||||
savedDataVec.end(), AZStd::make_move_iterator(newData.begin()), AZStd::make_move_iterator(newData.end()));
|
||||
}
|
||||
}
|
||||
|
||||
inline void ImGuiCpuProfiler::CullFrameData(const AZ::RHI::CpuTimingStatistics& currentCpuTimingStatistics)
|
||||
{
|
||||
const AZStd::sys_time_t frameToFrameTime = currentCpuTimingStatistics.m_frameToFrameTime;
|
||||
const AZStd::sys_time_t deleteBeforeTick = AZStd::GetTimeNowTicks() - frameToFrameTime * m_framesToCollect;
|
||||
|
||||
// Remove old frame boundary data
|
||||
auto firstBoundaryToKeepItr = AZStd::upper_bound(m_frameEndTicks.begin(), m_frameEndTicks.end(), deleteBeforeTick);
|
||||
m_frameEndTicks.erase(m_frameEndTicks.begin(), firstBoundaryToKeepItr);
|
||||
|
||||
// Remove old region data for each thread
|
||||
for (auto& [threadId, savedRegions] : m_savedData)
|
||||
{
|
||||
AZStd::size_t sizeBeforeRemove = savedRegions.size();
|
||||
|
||||
auto firstRegionToKeep = AZStd::lower_bound(
|
||||
savedRegions.begin(), savedRegions.end(), deleteBeforeTick,
|
||||
[](const TimeRegion& region, AZStd::sys_time_t target)
|
||||
{
|
||||
return region.m_startTick < target;
|
||||
});
|
||||
savedRegions.erase(savedRegions.begin(), firstRegionToKeep);
|
||||
|
||||
m_savedRegionCount -= sizeBeforeRemove - savedRegions.size();
|
||||
}
|
||||
}
|
||||
|
||||
inline void ImGuiCpuProfiler::DrawBlock(const TimeRegion& block, u64 targetRow)
|
||||
{
|
||||
float wy = ImGui::GetWindowPos().y - ImGui::GetScrollY();
|
||||
|
||||
ImDrawList* drawList = ImGui::GetWindowDrawList();
|
||||
|
||||
const float startPixel = ConvertTickToPixelSpace(block.m_startTick);
|
||||
const float endPixel = ConvertTickToPixelSpace(block.m_endTick);
|
||||
|
||||
const ImVec2 startPoint = { startPixel, wy + targetRow * RowHeight };
|
||||
const ImVec2 endPoint = { endPixel, wy + targetRow * RowHeight + 40 };
|
||||
|
||||
const ImU32 blockColor = GetBlockColor(block);
|
||||
|
||||
drawList->AddRectFilled(startPoint, endPoint, blockColor, 0);
|
||||
|
||||
// Draw the region name if possible
|
||||
// If the block's current width is too small, we skip drawing the label.
|
||||
const float regionPixelWidth = endPixel - startPixel;
|
||||
const float maxCharWidth = ImGui::CalcTextSize("M").x; // M is usually the largest character in most fonts (see CSS em)
|
||||
if (regionPixelWidth > maxCharWidth) // We can draw at least one character
|
||||
{
|
||||
const AZStd::string label =
|
||||
AZStd::string::format("%s/ %s", block.m_groupRegionName->m_groupName, block.m_groupRegionName->m_regionName);
|
||||
const float textWidth = ImGui::CalcTextSize(label.c_str()).x;
|
||||
|
||||
if (regionPixelWidth < textWidth) // Not enough space in the block to draw the whole name, draw clipped text.
|
||||
{
|
||||
// clipRect appears to only clip when a character is fully outside of its bounds which can lead to overflow
|
||||
// for now subtract the width of a character
|
||||
const ImVec4 clipRect = { startPoint.x, startPoint.y, endPoint.x - maxCharWidth, endPoint.y };
|
||||
const float fontSize = ImGui::GetFont()->FontSize;
|
||||
|
||||
ImGui::GetFont()->RenderText(drawList, fontSize, startPoint, IM_COL32_WHITE, clipRect, label.c_str(), 0);
|
||||
}
|
||||
else // We have enough space to draw the entire label, draw and center text.
|
||||
{
|
||||
const float remainingWidth = regionPixelWidth - textWidth;
|
||||
const float offset = remainingWidth * .5;
|
||||
|
||||
drawList->AddText({ startPoint.x + offset, startPoint.y }, IM_COL32_WHITE, label.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
// Tooltip and block highlighting
|
||||
if (ImGui::IsMouseHoveringRect(startPoint, endPoint) && ImGui::IsWindowHovered())
|
||||
{
|
||||
// Open function statistics map on click
|
||||
if (ImGui::IsMouseClicked(ImGuiMouseButton_Left))
|
||||
{
|
||||
const GroupRegionName* key = block.m_groupRegionName;
|
||||
m_regionStatisticsMap[key].m_draw = true;
|
||||
}
|
||||
|
||||
// Hovering outline
|
||||
drawList->AddRect(startPoint, endPoint, ImGui::GetColorU32({ 1, 1, 1, 1 }), 0.0, 0, 1.5);
|
||||
|
||||
ImGui::BeginTooltip();
|
||||
ImGui::Text("%s::%s", block.m_groupRegionName->m_groupName, block.m_groupRegionName->m_regionName);
|
||||
ImGui::Text("Execution time: %.3f ms", CpuProfilerImGuiHelper::TicksToMs(block.m_endTick - block.m_startTick));
|
||||
ImGui::Text("Ticks %lld => %lld", block.m_startTick, block.m_endTick);
|
||||
ImGui::EndTooltip();
|
||||
}
|
||||
}
|
||||
|
||||
inline ImU32 ImGuiCpuProfiler::GetBlockColor(const TimeRegion& block)
|
||||
{
|
||||
// Use the GroupRegionName pointer a key into the cache, equal regions will have equal pointers
|
||||
const GroupRegionName* key = block.m_groupRegionName;
|
||||
if (m_regionColorMap.contains(key)) // Cache hit
|
||||
{
|
||||
return ImGui::GetColorU32(m_regionColorMap[key]);
|
||||
}
|
||||
|
||||
// Cache miss, generate a new random color
|
||||
AZ::SimpleLcgRandom rand(aznumeric_cast<u64>(AZStd::GetTimeNowTicks()));
|
||||
const float r = AZStd::clamp(rand.GetRandomFloat(), .1f, .9f);
|
||||
const float g = AZStd::clamp(rand.GetRandomFloat(), .1f, .9f);
|
||||
const float b = AZStd::clamp(rand.GetRandomFloat(), .1f, .9f);
|
||||
const ImVec4 randomColor = {r, g, b, .8};
|
||||
m_regionColorMap.emplace(key, randomColor);
|
||||
return ImGui::GetColorU32(randomColor);
|
||||
}
|
||||
|
||||
inline void ImGuiCpuProfiler::DrawThreadSeparator(u64 baseRow, u64 maxDepth)
|
||||
{
|
||||
const ImU32 red = ImGui::GetColorU32({ 1, 0, 0, 1 });
|
||||
|
||||
auto [wx, wy] = ImGui::GetWindowPos();
|
||||
wy -= ImGui::GetScrollY();
|
||||
const float windowWidth = ImGui::GetWindowWidth();
|
||||
const float boundaryY = wy + (baseRow + maxDepth + 1) * RowHeight - 5;
|
||||
|
||||
ImGui::GetWindowDrawList()->AddLine({ wx, boundaryY }, { wx + windowWidth, boundaryY }, red, 2.0f);
|
||||
}
|
||||
|
||||
inline void ImGuiCpuProfiler::DrawThreadLabel(u64 baseRow, AZStd::thread_id threadId)
|
||||
{
|
||||
auto [wx, wy] = ImGui::GetWindowPos();
|
||||
wy -= ImGui::GetScrollY();
|
||||
const AZStd::string threadIdText = AZStd::string::format("Thread %zu", static_cast<size_t>(threadId.m_id));
|
||||
|
||||
ImGui::GetWindowDrawList()->AddText({ wx + 10, wy + baseRow * RowHeight + 5 }, IM_COL32_WHITE, threadIdText.c_str());
|
||||
}
|
||||
|
||||
inline void ImGuiCpuProfiler::DrawRegionStatistics()
|
||||
{
|
||||
for (auto& [groupRegionName, stat] : m_regionStatisticsMap)
|
||||
{
|
||||
if (stat.m_draw)
|
||||
{
|
||||
ImGui::SetNextWindowSize({300, 340}, ImGuiCond_FirstUseEver);
|
||||
ImGui::Begin(groupRegionName->m_regionName, &stat.m_draw, 0);
|
||||
|
||||
if (ImGui::Button(stat.m_record ? "Pause" : "Resume"))
|
||||
{
|
||||
stat.m_record = !stat.m_record;
|
||||
}
|
||||
|
||||
ImGui::Text("Invocations: %llu", stat.m_invocations);
|
||||
ImGui::Text("Average time: %.3f ms", stat.CalcAverageTimeMs());
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
ImGui::ColorPicker4("Region color", &m_regionColorMap[groupRegionName].x);
|
||||
ImGui::End();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inline void ImGuiCpuProfiler::DrawFrameBoundaries()
|
||||
{
|
||||
ImDrawList* drawList = ImGui::GetWindowDrawList();
|
||||
|
||||
const float wy = ImGui::GetWindowPos().y;
|
||||
const float windowHeight = ImGui::GetWindowHeight();
|
||||
const ImU32 red = ImGui::GetColorU32({ 1, 0, 0, 1 });
|
||||
|
||||
// End ticks are sorted in increasing order, find the first frame bound to draw
|
||||
auto endTickItr = AZStd::lower_bound(m_frameEndTicks.begin(), m_frameEndTicks.end(), m_viewportStartTick);
|
||||
|
||||
while (endTickItr != m_frameEndTicks.end() && *endTickItr < m_viewportEndTick)
|
||||
{
|
||||
const float horizontalPixel = ConvertTickToPixelSpace(*endTickItr);
|
||||
drawList->AddLine({ horizontalPixel, wy }, { horizontalPixel, wy + windowHeight }, red);
|
||||
endTickItr++;
|
||||
}
|
||||
}
|
||||
|
||||
inline void ImGuiCpuProfiler::DrawRuler()
|
||||
{
|
||||
// Use a pair of iterators to go through all saved frame boundaries and draw ruler lines
|
||||
auto lastFrameBoundaryItr = AZStd::lower_bound(m_frameEndTicks.begin(), m_frameEndTicks.end(), m_viewportStartTick);
|
||||
auto nextFrameBoundaryItr = lastFrameBoundaryItr;
|
||||
if (lastFrameBoundaryItr != m_frameEndTicks.begin())
|
||||
{
|
||||
lastFrameBoundaryItr--;
|
||||
}
|
||||
|
||||
const auto [wx, wy] = ImGui::GetWindowPos();
|
||||
ImDrawList* drawList = ImGui::GetWindowDrawList();
|
||||
|
||||
while (nextFrameBoundaryItr != m_frameEndTicks.end())
|
||||
{
|
||||
const AZStd::sys_time_t lastFrameBoundaryTick = *lastFrameBoundaryItr;
|
||||
const AZStd::sys_time_t nextFrameBoundaryTick = *nextFrameBoundaryItr;
|
||||
if (lastFrameBoundaryTick > m_viewportEndTick)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
const float lastFrameBoundaryPixel = ConvertTickToPixelSpace(lastFrameBoundaryTick);
|
||||
const float nextFrameBoundaryPixel = ConvertTickToPixelSpace(nextFrameBoundaryTick);
|
||||
|
||||
const AZStd::string label =
|
||||
AZStd::string::format("%.2f ms", CpuProfilerImGuiHelper::TicksToMs(nextFrameBoundaryTick - lastFrameBoundaryTick));
|
||||
const float labelWidth = ImGui::CalcTextSize(label.c_str()).x;
|
||||
|
||||
// The label can fit between the two boundaries, center it and draw
|
||||
if (labelWidth <= nextFrameBoundaryPixel - lastFrameBoundaryPixel)
|
||||
{
|
||||
const float offset = (nextFrameBoundaryPixel - lastFrameBoundaryPixel - labelWidth) /2;
|
||||
const float textBeginPixel = lastFrameBoundaryPixel + offset;
|
||||
const float textEndPixel = textBeginPixel + labelWidth;
|
||||
|
||||
// Execution time label
|
||||
drawList->AddText({ textBeginPixel, wy + ImGui::GetWindowHeight() / 4 }, IM_COL32_WHITE, label.c_str());
|
||||
|
||||
// Left side
|
||||
drawList->AddLine(
|
||||
{ lastFrameBoundaryPixel, wy + ImGui::GetWindowHeight() / 2 },
|
||||
{ textBeginPixel - 5, wy + ImGui::GetWindowHeight() / 2},
|
||||
IM_COL32_WHITE);
|
||||
|
||||
// Right side
|
||||
drawList->AddLine(
|
||||
{ textEndPixel, wy + ImGui::GetWindowHeight()/2 },
|
||||
{ nextFrameBoundaryPixel, wy + ImGui::GetWindowHeight()/2 },
|
||||
IM_COL32_WHITE);
|
||||
}
|
||||
else // Cannot fit inside, just draw a line between the two boundaries
|
||||
{
|
||||
drawList->AddLine(
|
||||
{ lastFrameBoundaryPixel, wy + ImGui::GetWindowHeight() / 2 },
|
||||
{ nextFrameBoundaryPixel, wy + ImGui::GetWindowHeight() / 2 },
|
||||
IM_COL32_WHITE);
|
||||
}
|
||||
|
||||
// Left bound
|
||||
drawList->AddLine(
|
||||
{ lastFrameBoundaryPixel, wy },
|
||||
{ lastFrameBoundaryPixel, wy + ImGui::GetWindowHeight() },
|
||||
IM_COL32_WHITE);
|
||||
|
||||
// Right bound
|
||||
drawList->AddLine(
|
||||
{ nextFrameBoundaryPixel, wy },
|
||||
{ nextFrameBoundaryPixel, wy + ImGui::GetWindowHeight() },
|
||||
IM_COL32_WHITE);
|
||||
|
||||
lastFrameBoundaryItr = nextFrameBoundaryItr;
|
||||
nextFrameBoundaryItr++;
|
||||
}
|
||||
}
|
||||
|
||||
inline AZStd::sys_time_t ImGuiCpuProfiler::GetViewportTickWidth() const
|
||||
{
|
||||
return m_viewportEndTick - m_viewportStartTick;
|
||||
}
|
||||
|
||||
inline float ImGuiCpuProfiler::ConvertTickToPixelSpace(AZStd::sys_time_t tick) const
|
||||
{
|
||||
const float wx = ImGui::GetWindowPos().x;
|
||||
const float tickSpaceShifted = aznumeric_cast<float>(tick - m_viewportStartTick); // This will be close to zero, so FP inaccuracy should not be too bad
|
||||
const float tickSpaceNormalized = tickSpaceShifted / GetViewportTickWidth();
|
||||
const float pixelSpace = tickSpaceNormalized * ImGui::GetWindowWidth() + wx;
|
||||
return pixelSpace;
|
||||
}
|
||||
|
||||
// System tick bus overrides
|
||||
inline void ImGuiCpuProfiler::OnSystemTick()
|
||||
{
|
||||
m_frameEndTicks.push_back(AZStd::GetTimeNowTicks());
|
||||
|
||||
if (!m_showVisualizer || m_paused)
|
||||
{
|
||||
SystemTickBus::Handler::BusDisconnect();
|
||||
}
|
||||
}
|
||||
|
||||
// ----- RegionStatistics implementation -----
|
||||
|
||||
inline float RegionStatistics::CalcAverageTimeMs() const
|
||||
{
|
||||
if (m_invocations == 0)
|
||||
{
|
||||
return 0.0;
|
||||
}
|
||||
const double averageTicks = aznumeric_cast<double>(m_totalTicks) / m_invocations;
|
||||
return CpuProfilerImGuiHelper::TicksToMs(aznumeric_cast<AZStd::sys_time_t>(averageTicks));
|
||||
}
|
||||
|
||||
inline void RegionStatistics::RecordRegion(const AZ::RHI::CachedTimeRegion& region)
|
||||
{
|
||||
m_invocations++;
|
||||
m_totalTicks += region.m_endTick - region.m_startTick;
|
||||
}
|
||||
} // namespace Render
|
||||
} // namespace AZ
|
||||
|
||||
Reference in New Issue
Block a user