From 3f71277220e6226ff54bc8f7a3b67826e58bfb6a Mon Sep 17 00:00:00 2001 From: Jacob Hilliard <64656371+jcbhl@users.noreply.github.com> Date: Thu, 22 Jul 2021 10:12:07 -0700 Subject: [PATCH] Integrate visualizer widget into ASV CPU profiler (#2106) * Visualizer: initial refactor to toggle behavior Signed-off-by: Jacob Hilliard * Visualizer: improve window integration Signed-off-by: Jacob Hilliard * Visualizer: remove duplicate logic and cleanup Signed-off-by: Jacob Hilliard * Visualizer: fix tabs Signed-off-by: Jacob Hilliard * Visualizer: move to iterator prefix increment Signed-off-by: Jacob Hilliard * Visualizer: fix format string type error Signed-off-by: Jacob Hilliard --- .../Include/Atom/Utils/ImGuiCpuProfiler.h | 20 +- .../Include/Atom/Utils/ImGuiCpuProfiler.inl | 642 +++++++++--------- 2 files changed, 334 insertions(+), 328 deletions(-) diff --git a/Gems/Atom/Utils/Code/Include/Atom/Utils/ImGuiCpuProfiler.h b/Gems/Atom/Utils/Code/Include/Atom/Utils/ImGuiCpuProfiler.h index 509474debf..c0ee4f8e02 100644 --- a/Gems/Atom/Utils/Code/Include/Atom/Utils/ImGuiCpuProfiler.h +++ b/Gems/Atom/Utils/Code/Include/Atom/Utils/ImGuiCpuProfiler.h @@ -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 diff --git a/Gems/Atom/Utils/Code/Include/Atom/Utils/ImGuiCpuProfiler.inl b/Gems/Atom/Utils/Code/Include/Atom/Utils/ImGuiCpuProfiler.inl index 0e90df85a7..b59747421e 100644 --- a/Gems/Atom/Utils/Code/Include/Atom/Utils/ImGuiCpuProfiler.inl +++ b/Gems/Atom/Utils/Code/Include/Atom/Utils/ImGuiCpuProfiler.inl @@ -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& 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 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 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& 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(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(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 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 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(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(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 ."); - } - - - 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(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(-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(0.05 * io.MouseWheel * GetViewportTickWidth()); - - // Split the overall delta between the two bounds depending on mouse pos - const auto newStartTick = m_viewportStartTick + aznumeric_cast(percentWindow * overallTickDelta); - const auto newEndTick = m_viewportEndTick - aznumeric_cast((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 ."); } - 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(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(-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(0.05 * io.MouseWheel * GetViewportTickWidth()); + + // Split the overall delta between the two bounds depending on mouse pos + const auto newStartTick = m_viewportStartTick + aznumeric_cast(percentWindow * overallTickDelta); + const auto newEndTick = m_viewportEndTick - aznumeric_cast((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 newData; - newData.reserve(singleThreadRegionMap.size()); // Avoids reallocation in the normal case when each region only has one invocation + AZStd::vector 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& 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 -----