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:
Jacob Hilliard
2021-08-23 13:14:00 -07:00
committed by GitHub
parent 89cc27fdef
commit 246621fda5
4 changed files with 116 additions and 46 deletions
@@ -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;
}