Visualizer: finish deserialization of captures (#3293)
* Visualizer: Initial capture loading support Signed-off-by: Jacob Hilliard <jhlliar@amazon.com> * Visualizer: Deserialize thread id Signed-off-by: Jacob Hilliard <jhlliar@amazon.com> * Visualizer: Quality of life improvements throughout - Decrease row size for more information onscreen - Remove fudge factors in drawing logic - Add comments to deserialization logic - Fix Editor text scaling bug - Early out for data culling to avoid erase_if call Signed-off-by: Jacob Hilliard <jhlliar@amazon.com>
This commit is contained in:
@@ -173,13 +173,14 @@ namespace AZ
|
||||
static void Reflect(AZ::ReflectContext* context);
|
||||
|
||||
CpuProfilingStatisticsSerializerEntry() = default;
|
||||
CpuProfilingStatisticsSerializerEntry(const RHI::CachedTimeRegion& cachedTimeRegion);
|
||||
CpuProfilingStatisticsSerializerEntry(const RHI::CachedTimeRegion& cachedTimeRegion, AZStd::thread_id threadId);
|
||||
|
||||
Name m_groupName;
|
||||
Name m_regionName;
|
||||
uint16_t m_stackDepth;
|
||||
AZStd::sys_time_t m_startTick;
|
||||
AZStd::sys_time_t m_endTick;
|
||||
size_t m_threadId;
|
||||
};
|
||||
|
||||
AZ_TYPE_INFO(CpuProfilingStatisticsSerializer, "{D5B02946-0D27-474F-9A44-364C2706DD41}");
|
||||
|
||||
@@ -417,14 +417,14 @@ namespace AZ
|
||||
// Create serializable entries
|
||||
for (const auto& timeRegionMap : continuousData)
|
||||
{
|
||||
for (const auto& threadEntry : timeRegionMap)
|
||||
for (const auto& [threadId, regionMap] : timeRegionMap)
|
||||
{
|
||||
for (const auto& cachedRegionEntry : threadEntry.second)
|
||||
for (const auto& [regionName, regionVec] : regionMap)
|
||||
{
|
||||
m_cpuProfilingStatisticsSerializerEntries.insert(
|
||||
m_cpuProfilingStatisticsSerializerEntries.end(),
|
||||
cachedRegionEntry.second.begin(),
|
||||
cachedRegionEntry.second.end());
|
||||
for (const auto& region : regionVec)
|
||||
{
|
||||
m_cpuProfilingStatisticsSerializerEntries.emplace_back(region, threadId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -445,13 +445,15 @@ namespace AZ
|
||||
|
||||
// --- CpuProfilingStatisticsSerializerEntry ---
|
||||
|
||||
CpuProfilingStatisticsSerializer::CpuProfilingStatisticsSerializerEntry::CpuProfilingStatisticsSerializerEntry(const RHI::CachedTimeRegion& cachedTimeRegion)
|
||||
CpuProfilingStatisticsSerializer::CpuProfilingStatisticsSerializerEntry::CpuProfilingStatisticsSerializerEntry(
|
||||
const RHI::CachedTimeRegion& cachedTimeRegion, AZStd::thread_id threadId)
|
||||
{
|
||||
m_groupName = cachedTimeRegion.m_groupRegionName->m_groupName;
|
||||
m_regionName = cachedTimeRegion.m_groupRegionName->m_regionName;
|
||||
m_stackDepth = cachedTimeRegion.m_stackDepth;
|
||||
m_startTick = cachedTimeRegion.m_startTick;
|
||||
m_endTick = cachedTimeRegion.m_endTick;
|
||||
m_threadId = AZStd::hash<AZStd::thread_id>{}(threadId);
|
||||
}
|
||||
|
||||
void CpuProfilingStatisticsSerializer::CpuProfilingStatisticsSerializerEntry::Reflect(AZ::ReflectContext* context)
|
||||
@@ -465,6 +467,7 @@ namespace AZ
|
||||
->Field("stackDepth", &CpuProfilingStatisticsSerializerEntry::m_stackDepth)
|
||||
->Field("startTick", &CpuProfilingStatisticsSerializerEntry::m_startTick)
|
||||
->Field("endTick", &CpuProfilingStatisticsSerializerEntry::m_endTick)
|
||||
->Field("threadId", &CpuProfilingStatisticsSerializerEntry::m_threadId)
|
||||
;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ namespace AZ
|
||||
};
|
||||
|
||||
// Update running statistics with new region data
|
||||
void RecordRegion(const AZ::RHI::CachedTimeRegion& region, AZStd::thread_id threadId);
|
||||
void RecordRegion(const AZ::RHI::CachedTimeRegion& region, size_t threadId);
|
||||
|
||||
void ResetPerFrameStatistics();
|
||||
|
||||
@@ -58,7 +58,7 @@ namespace AZ
|
||||
u64 m_invocationsLastFrame = 0;
|
||||
|
||||
// NOTE: set over unordered_set so the threads can be shown in increasing order in tooltip.
|
||||
AZStd::set<AZStd::thread_id> m_executingThreads;
|
||||
AZStd::set<size_t> m_executingThreads;
|
||||
|
||||
AZStd::sys_time_t m_lastFrameTotalTicks = 0;
|
||||
|
||||
@@ -95,7 +95,7 @@ namespace AZ
|
||||
void Draw(bool& keepDrawing, const AZ::RHI::CpuTimingStatistics& cpuTimingStatistics);
|
||||
|
||||
private:
|
||||
static constexpr float RowHeight = 50.0;
|
||||
static constexpr float RowHeight = 35.0;
|
||||
static constexpr int DefaultFramesToCollect = 50;
|
||||
static constexpr float MediumFrameTimeLimit = 16.6; // 60 fps
|
||||
static constexpr float HighFrameTimeLimit = 33.3; // 30 fps
|
||||
@@ -134,7 +134,7 @@ namespace AZ
|
||||
void DrawThreadSeparator(u64 threadBoundary, u64 maxDepth);
|
||||
|
||||
// Draw the "Thread XXXXX" label onto the viewport
|
||||
void DrawThreadLabel(u64 baseRow, AZStd::thread_id threadId);
|
||||
void DrawThreadLabel(u64 baseRow, size_t threadId);
|
||||
|
||||
// Draw the vertical lines separating frames in the timeline
|
||||
void DrawFrameBoundaries();
|
||||
@@ -169,7 +169,9 @@ namespace AZ
|
||||
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;
|
||||
// note: we use size_t as a proxy for thread_id because native_thread_id_type differs differs from
|
||||
// platform to platform, which causes problems when deserializing saved captures.
|
||||
AZStd::unordered_map<size_t, AZStd::vector<TimeRegion>> m_savedData;
|
||||
|
||||
// Region color cache
|
||||
AZStd::unordered_map<const GroupRegionName*, ImVec4> m_regionColorMap;
|
||||
@@ -213,6 +215,11 @@ namespace AZ
|
||||
|
||||
// Index into the file picker, used to determine which file to load when "Load File" is pressed.
|
||||
int m_currentFileIndex = 0;
|
||||
|
||||
|
||||
// --- Loading capture state ---
|
||||
AZStd::unordered_set<AZStd::string> m_deserializedStringPool;
|
||||
AZStd::unordered_set<RHI::CachedTimeRegion::GroupRegionName, RHI::CachedTimeRegion::GroupRegionName::Hash> m_deserializedGroupRegionNamePool;
|
||||
};
|
||||
} // namespace Render
|
||||
} // namespace AZ
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#include <Atom/RPI.Edit/Common/JsonUtils.h>
|
||||
#include <Atom/RPI.Public/RPISystemInterface.h>
|
||||
|
||||
#include <AzCore/Casting/lossy_cast.h>
|
||||
#include <AzCore/IO/FileIO.h>
|
||||
#include <AzCore/JSON/filereadstream.h>
|
||||
#include <AzCore/Serialization/Json/JsonSerialization.h>
|
||||
@@ -30,19 +31,6 @@ namespace AZ
|
||||
{
|
||||
namespace CpuProfilerImGuiHelper
|
||||
{
|
||||
// NOTE: Fix build error in case AZStd::thread_id is not of an arithmetic type, and instead a pointer
|
||||
template<typename ThreadId, typename AZStd::enable_if<AZStd::is_pointer<ThreadId>::value>::type* = nullptr>
|
||||
AZStd::string TextThreadId(ThreadId threadId)
|
||||
{
|
||||
return AZStd::string::format("Thread: %p", threadId);
|
||||
}
|
||||
|
||||
template<typename ThreadId, typename AZStd::enable_if<!AZStd::is_pointer<ThreadId>::value>::type* = nullptr>
|
||||
AZStd::string TextThreadId(ThreadId threadId)
|
||||
{
|
||||
return AZStd::string::format("Thread: %zu", static_cast<size_t>(threadId));
|
||||
}
|
||||
|
||||
inline float TicksToMs(AZStd::sys_time_t ticks)
|
||||
{
|
||||
// Note: converting to microseconds integer before converting to milliseconds float
|
||||
@@ -231,6 +219,7 @@ namespace AZ
|
||||
m_lastCapturedFilePath = resolvedPath;
|
||||
AZ::Render::ProfilingCaptureRequestBus::Broadcast(
|
||||
&AZ::Render::ProfilingCaptureRequestBus::Events::EndContinuousCpuProfilingCapture, frameDataFilePath);
|
||||
m_paused = true;
|
||||
}
|
||||
|
||||
else
|
||||
@@ -447,13 +436,65 @@ namespace AZ
|
||||
inline void ImGuiCpuProfiler::LoadFile()
|
||||
{
|
||||
const IO::Path& pathToLoad = m_cachedCapturePaths[m_currentFileIndex];
|
||||
auto res = CpuProfilerImGuiHelper::LoadSavedCpuProfilingStatistics(pathToLoad.String());
|
||||
if (!res.IsSuccess())
|
||||
auto loadResult = CpuProfilerImGuiHelper::LoadSavedCpuProfilingStatistics(pathToLoad.String());
|
||||
if (!loadResult.IsSuccess())
|
||||
{
|
||||
AZ_TracePrintf("ImGuiCpuProfiler", "%s", res.GetError().c_str());
|
||||
AZ_TracePrintf("ImGuiCpuProfiler", "%s", loadResult.GetError().c_str());
|
||||
return;
|
||||
}
|
||||
// TODO ATOM-16022 Parse this data and display it in the visualizer widget.
|
||||
|
||||
AZStd::vector<RHI::CpuProfilingStatisticsSerializer::CpuProfilingStatisticsSerializerEntry> deserializedData = loadResult.TakeValue();
|
||||
|
||||
// Clear visualizer and statistics view state
|
||||
m_savedRegionCount = deserializedData.size();
|
||||
m_savedData.clear();
|
||||
m_paused = true;
|
||||
AZ::RHI::CpuProfiler::Get()->SetProfilerEnabled(false);
|
||||
m_frameEndTicks.clear();
|
||||
|
||||
m_tableData.clear();
|
||||
m_groupRegionMap.clear();
|
||||
|
||||
for (const auto& entry : deserializedData)
|
||||
{
|
||||
const auto [groupNameItr, wasGroupNameInserted] = m_deserializedStringPool.emplace(entry.m_groupName.GetCStr());
|
||||
const auto [regionNameItr, wasRegionNameInserted] = m_deserializedStringPool.emplace(entry.m_regionName.GetCStr());
|
||||
const auto [groupRegionNameItr, wasGroupRegionNameInserted] =
|
||||
m_deserializedGroupRegionNamePool.emplace(groupNameItr->c_str(), regionNameItr->c_str());
|
||||
|
||||
const RHI::CachedTimeRegion newRegion(&(*groupRegionNameItr), entry.m_stackDepth, entry.m_startTick, entry.m_endTick);
|
||||
m_savedData[entry.m_threadId].push_back(newRegion);
|
||||
|
||||
// Since we don't serialize the frame boundaries, we need to use the RPI's OnSystemTick event as a heuristic.
|
||||
const static Name frameBoundaryName = Name("RPISystem: OnSystemTick");
|
||||
if (entry.m_regionName == frameBoundaryName)
|
||||
{
|
||||
m_frameEndTicks.push_back(entry.m_endTick);
|
||||
}
|
||||
|
||||
// Update running statistics
|
||||
if (!m_groupRegionMap[*groupNameItr].contains(*regionNameItr))
|
||||
{
|
||||
m_groupRegionMap[*groupNameItr][*regionNameItr].m_groupName = *groupNameItr;
|
||||
m_groupRegionMap[*groupNameItr][*regionNameItr].m_regionName = *regionNameItr;
|
||||
m_tableData.push_back(&m_groupRegionMap[*groupNameItr][*regionNameItr]);
|
||||
}
|
||||
m_groupRegionMap[*groupNameItr][*regionNameItr].RecordRegion(newRegion, entry.m_threadId);
|
||||
}
|
||||
|
||||
// Update viewport bounds with some added UX fudge factor
|
||||
m_viewportStartTick = deserializedData.back().m_startTick - 1000;
|
||||
m_viewportEndTick = deserializedData.back().m_endTick + 1000;
|
||||
|
||||
// Invariant: each vector in m_savedData must be sorted so that we can efficiently cull region data.
|
||||
for (auto& [threadId, singleThreadData] : m_savedData)
|
||||
{
|
||||
AZStd::sort(singleThreadData.begin(), singleThreadData.end(),
|
||||
[](const TimeRegion& lhs, const TimeRegion& rhs)
|
||||
{
|
||||
return lhs.m_startTick < rhs.m_startTick;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// -- CPU Visualizer --
|
||||
@@ -465,7 +506,7 @@ namespace AZ
|
||||
if (ImGui::BeginChild("Options and Statistics", { 0, 0 }, true))
|
||||
{
|
||||
ImGui::Columns(3, "Options", true);
|
||||
ImGui::SliderInt("Saved Frames", &m_framesToCollect, 10, 10000, "%d", ImGuiSliderFlags_AlwaysClamp | ImGuiSliderFlags_Logarithmic);
|
||||
ImGui::SliderInt("Saved Frames", &m_framesToCollect, 10, 20000, "%d", ImGuiSliderFlags_AlwaysClamp | ImGuiSliderFlags_Logarithmic);
|
||||
m_visualizerHighlightFilter.Draw("Find Region");
|
||||
|
||||
ImGui::NextColumn();
|
||||
@@ -631,6 +672,7 @@ namespace AZ
|
||||
// Iterate through the entire TimeRegionMap and copy the data since it will get deleted on the next frame
|
||||
for (const auto& [threadId, singleThreadRegionMap] : timeRegionMap)
|
||||
{
|
||||
const size_t threadIdHashed = AZStd::hash<AZStd::thread_id>{}(threadId);
|
||||
// The profiler can sometime return threads without any profiling events when dropping threads, FIXME(ATOM-15949)
|
||||
if (singleThreadRegionMap.size() == 0)
|
||||
{
|
||||
@@ -656,7 +698,7 @@ namespace AZ
|
||||
m_tableData.push_back(&m_groupRegionMap[groupName][regionName]);
|
||||
}
|
||||
|
||||
m_groupRegionMap[groupName][regionName].RecordRegion(region, threadId);
|
||||
m_groupRegionMap[groupName][regionName].RecordRegion(region, threadIdHashed);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -676,7 +718,7 @@ namespace AZ
|
||||
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];
|
||||
AZStd::vector<TimeRegion>& savedDataVec = m_savedData[threadIdHashed];
|
||||
savedDataVec.insert(
|
||||
savedDataVec.end(), AZStd::make_move_iterator(newVisualizerData.begin()), AZStd::make_move_iterator(newVisualizerData.end()));
|
||||
}
|
||||
@@ -696,6 +738,12 @@ namespace AZ
|
||||
{
|
||||
AZStd::size_t sizeBeforeRemove = savedRegions.size();
|
||||
|
||||
// Early out to avoid the linear erase_if call
|
||||
if (savedRegions.size() >= 1 && savedRegions.at(0).m_startTick > deleteBeforeTick)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Use erase_if over plain upper_bound + erase to avoid repeated shifts. erase requires a shift of all elements to the right
|
||||
// for each element that is erased, while erase_if squashes all removes into a single shift which significantly improves perf.
|
||||
AZStd::erase_if(
|
||||
@@ -732,12 +780,19 @@ namespace AZ
|
||||
const float startPixel = ConvertTickToPixelSpace(block.m_startTick, m_viewportStartTick, m_viewportEndTick);
|
||||
const float endPixel = ConvertTickToPixelSpace(block.m_endTick, m_viewportStartTick, m_viewportEndTick);
|
||||
|
||||
const ImVec2 startPoint = { startPixel, wy + targetRow * RowHeight };
|
||||
const ImVec2 endPoint = { endPixel, wy + targetRow * RowHeight + 40 };
|
||||
if (endPixel - startPixel < 0.5f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const ImVec2 startPoint = { startPixel, wy + targetRow * RowHeight + 1};
|
||||
const ImVec2 endPoint = { endPixel, wy + (targetRow + 1) * RowHeight };
|
||||
|
||||
const ImU32 blockColor = GetBlockColor(block);
|
||||
|
||||
drawList->AddRectFilled(startPoint, endPoint, blockColor, 0);
|
||||
drawList->AddLine(startPoint, { endPixel, startPoint.y }, IM_COL32_BLACK, 0.5f);
|
||||
drawList->AddLine({ startPixel, endPoint.y }, endPoint, IM_COL32_BLACK, 0.5f);
|
||||
|
||||
// Draw the region name if possible
|
||||
// If the block's current width is too small, we skip drawing the label.
|
||||
@@ -751,11 +806,13 @@ namespace AZ
|
||||
|
||||
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;
|
||||
|
||||
// NOTE: RenderText calls do not automatically account for the global scale (which is modified at high DPI)
|
||||
// so we must adjust for the scale manually.
|
||||
const float scaleFactor = ImGui::GetIO().FontGlobalScale;
|
||||
const float fontSize = ImGui::GetFont()->FontSize * scaleFactor;
|
||||
|
||||
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.
|
||||
@@ -815,18 +872,18 @@ namespace AZ
|
||||
auto [wx, wy] = ImGui::GetWindowPos();
|
||||
wy -= ImGui::GetScrollY();
|
||||
const float windowWidth = ImGui::GetWindowWidth();
|
||||
const float boundaryY = wy + (baseRow + maxDepth + 1) * RowHeight - 5;
|
||||
const float boundaryY = wy + (baseRow + maxDepth + 1) * RowHeight;
|
||||
|
||||
ImGui::GetWindowDrawList()->AddLine({ wx, boundaryY }, { wx + windowWidth, boundaryY }, red, 2.0f);
|
||||
ImGui::GetWindowDrawList()->AddLine({ wx, boundaryY }, { wx + windowWidth, boundaryY }, red, 1.0f);
|
||||
}
|
||||
|
||||
inline void ImGuiCpuProfiler::DrawThreadLabel(u64 baseRow, AZStd::thread_id threadId)
|
||||
inline void ImGuiCpuProfiler::DrawThreadLabel(u64 baseRow, size_t threadId)
|
||||
{
|
||||
auto [wx, wy] = ImGui::GetWindowPos();
|
||||
wy -= ImGui::GetScrollY();
|
||||
const AZStd::string threadIdText = CpuProfilerImGuiHelper::TextThreadId(threadId.m_id);
|
||||
const AZStd::string threadIdText = AZStd::string::format("Thread: %zu", threadId);
|
||||
|
||||
ImGui::GetWindowDrawList()->AddText({ wx + 10, wy + baseRow * RowHeight + 5 }, IM_COL32_WHITE, threadIdText.c_str());
|
||||
ImGui::GetWindowDrawList()->AddText({ wx + 10, wy + baseRow * RowHeight}, IM_COL32_WHITE, threadIdText.c_str());
|
||||
}
|
||||
|
||||
inline void ImGuiCpuProfiler::DrawFrameBoundaries()
|
||||
@@ -884,8 +941,10 @@ namespace AZ
|
||||
const float textBeginPixel = lastFrameBoundaryPixel + offset;
|
||||
const float textEndPixel = textBeginPixel + labelWidth;
|
||||
|
||||
const float verticalOffset = (ImGui::GetWindowHeight() - ImGui::GetFontSize()) / 2;
|
||||
|
||||
// Execution time label
|
||||
drawList->AddText({ textBeginPixel, wy + ImGui::GetWindowHeight() / 4 }, IM_COL32_WHITE, label.c_str());
|
||||
drawList->AddText({ textBeginPixel, wy + verticalOffset }, IM_COL32_WHITE, label.c_str());
|
||||
|
||||
// Left side
|
||||
drawList->AddLine(
|
||||
@@ -1043,7 +1102,7 @@ namespace AZ
|
||||
|
||||
// ---- TableRow impl ----
|
||||
|
||||
inline void TableRow::RecordRegion(const AZ::RHI::CachedTimeRegion& region, AZStd::thread_id threadId)
|
||||
inline void TableRow::RecordRegion(const AZ::RHI::CachedTimeRegion& region, size_t threadId)
|
||||
{
|
||||
const AZStd::sys_time_t deltaTime = region.m_endTick - region.m_startTick;
|
||||
|
||||
@@ -1072,7 +1131,7 @@ namespace AZ
|
||||
auto threadString = AZStd::string::format("Executed in %zu threads\n", m_executingThreads.size());
|
||||
for (const auto& threadId : m_executingThreads)
|
||||
{
|
||||
threadString.append(CpuProfilerImGuiHelper::TextThreadId(threadId.m_id) + "\n");
|
||||
threadString.append(AZStd::string::format("Thread: %zu\n", threadId));
|
||||
}
|
||||
return threadString;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user