Integrate visualizer widget into ASV CPU profiler (#2106)
* Visualizer: initial refactor to toggle behavior Signed-off-by: Jacob Hilliard <jhlliar@amazon.com> * Visualizer: improve window integration Signed-off-by: Jacob Hilliard <jhlliar@amazon.com> * Visualizer: remove duplicate logic and cleanup Signed-off-by: Jacob Hilliard <jhlliar@amazon.com> * Visualizer: fix tabs Signed-off-by: Jacob Hilliard <jhlliar@amazon.com> * Visualizer: move to iterator prefix increment Signed-off-by: Jacob Hilliard <jhlliar@amazon.com> * Visualizer: fix format string type error Signed-off-by: Jacob Hilliard <jhlliar@amazon.com>
This commit is contained in:
@@ -63,27 +63,37 @@ namespace AZ
|
||||
ImGuiCpuProfiler() = default;
|
||||
~ImGuiCpuProfiler() = default;
|
||||
|
||||
//! Draws the provided Cpu statistics.
|
||||
//! 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(bool& keepDrawing, const AZ::RHI::CpuTimingStatistics& currentCpuTimingStatistics);
|
||||
void DrawVisualizer();
|
||||
|
||||
private:
|
||||
static constexpr float RowHeight = 50.0;
|
||||
static constexpr int DefaultFramesToCollect = 50;
|
||||
|
||||
// Update the GroupRegionMap with the latest cached time regions
|
||||
void UpdateGroupRegionMap();
|
||||
// Draw the shared header between the two windows
|
||||
void DrawCommonHeader();
|
||||
|
||||
// ImGui filter used to filter TimedRegions.
|
||||
ImGuiTextFilter m_timedRegionFilter;
|
||||
|
||||
// Saves statistical view data organized by group name -> region name -> regions
|
||||
GroupRegionMap m_groupRegionMap;
|
||||
|
||||
// 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;
|
||||
|
||||
@@ -131,8 +141,6 @@ namespace AZ
|
||||
|
||||
// Visualizer state
|
||||
|
||||
bool m_showVisualizer = false;
|
||||
|
||||
int m_framesToCollect = DefaultFramesToCollect;
|
||||
|
||||
// Tally of the number of saved profiling events so far
|
||||
|
||||
@@ -52,110 +52,168 @@ namespace AZ
|
||||
|
||||
const ImVec2 windowSize(900.0f, 600.0f);
|
||||
ImGui::SetNextWindowSize(windowSize, ImGuiCond_Once);
|
||||
bool captureToFile = false;
|
||||
if (ImGui::Begin("Cpu Profiler", &keepDrawing, ImGuiWindowFlags_None))
|
||||
if (ImGui::Begin("CPU Profiler", &keepDrawing, ImGuiWindowFlags_None))
|
||||
{
|
||||
m_paused = !AZ::RHI::CpuProfiler::Get()->IsProfilerEnabled();
|
||||
if (ImGui::Button(m_paused ? "Resume" : "Pause"))
|
||||
{
|
||||
m_paused = !m_paused;
|
||||
AZ::RHI::CpuProfiler::Get()->SetProfilerEnabled(!m_paused);
|
||||
}
|
||||
|
||||
// Update region map and cache the input cpu timing statistics when the profiling is not paused
|
||||
// Collect the last frame's profiling data
|
||||
if (!m_paused)
|
||||
{
|
||||
UpdateGroupRegionMap();
|
||||
// Update region map and cache the input cpu timing statistics when the profiling is not paused
|
||||
m_cpuTimingStatisticsWhenPause = currentCpuTimingStatistics;
|
||||
}
|
||||
|
||||
if (ImGui::Button("Capture"))
|
||||
{
|
||||
captureToFile = true;
|
||||
}
|
||||
CollectFrameData();
|
||||
CullFrameData(currentCpuTimingStatistics);
|
||||
|
||||
if (!m_lastCapturedFilePath.empty())
|
||||
{
|
||||
ImGui::SameLine();
|
||||
ImGui::Text(m_lastCapturedFilePath.c_str());
|
||||
}
|
||||
|
||||
const AZ::RHI::CpuTimingStatistics& cpuTimingStatistics = m_cpuTimingStatisticsWhenPause;
|
||||
|
||||
const AZStd::sys_time_t ticksPerSecond = AZStd::GetTimeTicksPerSecond();
|
||||
|
||||
const auto ShowTimeInMs = [ticksPerSecond](AZStd::sys_time_t duration)
|
||||
{
|
||||
ImGui::Text("%.2f ms", CpuProfilerImGuiHelper::TicksToMs(duration));
|
||||
};
|
||||
|
||||
const auto ShowRow = [ticksPerSecond, &ShowTimeInMs](const char* regionLabel, AZStd::sys_time_t duration)
|
||||
{
|
||||
ImGui::Text(regionLabel);
|
||||
ImGui::NextColumn();
|
||||
|
||||
ShowTimeInMs(duration);
|
||||
ImGui::NextColumn();
|
||||
};
|
||||
|
||||
const auto DrawRegionHoverMarker = [this, &ShowTimeInMs](AZStd::vector<ThreadRegionEntry>& entries)
|
||||
{
|
||||
if (ImGui::IsItemHovered())
|
||||
// Only listen to system ticks when the profiler is active
|
||||
if (!SystemTickBus::Handler::BusIsConnected())
|
||||
{
|
||||
ImGui::BeginTooltip();
|
||||
ImGui::PushTextWrapPos(ImGui::GetFontSize() * 60.0f);
|
||||
|
||||
for (ThreadRegionEntry& entry : entries)
|
||||
{
|
||||
ImGui::Text(CpuProfilerImGuiHelper::TextThreadId(entry.m_threadId.m_id).c_str());
|
||||
|
||||
const AZStd::sys_time_t elapsed = entry.m_endTick - entry.m_startTick;
|
||||
ShowTimeInMs(elapsed);
|
||||
ImGui::Separator();
|
||||
}
|
||||
|
||||
ImGui::PopTextWrapPos();
|
||||
ImGui::EndTooltip();
|
||||
SystemTickBus::Handler::BusConnect();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const auto ShowRegionRow =
|
||||
[ticksPerSecond, &DrawRegionHoverMarker,
|
||||
&ShowTimeInMs](const char* regionLabel, AZStd::vector<ThreadRegionEntry> regions, AZStd::sys_time_t duration)
|
||||
if (m_enableVisualizer)
|
||||
{
|
||||
// Draw the region label
|
||||
ImGui::Text(regionLabel);
|
||||
ImGui::NextColumn();
|
||||
DrawVisualizer();
|
||||
}
|
||||
else
|
||||
{
|
||||
DrawStatisticsView();
|
||||
}
|
||||
}
|
||||
ImGui::End();
|
||||
|
||||
if (m_captureToFile)
|
||||
{
|
||||
AZStd::sys_time_t timeNow = AZStd::GetTimeNowSecond();
|
||||
AZStd::string timeString;
|
||||
AZStd::to_string(timeString, timeNow);
|
||||
u64 currentTick = AZ::RPI::RPISystemInterface::Get()->GetCurrentTick();
|
||||
const AZStd::string frameDataFilePath = AZStd::string::format(
|
||||
"@user@/CpuProfiler/%s_%llu.json",
|
||||
timeString.c_str(),
|
||||
currentTick);
|
||||
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);
|
||||
}
|
||||
m_captureToFile = false;
|
||||
|
||||
// Draw the thread count label
|
||||
AZStd::sys_time_t totalTime = 0;
|
||||
AZStd::set<AZStd::thread_id> threads;
|
||||
for (ThreadRegionEntry& entry : regions) // Find the thread count and total execution time for all threads
|
||||
// Toggle if the bool isn't the same as the cached value
|
||||
if (cachedShowCpuProfiler != keepDrawing)
|
||||
{
|
||||
AZ::RHI::CpuProfiler::Get()->SetProfilerEnabled(keepDrawing);
|
||||
}
|
||||
}
|
||||
|
||||
inline void ImGuiCpuProfiler::DrawCommonHeader()
|
||||
{
|
||||
if (ImGui::Button(m_enableVisualizer ? "Swap to statistics" : "Swap to visualizer"))
|
||||
{
|
||||
m_enableVisualizer = !m_enableVisualizer;
|
||||
}
|
||||
|
||||
ImGui::SameLine();
|
||||
m_paused = !AZ::RHI::CpuProfiler::Get()->IsProfilerEnabled();
|
||||
if (ImGui::Button(m_paused ? "Resume" : "Pause"))
|
||||
{
|
||||
m_paused = !m_paused;
|
||||
AZ::RHI::CpuProfiler::Get()->SetProfilerEnabled(!m_paused);
|
||||
}
|
||||
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Capture"))
|
||||
{
|
||||
m_captureToFile = true;
|
||||
}
|
||||
|
||||
if (!m_lastCapturedFilePath.empty())
|
||||
{
|
||||
ImGui::SameLine();
|
||||
ImGui::Text("Saved: %s", m_lastCapturedFilePath.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
inline void ImGuiCpuProfiler::DrawStatisticsView()
|
||||
{
|
||||
DrawCommonHeader();
|
||||
|
||||
const AZ::RHI::CpuTimingStatistics& cpuTimingStatistics = m_cpuTimingStatisticsWhenPause;
|
||||
|
||||
const AZStd::sys_time_t ticksPerSecond = AZStd::GetTimeTicksPerSecond();
|
||||
|
||||
const auto ShowTimeInMs = [ticksPerSecond](AZStd::sys_time_t duration)
|
||||
{
|
||||
ImGui::Text("%.2f ms", CpuProfilerImGuiHelper::TicksToMs(duration));
|
||||
};
|
||||
|
||||
const auto ShowRow = [ticksPerSecond, &ShowTimeInMs](const char* regionLabel, AZStd::sys_time_t duration)
|
||||
{
|
||||
ImGui::Text(regionLabel);
|
||||
ImGui::NextColumn();
|
||||
|
||||
ShowTimeInMs(duration);
|
||||
ImGui::NextColumn();
|
||||
};
|
||||
|
||||
const auto DrawRegionHoverMarker = [this, &ShowTimeInMs](AZStd::vector<ThreadRegionEntry>& entries)
|
||||
{
|
||||
if (ImGui::IsItemHovered())
|
||||
{
|
||||
ImGui::BeginTooltip();
|
||||
ImGui::PushTextWrapPos(ImGui::GetFontSize() * 60.0f);
|
||||
|
||||
for (ThreadRegionEntry& entry : entries)
|
||||
{
|
||||
threads.insert(entry.m_threadId);
|
||||
totalTime += entry.m_endTick - entry.m_startTick;
|
||||
ImGui::Text(CpuProfilerImGuiHelper::TextThreadId(entry.m_threadId.m_id).c_str());
|
||||
|
||||
const AZStd::sys_time_t elapsed = entry.m_endTick - entry.m_startTick;
|
||||
ShowTimeInMs(elapsed);
|
||||
ImGui::Separator();
|
||||
}
|
||||
const AZStd::string threadLabel = AZStd::string::format("Threads: %u", static_cast<uint32_t>(threads.size()));
|
||||
ImGui::Text(threadLabel.c_str());
|
||||
DrawRegionHoverMarker(regions);
|
||||
ImGui::NextColumn();
|
||||
|
||||
// Draw the overall invocation count
|
||||
const AZStd::string invocationLabel = AZStd::string::format("Total calls: %u", static_cast<uint32_t>(regions.size()));
|
||||
ImGui::Text(invocationLabel.c_str());
|
||||
DrawRegionHoverMarker(regions);
|
||||
ImGui::NextColumn();
|
||||
ImGui::PopTextWrapPos();
|
||||
ImGui::EndTooltip();
|
||||
}
|
||||
};
|
||||
|
||||
// Draw the time labels (max and then total)
|
||||
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();
|
||||
};
|
||||
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);
|
||||
ImGui::NextColumn();
|
||||
|
||||
ImGui::Checkbox("Enable Visualizer", &m_showVisualizer);
|
||||
// Draw the thread count label
|
||||
AZStd::sys_time_t totalTime = 0;
|
||||
AZStd::set<AZStd::thread_id> threads;
|
||||
for (ThreadRegionEntry& entry : regions) // Find the thread count and total execution time for all threads
|
||||
{
|
||||
threads.insert(entry.m_threadId);
|
||||
totalTime += entry.m_endTick - entry.m_startTick;
|
||||
}
|
||||
const AZStd::string threadLabel = AZStd::string::format("Threads: %u", static_cast<uint32_t>(threads.size()));
|
||||
ImGui::Text(threadLabel.c_str());
|
||||
DrawRegionHoverMarker(regions);
|
||||
ImGui::NextColumn();
|
||||
|
||||
// Draw the overall invocation count
|
||||
const AZStd::string invocationLabel = AZStd::string::format("Total calls: %u", static_cast<uint32_t>(regions.size()));
|
||||
ImGui::Text(invocationLabel.c_str());
|
||||
DrawRegionHoverMarker(regions);
|
||||
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),
|
||||
CpuProfilerImGuiHelper::TicksToMs(totalTime));
|
||||
ImGui::Text(timeLabel.c_str());
|
||||
ImGui::NextColumn();
|
||||
};
|
||||
|
||||
if (ImGui::BeginChild("Statistics View", { 0, 0 }, true))
|
||||
{
|
||||
// Set column settings.
|
||||
ImGui::Columns(2, "view", false);
|
||||
ImGui::SetColumnWidth(0, 660.0f);
|
||||
@@ -187,20 +245,20 @@ namespace AZ
|
||||
ImGui::SetColumnWidth(2, 150.0f);
|
||||
ImGui::SetColumnWidth(3, 240.0f);
|
||||
|
||||
for (auto& reigon : timeRegionMapEntry.second)
|
||||
for (auto& region : timeRegionMapEntry.second)
|
||||
{
|
||||
// Calculate the region with the longest execution time
|
||||
AZStd::sys_time_t threadExecutionElapsed = 0;
|
||||
for (ThreadRegionEntry& entry : reigon.second)
|
||||
for (ThreadRegionEntry& entry : region.second)
|
||||
{
|
||||
const AZStd::sys_time_t elapsed = entry.m_endTick - entry.m_startTick;
|
||||
threadExecutionElapsed = AZStd::max(threadExecutionElapsed, elapsed);
|
||||
}
|
||||
|
||||
// Only draw the TimedRegion rows when it passes the filter
|
||||
if (m_timedRegionFilter.PassFilter(reigon.first.c_str()))
|
||||
if (m_timedRegionFilter.PassFilter(region.first.c_str()))
|
||||
{
|
||||
ShowRegionRow(reigon.first.c_str(), reigon.second, threadExecutionElapsed);
|
||||
ShowRegionRow(region.first.c_str(), region.second, threadExecutionElapsed);
|
||||
}
|
||||
}
|
||||
ImGui::Columns(1, "view", false);
|
||||
@@ -209,227 +267,168 @@ namespace AZ
|
||||
}
|
||||
ImGui::EndChild();
|
||||
}
|
||||
}
|
||||
ImGui::End();
|
||||
|
||||
if (captureToFile)
|
||||
{
|
||||
AZStd::sys_time_t timeNow = AZStd::GetTimeNowSecond();
|
||||
AZStd::string timeString;
|
||||
AZStd::to_string(timeString, timeNow);
|
||||
u64 currentTick = AZ::RPI::RPISystemInterface::Get()->GetCurrentTick();
|
||||
AZStd::string frameDataFilePath = AZStd::string::format("@user@/CpuProfiler/%s_%llu.json", timeString.c_str(), currentTick);
|
||||
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);
|
||||
}
|
||||
|
||||
// Toggle if the bool isn't the same as the cached value
|
||||
if (cachedShowCpuProfiler != keepDrawing)
|
||||
{
|
||||
AZ::RHI::CpuProfiler::Get()->SetProfilerEnabled(keepDrawing);
|
||||
}
|
||||
|
||||
if (m_showVisualizer)
|
||||
{
|
||||
DrawVisualizer(m_showVisualizer, currentCpuTimingStatistics);
|
||||
}
|
||||
}
|
||||
|
||||
inline void ImGuiCpuProfiler::UpdateGroupRegionMap()
|
||||
{
|
||||
// Clear the cached entries
|
||||
m_groupRegionMap.clear();
|
||||
|
||||
// Get the latest TimeRegionMap
|
||||
const RHI::CpuProfiler::TimeRegionMap& timeRegionMap = RHI::CpuProfiler::Get()->GetTimeRegionMap();
|
||||
|
||||
// Iterate through all the cached regions from all threads, and add the entries to this map
|
||||
for (auto& threadEntry : timeRegionMap)
|
||||
{
|
||||
for (auto& cachedRegionEntry : threadEntry.second)
|
||||
{
|
||||
const AZStd::string& regionName = cachedRegionEntry.first;
|
||||
for (auto& cachedRegion : cachedRegionEntry.second)
|
||||
{
|
||||
const AZStd::string& groupName = cachedRegion.m_groupRegionName->m_groupName;
|
||||
|
||||
m_groupRegionMap[groupName][regionName].push_back(
|
||||
{ threadEntry.first, cachedRegion.m_startTick, cachedRegion.m_endTick });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -- CPU Visualizer --
|
||||
inline void ImGuiCpuProfiler::DrawVisualizer(bool& keepDrawing, const AZ::RHI::CpuTimingStatistics& currentCpuTimingStatistics)
|
||||
inline void ImGuiCpuProfiler::DrawVisualizer()
|
||||
{
|
||||
ImGui::SetNextWindowSize({ 900, 600 }, ImGuiCond_Once);
|
||||
if (ImGui::Begin("CPU Visualizer", &keepDrawing, ImGuiWindowFlags_None))
|
||||
DrawCommonHeader();
|
||||
|
||||
// Options & Statistics
|
||||
if (ImGui::BeginChild("Options and Statistics", { 0, 0 }, true))
|
||||
{
|
||||
// Get the instrumentation data for the last frame if active
|
||||
if (!m_paused && m_groupRegionMap.size() != 0)
|
||||
{
|
||||
CollectFrameData(); // Also updates viewport bounds
|
||||
ImGui::Columns(3, "Options", true);
|
||||
ImGui::Text("Frames To Collect:");
|
||||
ImGui::SliderInt("", &m_framesToCollect, 10, 100, "%d", ImGuiSliderFlags_AlwaysClamp | ImGuiSliderFlags_Logarithmic);
|
||||
|
||||
CullFrameData(currentCpuTimingStatistics); // Trim data if necessary
|
||||
ImGui::NextColumn();
|
||||
|
||||
if (!SystemTickBus::Handler::BusIsConnected())
|
||||
{
|
||||
SystemTickBus::Handler::BusConnect();
|
||||
}
|
||||
}
|
||||
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);
|
||||
|
||||
// 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::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::TextWrapped(
|
||||
"Hold the right mouse button to move around. Zoom by scrolling the mouse wheel while holding <ctrl>.");
|
||||
}
|
||||
ImGui::End();
|
||||
|
||||
|
||||
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)
|
||||
{
|
||||
--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
|
||||
{
|
||||
const 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();
|
||||
}
|
||||
|
||||
inline void ImGuiCpuProfiler::CollectFrameData()
|
||||
{
|
||||
// We maintain separate datastores for the visualizer and the statistical view because they require different
|
||||
// data formats - one grouped by thread ID versus the other organized by group + region. Since the statistical
|
||||
// view is only holding data from the last frame, the memory overhead is minimal and gives us a faster redraw
|
||||
// compared to if we needed to transform the visualizer's data into the statistical format every frame.
|
||||
|
||||
// Clear the statistical view's cached entries
|
||||
m_groupRegionMap.clear();
|
||||
|
||||
// Get the latest TimeRegionMap
|
||||
const RHI::CpuProfiler::TimeRegionMap& timeRegionMap = RHI::CpuProfiler::Get()->GetTimeRegionMap();
|
||||
|
||||
m_viewportStartTick = INT64_MAX;
|
||||
@@ -445,13 +444,18 @@ namespace AZ
|
||||
}
|
||||
|
||||
// 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
|
||||
AZStd::vector<TimeRegion> newVisualizerData;
|
||||
newVisualizerData.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
|
||||
newVisualizerData.push_back(region); // Copies
|
||||
|
||||
// Also update the statistical view's data
|
||||
const AZStd::string& groupName = region.m_groupRegionName->m_groupName;
|
||||
m_groupRegionMap[groupName][regionName].push_back(
|
||||
{ threadId, region.m_startTick, region.m_endTick });
|
||||
|
||||
// Update running statistics if we want to record this region's data
|
||||
if (m_regionStatisticsMap[region.m_groupRegionName].m_record)
|
||||
@@ -464,22 +468,22 @@ namespace AZ
|
||||
// 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(),
|
||||
newVisualizerData.begin(), newVisualizerData.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_viewportStartTick = AZStd::min(newVisualizerData.front().m_startTick, m_viewportStartTick);
|
||||
m_viewportEndTick = AZStd::max(newVisualizerData.back().m_endTick, m_viewportEndTick);
|
||||
|
||||
m_savedRegionCount += newData.size();
|
||||
m_savedRegionCount += newVisualizerData.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()));
|
||||
savedDataVec.end(), AZStd::make_move_iterator(newVisualizerData.begin()), AZStd::make_move_iterator(newVisualizerData.end()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -650,14 +654,11 @@ namespace AZ
|
||||
// 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);
|
||||
|
||||
// Draw to one element before the last collected boundary if possible to avoid empty frame at the end
|
||||
auto drawToItr = m_frameEndTicks.size() > 1 ? m_frameEndTicks.end() - 1 : m_frameEndTicks.end();
|
||||
|
||||
while (endTickItr != drawToItr && *endTickItr < m_viewportEndTick)
|
||||
while (endTickItr != m_frameEndTicks.end() && *endTickItr < m_viewportEndTick)
|
||||
{
|
||||
const float horizontalPixel = ConvertTickToPixelSpace(*endTickItr);
|
||||
drawList->AddLine({ horizontalPixel, wy }, { horizontalPixel, wy + windowHeight }, red);
|
||||
endTickItr++;
|
||||
++endTickItr;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -668,15 +669,13 @@ namespace AZ
|
||||
auto nextFrameBoundaryItr = lastFrameBoundaryItr;
|
||||
if (lastFrameBoundaryItr != m_frameEndTicks.begin())
|
||||
{
|
||||
lastFrameBoundaryItr--;
|
||||
--lastFrameBoundaryItr;
|
||||
}
|
||||
|
||||
const auto [wx, wy] = ImGui::GetWindowPos();
|
||||
ImDrawList* drawList = ImGui::GetWindowDrawList();
|
||||
|
||||
auto drawToItr = m_frameEndTicks.size() > 1 ? m_frameEndTicks.end() - 1 : m_frameEndTicks.end();
|
||||
|
||||
while (nextFrameBoundaryItr != drawToItr && *lastFrameBoundaryItr <= m_viewportEndTick)
|
||||
while (nextFrameBoundaryItr != m_frameEndTicks.end() && *lastFrameBoundaryItr <= m_viewportEndTick)
|
||||
{
|
||||
const AZStd::sys_time_t lastFrameBoundaryTick = *lastFrameBoundaryItr;
|
||||
const AZStd::sys_time_t nextFrameBoundaryTick = *nextFrameBoundaryItr;
|
||||
@@ -735,7 +734,7 @@ namespace AZ
|
||||
IM_COL32_WHITE);
|
||||
|
||||
lastFrameBoundaryItr = nextFrameBoundaryItr;
|
||||
nextFrameBoundaryItr++;
|
||||
++nextFrameBoundaryItr;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -756,15 +755,14 @@ namespace AZ
|
||||
// System tick bus overrides
|
||||
inline void ImGuiCpuProfiler::OnSystemTick()
|
||||
{
|
||||
if (!m_paused)
|
||||
{
|
||||
m_frameEndTicks.push_back(AZStd::GetTimeNowTicks());
|
||||
}
|
||||
|
||||
if (!m_showVisualizer || m_paused)
|
||||
if (m_paused)
|
||||
{
|
||||
SystemTickBus::Handler::BusDisconnect();
|
||||
}
|
||||
else
|
||||
{
|
||||
m_frameEndTicks.push_back(AZStd::GetTimeNowTicks());
|
||||
}
|
||||
}
|
||||
|
||||
// ----- RegionStatistics implementation -----
|
||||
|
||||
Reference in New Issue
Block a user