Profiler: implement loading from saved captures (#3026)

* Profiler: implement loading from saved capture
Adds functionality for finding a saved capture on disk and then
deserializing it using rapidjson's built-in buffered stream reader. This
does require use of raw file pointers since saved captures can be
hundreds of megabytes.
Actually showing the data in the visualizer is TODO.
* Profiler: use heap buffer over stack buffer
* Profiler: move deserialization logic to ImGuiCpuProfiler

Signed-off-by: Jacob Hilliard <jhlliar@amazon.com>
monroegm-disable-blank-issue-2
Jacob Hilliard 4 years ago committed by GitHub
parent fa2fc03a25
commit e865ad5d23
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -9,6 +9,7 @@
#include "ProfilingCaptureSystemComponent.h"
#include <Atom/RHI/CpuProfiler.h>
#include <Atom/RHI/CpuProfilerImpl.h>
#include <Atom/RHI/RHIUtils.h>
#include <Atom/RHI/RHISystemInterface.h>
#include <Atom/RHI.Reflect/CpuTimingStatistics.h>
@ -141,36 +142,6 @@ namespace AZ
AZStd::vector<PipelineStatisticsSerializerEntry> m_pipelineStatisticsEntries;
};
// Intermediate class to serialize Cpu TimedRegion data.
class CpuProfilingStatisticsSerializer
{
public:
class CpuProfilingStatisticsSerializerEntry
{
public:
AZ_TYPE_INFO(CpuProfilingStatisticsSerializer::CpuProfilingStatisticsSerializerEntry, "{26B78F65-EB96-46E2-BE7E-A1233880B225}");
static void Reflect(AZ::ReflectContext* context);
CpuProfilingStatisticsSerializerEntry() = default;
CpuProfilingStatisticsSerializerEntry(const RHI::CachedTimeRegion& cachedTimeRegion);
private:
Name m_groupName;
Name m_regionName;
uint16_t m_stackDepth;
AZStd::sys_time_t m_startTick;
AZStd::sys_time_t m_endTick;
};
AZ_TYPE_INFO(CpuProfilingStatisticsSerializer, "{D5B02946-0D27-474F-9A44-364C2706DD41}");
static void Reflect(AZ::ReflectContext* context);
CpuProfilingStatisticsSerializer() = default;
CpuProfilingStatisticsSerializer(const AZStd::ring_buffer<RHI::CpuProfiler::TimeRegionMap>& continuousData);
AZStd::vector<CpuProfilingStatisticsSerializerEntry> m_cpuProfilingStatisticsSerializerEntries;
};
// Intermediate class to serialize benchmark metadata.
class BenchmarkMetadataSerializer
{
@ -327,65 +298,6 @@ namespace AZ
}
}
// --- CpuProfilingStatisticsSerializer ---
CpuProfilingStatisticsSerializer::CpuProfilingStatisticsSerializer(const AZStd::ring_buffer<RHI::CpuProfiler::TimeRegionMap>& continuousData)
{
// Create serializable entries
for (const auto& timeRegionMap : continuousData)
{
for (const auto& threadEntry : timeRegionMap)
{
for (const auto& cachedRegionEntry : threadEntry.second)
{
m_cpuProfilingStatisticsSerializerEntries.insert(
m_cpuProfilingStatisticsSerializerEntries.end(),
cachedRegionEntry.second.begin(),
cachedRegionEntry.second.end());
}
}
}
}
void CpuProfilingStatisticsSerializer::Reflect(AZ::ReflectContext* context)
{
if (auto* serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
{
serializeContext->Class<CpuProfilingStatisticsSerializer>()
->Version(1)
->Field("cpuProfilingStatisticsSerializerEntry", &CpuProfilingStatisticsSerializer::m_cpuProfilingStatisticsSerializerEntries)
;
}
CpuProfilingStatisticsSerializerEntry::Reflect(context);
}
// --- CpuProfilingStatisticsSerializerEntry ---
CpuProfilingStatisticsSerializer::CpuProfilingStatisticsSerializerEntry::CpuProfilingStatisticsSerializerEntry(const RHI::CachedTimeRegion& cachedTimeRegion)
{
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;
}
void CpuProfilingStatisticsSerializer::CpuProfilingStatisticsSerializerEntry::Reflect(AZ::ReflectContext* context)
{
if (auto* serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
{
serializeContext->Class<CpuProfilingStatisticsSerializerEntry>()
->Version(1)
->Field("groupName", &CpuProfilingStatisticsSerializerEntry::m_groupName)
->Field("regionName", &CpuProfilingStatisticsSerializerEntry::m_regionName)
->Field("stackDepth", &CpuProfilingStatisticsSerializerEntry::m_stackDepth)
->Field("startTick", &CpuProfilingStatisticsSerializerEntry::m_startTick)
->Field("endTick", &CpuProfilingStatisticsSerializerEntry::m_endTick)
;
}
}
// --- BenchmarkMetadataSerializer ---
BenchmarkMetadataSerializer::BenchmarkMetadataSerializer(const AZStd::string& benchmarkName, const RHI::PhysicalDeviceDescriptor& gpuDescriptor)
@ -458,7 +370,7 @@ namespace AZ
TimestampSerializer::Reflect(context);
CpuFrameTimeSerializer::Reflect(context);
PipelineStatisticsSerializer::Reflect(context);
CpuProfilingStatisticsSerializer::Reflect(context);
RHI::CpuProfilingStatisticsSerializer::Reflect(context);
BenchmarkMetadataSerializer::Reflect(context);
}
@ -651,10 +563,10 @@ namespace AZ
JsonSerializerSettings serializationSettings;
serializationSettings.m_keepDefaults = true;
CpuProfilingStatisticsSerializer serializer(data);
RHI::CpuProfilingStatisticsSerializer serializer(data);
const auto saveResult = JsonSerializationUtils::SaveObjectToFile(&serializer,
outputFilePath, (CpuProfilingStatisticsSerializer*)nullptr, &serializationSettings);
outputFilePath, (RHI::CpuProfilingStatisticsSerializer*)nullptr, &serializationSettings);
AZStd::string captureInfo = outputFilePath;
if (!saveResult.IsSuccess())
@ -694,7 +606,7 @@ namespace AZ
const bool captureStarted = m_cpuProfilingStatisticsCapture.StartCapture([this, outputFilePath, wasEnabled]()
{
// Blocking call for a single frame of data, avoid thread overhead
AZStd::ring_buffer<RHI::CpuProfiler::TimeRegionMap> singleFrameData;
AZStd::ring_buffer<RHI::CpuProfiler::TimeRegionMap> singleFrameData(1);
singleFrameData.push_back(RHI::CpuProfiler::Get()->GetTimeRegionMap());
SerializeCpuProfilingData(singleFrameData, outputFilePath, wasEnabled);
});

