@ -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()
{