[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:
Jacob Hilliard
2021-07-08 09:52:04 -07:00
committed by GitHub
parent 82d2fb9244
commit 08c2796760
3 changed files with 635 additions and 16 deletions
@@ -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