@ -12,6 +12,7 @@
#include <AzCore/Component/TickBus.h>
#include <AzCore/Memory/OSAllocator.h>
#include <AzCore/Name/Name.h>
#include <AzCore/std/containers/map.h>
#include <AzCore/std/containers/unordered_set.h>
#include <AzCore/std/parallel/mutex.h>
@ -161,5 +162,33 @@ namespace AZ
AZStd::ring_buffer<TimeRegionMap> m_continuousCaptureData;
};
}; // namespace RPI
// Intermediate class to serialize Cpu TimedRegion data.
class CpuProfilingStatisticsSerializer
{
public:
class CpuProfilingStatisticsSerializerEntry
{
public:
AZ_TYPE_INFO(CpuProfilingStatisticsSerializer::CpuProfilingStatisticsSerializerEntry, "{26B78F65-EB96-46E2-BE7E-A1233880B225}");
static void Reflect(AZ::ReflectContext* context);
CpuProfilingStatisticsSerializerEntry() = default;
CpuProfilingStatisticsSerializerEntry(const RHI::CachedTimeRegion& cachedTimeRegion);
Name m_groupName;
Name m_regionName;
uint16_t m_stackDepth;
AZStd::sys_time_t m_startTick;
AZStd::sys_time_t m_endTick;
};
AZ_TYPE_INFO(CpuProfilingStatisticsSerializer, "{D5B02946-0D27-474F-9A44-364C2706DD41}");
static void Reflect(AZ::ReflectContext* context);
CpuProfilingStatisticsSerializer() = default;
CpuProfilingStatisticsSerializer(const AZStd::ring_buffer<RHI::CpuProfiler::TimeRegionMap>& continuousData);
AZStd::vector<CpuProfilingStatisticsSerializerEntry> m_cpuProfilingStatisticsSerializerEntries;
};
}; // namespace RHI
}; // namespace AZ

@ -409,5 +409,64 @@ namespace AZ
m_cachedTimeRegionMutex.unlock();
}
}
}
}
// --- CpuProfilingStatisticsSerializer ---
CpuProfilingStatisticsSerializer::CpuProfilingStatisticsSerializer(const AZStd::ring_buffer<RHI::CpuProfiler::TimeRegionMap>& continuousData)
{
// Create serializable entries
for (const auto& timeRegionMap : continuousData)
{
for (const auto& threadEntry : timeRegionMap)
{
for (const auto& cachedRegionEntry : threadEntry.second)
{
m_cpuProfilingStatisticsSerializerEntries.insert(
m_cpuProfilingStatisticsSerializerEntries.end(),
cachedRegionEntry.second.begin(),
cachedRegionEntry.second.end());
}
}
}
}
void CpuProfilingStatisticsSerializer::Reflect(AZ::ReflectContext* context)
{
if (auto* serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
{
serializeContext->Class<CpuProfilingStatisticsSerializer>()
->Version(1)
->Field("cpuProfilingStatisticsSerializerEntries", &CpuProfilingStatisticsSerializer::m_cpuProfilingStatisticsSerializerEntries)
;
}
CpuProfilingStatisticsSerializerEntry::Reflect(context);
}
// --- CpuProfilingStatisticsSerializerEntry ---
CpuProfilingStatisticsSerializer::CpuProfilingStatisticsSerializerEntry::CpuProfilingStatisticsSerializerEntry(const RHI::CachedTimeRegion& cachedTimeRegion)
{
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;
}
void CpuProfilingStatisticsSerializer::CpuProfilingStatisticsSerializerEntry::Reflect(AZ::ReflectContext* context)
{
if (auto* serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
{
serializeContext->Class<CpuProfilingStatisticsSerializerEntry>()
->Version(1)
->Field("groupName", &CpuProfilingStatisticsSerializerEntry::m_groupName)
->Field("regionName", &CpuProfilingStatisticsSerializerEntry::m_regionName)
->Field("stackDepth", &CpuProfilingStatisticsSerializerEntry::m_stackDepth)
->Field("startTick", &CpuProfilingStatisticsSerializerEntry::m_startTick)
->Field("endTick", &CpuProfilingStatisticsSerializerEntry::m_endTick)
;
}
}
} // namespace RHI
} // namespace AZ

@ -10,7 +10,9 @@
#include <AzCore/JSON/document.h>
#include <AzCore/Serialization/Json/JsonSerialization.h>
#include <AtomCore/Serialization/Json/JsonUtils.h>
#include <Atom/RPI.Edit/Common/JsonFileLoadContext.h>
#include <Atom/RPI.Edit/Common/JsonReportingHelper.h>
@ -118,7 +120,6 @@ namespace AZ
AZ_Error("AZ::RPI::JsonUtils", false, "Failed to load object from json string: %s", loadResult.GetError().c_str());
return false;
}
} // namespace JsonUtils
} // namespace RPI
} // namespace AZ

@ -9,6 +9,7 @@
#pragma once
#include <AzCore/Component/TickBus.h>
#include <AzCore/IO/Path/Path.h>
#include <AzCore/Math/Random.h>
#include <Atom/RHI.Reflect/CpuTimingStatistics.h>
@ -102,6 +103,12 @@ namespace AZ
//! Draws the statistical view of the CPU profiling data.
void DrawStatisticsView();
//! Callback invoked when the "Load File" button is pressed in the file picker.
void LoadFile();
//! Draws the file picker window.
void DrawFilePicker();
//! Draws the CPU profiling visualizer.
void DrawVisualizer();
@ -198,6 +205,14 @@ namespace AZ
AZ::RHI::CpuTimingStatistics m_cpuTimingStatisticsWhenPause;
AZStd::string m_lastCapturedFilePath;
bool m_showFilePicker = false;
// Cached file paths to previous traces on disk, sorted with the most recent trace at the front.
AZStd::vector<IO::Path> m_cachedCapturePaths;
// Index into the file picker, used to determine which file to load when "Load File" is pressed.
int m_currentFileIndex = 0;
};
} // namespace Render
} // namespace AZ

@ -9,9 +9,14 @@
#include <Atom/Feature/Utils/ProfilingCaptureBus.h>
#include <Atom/RHI.Reflect/CpuTimingStatistics.h>
#include <Atom/RHI/CpuProfiler.h>
#include <Atom/RHI/CpuProfilerImpl.h>
#include <Atom/RPI.Edit/Common/JsonUtils.h>
#include <Atom/RPI.Public/RPISystemInterface.h>
#include <AzCore/IO/Path/Path_fwd.h>
#include <AzCore/IO/FileIO.h>
#include <AzCore/JSON/filereadstream.h>
#include <AzCore/Serialization/Json/JsonSerialization.h>
#include <AzCore/Serialization/Json/JsonSerializationResult.h>
#include <AzCore/std/containers/map.h>
#include <AzCore/std/containers/set.h>
#include <AzCore/std/limits.h>
@ -45,6 +50,74 @@ namespace AZ
AZ_Assert(ticksPerSecond >= 1000, "Error in converting ticks to ms, expected ticksPerSecond >= 1000");
return static_cast<float>((ticks * 1000) / (ticksPerSecond / 1000)) / 1000.0f;
}
using DeserializedCpuData = AZStd::vector<RHI::CpuProfilingStatisticsSerializer::CpuProfilingStatisticsSerializerEntry>;
inline Outcome<DeserializedCpuData, AZStd::string> LoadSavedCpuProfilingStatistics(const AZStd::string& capturePath)
{
auto* base = IO::FileIOBase::GetInstance();
char resolvedPath[IO::MaxPathLength];
if (!base->ResolvePath(capturePath.c_str(), resolvedPath, IO::MaxPathLength))
{
return Failure(AZStd::string::format("Could not resolve the path to file %s, is the path correct?", resolvedPath));
}
u64 captureSizeBytes;
const IO::Result fileSizeResult = base->Size(resolvedPath, captureSizeBytes);
if (!fileSizeResult)
{
return Failure(AZStd::string::format("Could not read the size of file %s, is the path correct?", resolvedPath));
}
// NOTE: this uses raw file pointers over the abstractions and utility functions provided by AZ::JsonSerializationUtils because
// saved profiling captures can be upwards of 400 MB. This necessitates a buffered approach to avoid allocating huge chunks of memory.
FILE* fp = nullptr;
azfopen(&fp, resolvedPath, "rb");
if (!fp)
{
return Failure(AZStd::string::format("Could not fopen file %s, is the path correct?\n", resolvedPath));
}
constexpr AZStd::size_t MaxBufSize = 65536;
const AZStd::size_t bufSize = AZStd::min(MaxBufSize, aznumeric_cast<AZStd::size_t>(captureSizeBytes));
char* buf = reinterpret_cast<char*>(azmalloc(bufSize));
rapidjson::Document document;
rapidjson::FileReadStream inputStream(fp, buf, bufSize);
document.ParseStream(inputStream);
azfree(buf);
fclose(fp);
if (document.HasParseError())
{
const auto pe = document.GetParseError();
return Failure(AZStd::string::format(
"Rapidjson could not parse the document with ParseErrorCode %u. See 3rdParty/rapidjson/error.h for definitions.\n", pe));
}
if (!document.IsObject() || !document.HasMember("ClassData"))
{
return Failure(AZStd::string::format(
"Error in loading saved capture: top-level object does not have a ClassData field. Did the serialization format change recently?\n"));
}
AZ_TracePrintf("JsonUtils", "Successfully loaded JSON into memory.\n");
const auto& root = document["ClassData"];
RHI::CpuProfilingStatisticsSerializer serializer;
const JsonSerializationResult::ResultCode deserializationResult = JsonSerialization::Load(serializer, root);
if (deserializationResult.GetProcessing() == JsonSerializationResult::Processing::Halted
|| serializer.m_cpuProfilingStatisticsSerializerEntries.empty())
{
return Failure(AZStd::string::format("Error in deserializing document: %s\n", deserializationResult.ToString(capturePath.c_str()).c_str()));
}
AZ_TracePrintf("JsonUtils", "Successfully loaded CPU profiling data with %zu profiling entries.\n",
serializer.m_cpuProfilingStatisticsSerializerEntries.size());
return Success(AZStd::move(serializer.m_cpuProfilingStatisticsSerializerEntries));
}
} // namespace CpuProfilerImGuiHelper
inline void ImGuiCpuProfiler::Draw(bool& keepDrawing, const AZ::RHI::CpuTimingStatistics& currentCpuTimingStatistics)
@ -80,6 +153,11 @@ namespace AZ
{
DrawStatisticsView();
}
if (m_showFilePicker)
{
DrawFilePicker();
}
}
ImGui::End();
@ -110,6 +188,11 @@ namespace AZ
inline void ImGuiCpuProfiler::DrawCommonHeader()
{
if (!m_lastCapturedFilePath.empty())
{
ImGui::Text("Saved: %s", m_lastCapturedFilePath.c_str());
}
if (ImGui::Button(m_enableVisualizer ? "Swap to statistics" : "Swap to visualizer"))
{
m_enableVisualizer = !m_enableVisualizer;
@ -157,10 +240,31 @@ namespace AZ
}
}
if (!m_lastCapturedFilePath.empty())
ImGui::SameLine();
if (ImGui::Button("Load file"))
{
ImGui::SameLine();
ImGui::Text("Saved: %s", m_lastCapturedFilePath.c_str());
m_showFilePicker = true;
// Only update the cached file list when opened so that we aren't making IO calls on every frame.
auto* base = AZ::IO::FileIOBase::GetInstance();
const AZStd::string defaultSavedCapturePath = "@user@/CpuProfiler";
m_cachedCapturePaths.clear();
base->FindFiles(
defaultSavedCapturePath.c_str(), "*.json",
[&paths = m_cachedCapturePaths](const char* path) -> bool
{
auto foundPath = IO::Path(path);
paths.push_back(foundPath);
return true;
});
// Sort by decreasing modification time (most recent at the top)
AZStd::sort(m_cachedCapturePaths.begin(), m_cachedCapturePaths.end(),
[&base](const IO::Path& lhs, const IO::Path& rhs)
{
return base->ModificationTime(lhs.c_str()) > base->ModificationTime(rhs.c_str());
});
}
}
@ -313,6 +417,45 @@ namespace AZ
}
}
inline void ImGuiCpuProfiler::DrawFilePicker()
{
ImGui::SetNextWindowSize({ 500, 200 }, ImGuiCond_Once);
if (ImGui::Begin("File Picker", &m_showFilePicker))
{
if (ImGui::Button("Load selected"))
{
LoadFile();
}
auto getter = [](void* vectorPointer, int idx, const char** out_text) -> bool
{
const auto& pathVec = *static_cast<AZStd::vector<IO::Path>*>(vectorPointer);
if (idx < 0 || idx >= pathVec.size())
{
return false;
}
*out_text = pathVec[idx].c_str();
return true;
};
ImGui::SetNextItemWidth(ImGui::GetWindowContentRegionWidth());
ImGui::ListBox("", &m_currentFileIndex, getter, &m_cachedCapturePaths, aznumeric_cast<int>(m_cachedCapturePaths.size()));
}
ImGui::End();
}
inline void ImGuiCpuProfiler::LoadFile()
{
const IO::Path& pathToLoad = m_cachedCapturePaths[m_currentFileIndex];
auto res = CpuProfilerImGuiHelper::LoadSavedCpuProfilingStatistics(pathToLoad.String());
if (!res.IsSuccess())
{
AZ_TracePrintf("ImGuiCpuProfiler", "%s", res.GetError().c_str());
return;
}
// TODO ATOM-16022 Parse this data and display it in the visualizer widget.
}
// -- CPU Visualizer --
inline void ImGuiCpuProfiler::DrawVisualizer()
{

Loading…
Cancel
Save