From d193dc40a99bd919343d1e35f71a4c35c55d6a30 Mon Sep 17 00:00:00 2001 From: AMZN-ScottR <24445312+AMZN-ScottR@users.noreply.github.com> Date: Mon, 11 Oct 2021 16:46:59 -0700 Subject: [PATCH] [atom_cpu_profiler_gem_promotion] updated namespaces for moved files Signed-off-by: AMZN-ScottR <24445312+AMZN-ScottR@users.noreply.github.com> --- Gems/Profiler/Code/Source/CpuProfiler.h | 96 +- Gems/Profiler/Code/Source/CpuProfilerImpl.cpp | 633 +++--- Gems/Profiler/Code/Source/CpuProfilerImpl.h | 247 ++- .../Profiler/Code/Source/ImGuiCpuProfiler.cpp | 1727 ++++++++--------- Gems/Profiler/Code/Source/ImGuiCpuProfiler.h | 291 ++- 5 files changed, 1489 insertions(+), 1505 deletions(-) diff --git a/Gems/Profiler/Code/Source/CpuProfiler.h b/Gems/Profiler/Code/Source/CpuProfiler.h index 70c5771b57..289a549ddd 100644 --- a/Gems/Profiler/Code/Source/CpuProfiler.h +++ b/Gems/Profiler/Code/Source/CpuProfiler.h @@ -15,73 +15,69 @@ #include #include -namespace AZ +namespace Profiler { - namespace RHI + //! Structure that is used to cache a timed region into the thread's local storage. + struct CachedTimeRegion { - //! Structure that is used to cache a timed region into the thread's local storage. - struct CachedTimeRegion + //! Structure used internally for caching assumed global string pointers (ideally literals) to the marker group/region + //! NOTE: When used in a separate shared library, the library mustn't be unloaded before the CpuProfiler is shutdown. + struct GroupRegionName { - //! Structure used internally for caching assumed global string pointers (ideally literals) to the marker group/region - //! NOTE: When used in a separate shared library, the library mustn't be unloaded before the CpuProfiler is shutdown. - struct GroupRegionName - { - GroupRegionName() = delete; - GroupRegionName(const char* const group, const char* const region); + GroupRegionName() = delete; + GroupRegionName(const char* const group, const char* const region); - const char* m_groupName = nullptr; - const char* m_regionName = nullptr; + const char* m_groupName = nullptr; + const char* m_regionName = nullptr; - struct Hash - { - AZStd::size_t operator()(const GroupRegionName& name) const; - }; - bool operator==(const GroupRegionName& other) const; + struct Hash + { + AZStd::size_t operator()(const GroupRegionName& name) const; }; + bool operator==(const GroupRegionName& other) const; + }; - CachedTimeRegion() = default; - CachedTimeRegion(const GroupRegionName& groupRegionName); - CachedTimeRegion(const GroupRegionName& groupRegionName, uint16_t stackDepth, uint64_t startTick, uint64_t endTick); - - GroupRegionName m_groupRegionName{nullptr, nullptr}; + CachedTimeRegion() = default; + CachedTimeRegion(const GroupRegionName& groupRegionName); + CachedTimeRegion(const GroupRegionName& groupRegionName, uint16_t stackDepth, uint64_t startTick, uint64_t endTick); - uint16_t m_stackDepth = 0u; - AZStd::sys_time_t m_startTick = 0; - AZStd::sys_time_t m_endTick = 0; - }; + GroupRegionName m_groupRegionName{nullptr, nullptr}; - //! Interface class of the CpuProfiler - class CpuProfiler - { - public: - using ThreadTimeRegionMap = AZStd::unordered_map>; - using TimeRegionMap = AZStd::unordered_map; + uint16_t m_stackDepth = 0u; + AZStd::sys_time_t m_startTick = 0; + AZStd::sys_time_t m_endTick = 0; + }; - AZ_RTTI(CpuProfiler, "{127C1D0B-BE05-4E18-A8F6-24F3EED2ECA6}"); + //! Interface class of the CpuProfiler + class CpuProfiler + { + public: + using ThreadTimeRegionMap = AZStd::unordered_map>; + using TimeRegionMap = AZStd::unordered_map; - CpuProfiler() = default; - virtual ~CpuProfiler() = default; + AZ_RTTI(CpuProfiler, "{127C1D0B-BE05-4E18-A8F6-24F3EED2ECA6}"); - AZ_DISABLE_COPY_MOVE(CpuProfiler); + CpuProfiler() = default; + virtual ~CpuProfiler() = default; - static CpuProfiler* Get(); + AZ_DISABLE_COPY_MOVE(CpuProfiler); - //! Get the last frame's TimeRegionMap - virtual const TimeRegionMap& GetTimeRegionMap() const = 0; + static CpuProfiler* Get(); - //! Begin a continuous capture. Blocks the profiler from being toggled off until EndContinuousCapture is called. - [[nodiscard]] virtual bool BeginContinuousCapture() = 0; + //! Get the last frame's TimeRegionMap + virtual const TimeRegionMap& GetTimeRegionMap() const = 0; - //! Flush the CPU Profiler's saved data into the passed ring buffer . - [[nodiscard]] virtual bool EndContinuousCapture(AZStd::ring_buffer& flushTarget) = 0; + //! Begin a continuous capture. Blocks the profiler from being toggled off until EndContinuousCapture is called. + [[nodiscard]] virtual bool BeginContinuousCapture() = 0; - virtual bool IsContinuousCaptureInProgress() const = 0; + //! Flush the CPU Profiler's saved data into the passed ring buffer . + [[nodiscard]] virtual bool EndContinuousCapture(AZStd::ring_buffer& flushTarget) = 0; - //! Enable/Disable the CpuProfiler - virtual void SetProfilerEnabled(bool enabled) = 0; + virtual bool IsContinuousCaptureInProgress() const = 0; - virtual bool IsProfilerEnabled() const = 0 ; - }; + //! Enable/Disable the CpuProfiler + virtual void SetProfilerEnabled(bool enabled) = 0; - } // namespace RPI -} // namespace AZ + virtual bool IsProfilerEnabled() const = 0 ; + }; +} // namespace Profiler diff --git a/Gems/Profiler/Code/Source/CpuProfilerImpl.cpp b/Gems/Profiler/Code/Source/CpuProfilerImpl.cpp index 826e0b6aa3..15713e647a 100644 --- a/Gems/Profiler/Code/Source/CpuProfilerImpl.cpp +++ b/Gems/Profiler/Code/Source/CpuProfilerImpl.cpp @@ -15,434 +15,431 @@ #include #include -namespace AZ +namespace Profiler { - namespace RHI - { - thread_local CpuTimingLocalStorage* CpuProfilerImpl::ms_threadLocalStorage = nullptr; + thread_local CpuTimingLocalStorage* CpuProfilerImpl::ms_threadLocalStorage = nullptr; - // --- CpuProfiler --- + // --- CpuProfiler --- - CpuProfiler* CpuProfiler::Get() - { - return Interface::Get(); - } + CpuProfiler* CpuProfiler::Get() + { + return Interface::Get(); + } - // --- CachedTimeRegion --- + // --- CachedTimeRegion --- - CachedTimeRegion::CachedTimeRegion(const GroupRegionName& groupRegionName) - { - m_groupRegionName = groupRegionName; - } + CachedTimeRegion::CachedTimeRegion(const GroupRegionName& groupRegionName) + { + m_groupRegionName = groupRegionName; + } - CachedTimeRegion::CachedTimeRegion(const GroupRegionName& groupRegionName, uint16_t stackDepth, uint64_t startTick, uint64_t endTick) - { - m_groupRegionName = groupRegionName; - m_stackDepth = stackDepth; - m_startTick = startTick; - m_endTick = endTick; - } + CachedTimeRegion::CachedTimeRegion(const GroupRegionName& groupRegionName, uint16_t stackDepth, uint64_t startTick, uint64_t endTick) + { + m_groupRegionName = groupRegionName; + m_stackDepth = stackDepth; + m_startTick = startTick; + m_endTick = endTick; + } - // --- GroupRegionName --- + // --- GroupRegionName --- - CachedTimeRegion::GroupRegionName::GroupRegionName(const char* const group, const char* const region) : - m_groupName(group), - m_regionName(region) - { - } + CachedTimeRegion::GroupRegionName::GroupRegionName(const char* const group, const char* const region) : + m_groupName(group), + m_regionName(region) + { + } - AZStd::size_t CachedTimeRegion::GroupRegionName::Hash::operator()(const CachedTimeRegion::GroupRegionName& name) const - { - AZStd::size_t seed = 0; - AZStd::hash_combine(seed, name.m_groupName); - AZStd::hash_combine(seed, name.m_regionName); - return seed; - } + AZStd::size_t CachedTimeRegion::GroupRegionName::Hash::operator()(const CachedTimeRegion::GroupRegionName& name) const + { + AZStd::size_t seed = 0; + AZStd::hash_combine(seed, name.m_groupName); + AZStd::hash_combine(seed, name.m_regionName); + return seed; + } - bool CachedTimeRegion::GroupRegionName::operator==(const GroupRegionName& other) const - { - return (m_groupName == other.m_groupName) && (m_regionName == other.m_regionName); - } + bool CachedTimeRegion::GroupRegionName::operator==(const GroupRegionName& other) const + { + return (m_groupName == other.m_groupName) && (m_regionName == other.m_regionName); + } - // --- CpuProfilerImpl --- + // --- CpuProfilerImpl --- - void CpuProfilerImpl::Init() - { - Interface::Register(this); - Interface::Register(this); - m_initialized = true; - SystemTickBus::Handler::BusConnect(); - m_continuousCaptureData.set_capacity(10); + void CpuProfilerImpl::Init() + { + Interface::Register(this); + Interface::Register(this); + m_initialized = true; + SystemTickBus::Handler::BusConnect(); + m_continuousCaptureData.set_capacity(10); - if (auto statsProfiler = AZ::Interface::Get(); statsProfiler) - { - statsProfiler->ActivateProfiler(AZ_CRC_CE("RHI"), true); - } + if (auto statsProfiler = AZ::Interface::Get(); statsProfiler) + { + statsProfiler->ActivateProfiler(AZ_CRC_CE("RHI"), true); } + } - void CpuProfilerImpl::Shutdown() + void CpuProfilerImpl::Shutdown() + { + if (!m_initialized) { - if (!m_initialized) - { - return; - } - // When this call is made, no more thread profiling calls can be performed anymore - Interface::Unregister(this); - Interface::Unregister(this); + return; + } + // When this call is made, no more thread profiling calls can be performed anymore + Interface::Unregister(this); + Interface::Unregister(this); - // Wait for the remaining threads that might still be processing its profiling calls - AZStd::unique_lock shutdownLock(m_shutdownMutex); + // Wait for the remaining threads that might still be processing its profiling calls + AZStd::unique_lock shutdownLock(m_shutdownMutex); - m_enabled = false; + m_enabled = false; - // Cleanup all TLS - m_registeredThreads.clear(); - m_timeRegionMap.clear(); - m_initialized = false; - m_continuousCaptureInProgress.store(false); - m_continuousCaptureData.clear(); - SystemTickBus::Handler::BusDisconnect(); - } + // Cleanup all TLS + m_registeredThreads.clear(); + m_timeRegionMap.clear(); + m_initialized = false; + m_continuousCaptureInProgress.store(false); + m_continuousCaptureData.clear(); + SystemTickBus::Handler::BusDisconnect(); + } - void CpuProfilerImpl::BeginRegion(const AZ::Debug::Budget* budget, const char* eventName) + void CpuProfilerImpl::BeginRegion(const AZ::Debug::Budget* budget, const char* eventName) + { + // Try to lock here, the shutdownMutex will only be contested when the CpuProfiler is shutting down. + if (m_shutdownMutex.try_lock_shared()) { - // Try to lock here, the shutdownMutex will only be contested when the CpuProfiler is shutting down. - if (m_shutdownMutex.try_lock_shared()) + if (m_enabled) { - if (m_enabled) - { - // Lazy initialization, creates an instance of the Thread local data if it's not created, and registers it - RegisterThreadStorage(); + // Lazy initialization, creates an instance of the Thread local data if it's not created, and registers it + RegisterThreadStorage(); - // Push it to the stack - CachedTimeRegion timeRegion({budget->Name(), eventName}); - ms_threadLocalStorage->RegionStackPushBack(timeRegion); - } - - m_shutdownMutex.unlock_shared(); + // Push it to the stack + CachedTimeRegion timeRegion({budget->Name(), eventName}); + ms_threadLocalStorage->RegionStackPushBack(timeRegion); } + + m_shutdownMutex.unlock_shared(); } + } - void CpuProfilerImpl::EndRegion([[maybe_unused]] const AZ::Debug::Budget* budget) + void CpuProfilerImpl::EndRegion([[maybe_unused]] const AZ::Debug::Budget* budget) + { + // Try to lock here, the shutdownMutex will only be contested when the CpuProfiler is shutting down. + if (m_shutdownMutex.try_lock_shared()) { - // Try to lock here, the shutdownMutex will only be contested when the CpuProfiler is shutting down. - if (m_shutdownMutex.try_lock_shared()) + // guard against enabling mid-marker + if (m_enabled && ms_threadLocalStorage != nullptr) { - // guard against enabling mid-marker - if (m_enabled && ms_threadLocalStorage != nullptr) - { - ms_threadLocalStorage->RegionStackPopBack(); - } - - m_shutdownMutex.unlock_shared(); + ms_threadLocalStorage->RegionStackPopBack(); } + + m_shutdownMutex.unlock_shared(); } + } + + const CpuProfiler::TimeRegionMap& CpuProfilerImpl::GetTimeRegionMap() const + { + return m_timeRegionMap; + } - const CpuProfiler::TimeRegionMap& CpuProfilerImpl::GetTimeRegionMap() const + bool CpuProfilerImpl::BeginContinuousCapture() + { + bool expected = false; + if (m_continuousCaptureInProgress.compare_exchange_strong(expected, true)) { - return m_timeRegionMap; + m_enabled = true; + AZ_TracePrintf("Profiler", "Continuous capture started\n"); + return true; } - bool CpuProfilerImpl::BeginContinuousCapture() + AZ_TracePrintf("Profiler", "Attempting to start a continuous capture while one already in progress"); + return false; + } + + bool CpuProfilerImpl::EndContinuousCapture(AZStd::ring_buffer& flushTarget) + { + if (!m_continuousCaptureInProgress.load()) { - bool expected = false; - if (m_continuousCaptureInProgress.compare_exchange_strong(expected, true)) - { - m_enabled = true; - AZ_TracePrintf("Profiler", "Continuous capture started\n"); - return true; - } - - AZ_TracePrintf("Profiler", "Attempting to start a continuous capture while one already in progress"); + AZ_TracePrintf("Profiler", "Attempting to end a continuous capture while one not in progress"); return false; } - bool CpuProfilerImpl::EndContinuousCapture(AZStd::ring_buffer& flushTarget) + if (m_continuousCaptureEndingMutex.try_lock()) { - if (!m_continuousCaptureInProgress.load()) - { - AZ_TracePrintf("Profiler", "Attempting to end a continuous capture while one not in progress"); - return false; - } - - if (m_continuousCaptureEndingMutex.try_lock()) - { - m_enabled = false; - flushTarget = AZStd::move(m_continuousCaptureData); - m_continuousCaptureData.clear(); - AZ_TracePrintf("Profiler", "Continuous capture ended\n"); - m_continuousCaptureInProgress.store(false); - - m_continuousCaptureEndingMutex.unlock(); - return true; - } + m_enabled = false; + flushTarget = AZStd::move(m_continuousCaptureData); + m_continuousCaptureData.clear(); + AZ_TracePrintf("Profiler", "Continuous capture ended\n"); + m_continuousCaptureInProgress.store(false); - return false; + m_continuousCaptureEndingMutex.unlock(); + return true; } - bool CpuProfilerImpl::IsContinuousCaptureInProgress() const + return false; + } + + bool CpuProfilerImpl::IsContinuousCaptureInProgress() const + { + return m_continuousCaptureInProgress.load(); + } + + void CpuProfilerImpl::SetProfilerEnabled(bool enabled) + { + AZStd::unique_lock lock(m_threadRegisterMutex); + + // Early out if the state is already the same or a continuous capture is in progress + if (m_enabled == enabled || m_continuousCaptureInProgress.load()) { - return m_continuousCaptureInProgress.load(); + return; } - void CpuProfilerImpl::SetProfilerEnabled(bool enabled) + // Set the dirty flag in all the TLS to clear the caches + if (enabled) { - AZStd::unique_lock lock(m_threadRegisterMutex); - - // Early out if the state is already the same or a continuous capture is in progress - if (m_enabled == enabled || m_continuousCaptureInProgress.load()) + // Iterate through all the threads, and set the clearing flag + for (auto& threadLocal : m_registeredThreads) { - return; + threadLocal->m_clearContainers = true; } - // Set the dirty flag in all the TLS to clear the caches - if (enabled) - { - // Iterate through all the threads, and set the clearing flag - for (auto& threadLocal : m_registeredThreads) - { - threadLocal->m_clearContainers = true; - } - - m_enabled = true; - } - else - { - m_enabled = false; - } + m_enabled = true; } - - bool CpuProfilerImpl::IsProfilerEnabled() const + else { - return m_enabled; + m_enabled = false; } + } - void CpuProfilerImpl::OnSystemTick() + bool CpuProfilerImpl::IsProfilerEnabled() const + { + return m_enabled; + } + + void CpuProfilerImpl::OnSystemTick() + { + if (!m_enabled) { - if (!m_enabled) - { - return; - } + return; + } - if (m_continuousCaptureInProgress.load() && m_continuousCaptureEndingMutex.try_lock()) + if (m_continuousCaptureInProgress.load() && m_continuousCaptureEndingMutex.try_lock()) + { + if (m_continuousCaptureData.full() && m_continuousCaptureData.size() != MaxFramesToSave) { - if (m_continuousCaptureData.full() && m_continuousCaptureData.size() != MaxFramesToSave) - { - const AZStd::size_t size = m_continuousCaptureData.size(); - m_continuousCaptureData.set_capacity(AZStd::min(MaxFramesToSave, size + size / 2)); - } - - m_continuousCaptureData.push_back(AZStd::move(m_timeRegionMap)); - m_timeRegionMap.clear(); - m_continuousCaptureEndingMutex.unlock(); + const AZStd::size_t size = m_continuousCaptureData.size(); + m_continuousCaptureData.set_capacity(AZStd::min(MaxFramesToSave, size + size / 2)); } - AZStd::unique_lock lock(m_threadRegisterMutex); - - // Iterate through all the threads, and collect the thread's cached time regions - TimeRegionMap newMap; - for (auto& threadLocal : m_registeredThreads) - { - ThreadTimeRegionMap& threadMapEntry = newMap[threadLocal->m_executingThreadId]; - threadLocal->TryFlushCachedMap(threadMapEntry); - } + m_continuousCaptureData.push_back(AZStd::move(m_timeRegionMap)); + m_timeRegionMap.clear(); + m_continuousCaptureEndingMutex.unlock(); + } - // Clear all TLS that flagged themselves to be deleted, meaning that the thread is already terminated - AZStd::remove_if(m_registeredThreads.begin(), m_registeredThreads.end(), [](const RHI::Ptr& thread) - { - return thread->m_deleteFlag.load(); - }); + AZStd::unique_lock lock(m_threadRegisterMutex); - // Update our saved time regions to the last frame's collected data - m_timeRegionMap = AZStd::move(newMap); + // Iterate through all the threads, and collect the thread's cached time regions + TimeRegionMap newMap; + for (auto& threadLocal : m_registeredThreads) + { + ThreadTimeRegionMap& threadMapEntry = newMap[threadLocal->m_executingThreadId]; + threadLocal->TryFlushCachedMap(threadMapEntry); } - void CpuProfilerImpl::RegisterThreadStorage() + // Clear all TLS that flagged themselves to be deleted, meaning that the thread is already terminated + AZStd::remove_if(m_registeredThreads.begin(), m_registeredThreads.end(), [](const RHI::Ptr& thread) { - AZStd::unique_lock lock(m_threadRegisterMutex); - if (!ms_threadLocalStorage) - { - ms_threadLocalStorage = aznew CpuTimingLocalStorage(); - m_registeredThreads.emplace_back(ms_threadLocalStorage); - } - } + return thread->m_deleteFlag.load(); + }); - // --- CpuTimingLocalStorage --- + // Update our saved time regions to the last frame's collected data + m_timeRegionMap = AZStd::move(newMap); + } - CpuTimingLocalStorage::CpuTimingLocalStorage() + void CpuProfilerImpl::RegisterThreadStorage() + { + AZStd::unique_lock lock(m_threadRegisterMutex); + if (!ms_threadLocalStorage) { - m_executingThreadId = AZStd::this_thread::get_id(); + ms_threadLocalStorage = aznew CpuTimingLocalStorage(); + m_registeredThreads.emplace_back(ms_threadLocalStorage); } + } - CpuTimingLocalStorage::~CpuTimingLocalStorage() - { - m_deleteFlag = true; - } + // --- CpuTimingLocalStorage --- + + CpuTimingLocalStorage::CpuTimingLocalStorage() + { + m_executingThreadId = AZStd::this_thread::get_id(); + } - void CpuTimingLocalStorage::RegionStackPushBack(CachedTimeRegion& timeRegion) + CpuTimingLocalStorage::~CpuTimingLocalStorage() + { + m_deleteFlag = true; + } + + void CpuTimingLocalStorage::RegionStackPushBack(CachedTimeRegion& timeRegion) + { + // If it was (re)enabled, clear the lists first + if (m_clearContainers) { - // If it was (re)enabled, clear the lists first - if (m_clearContainers) - { - m_clearContainers = false; + m_clearContainers = false; - m_stackLevel = 0; - m_cachedTimeRegionMap.clear(); - m_timeRegionStack.clear(); - m_cachedTimeRegions.clear(); - } + m_stackLevel = 0; + m_cachedTimeRegionMap.clear(); + m_timeRegionStack.clear(); + m_cachedTimeRegions.clear(); + } - timeRegion.m_stackDepth = static_cast(m_stackLevel); + timeRegion.m_stackDepth = static_cast(m_stackLevel); - AZ_Assert(m_timeRegionStack.size() < TimeRegionStackSize, "Adding too many time regions to the stack. Increase the size of TimeRegionStackSize."); - m_timeRegionStack.push_back(timeRegion); + AZ_Assert(m_timeRegionStack.size() < TimeRegionStackSize, "Adding too many time regions to the stack. Increase the size of TimeRegionStackSize."); + m_timeRegionStack.push_back(timeRegion); - // Increment the stack - m_stackLevel++; + // Increment the stack + m_stackLevel++; - // Set the starting time at the end, to avoid recording the minor overhead - m_timeRegionStack.back().m_startTick = AZStd::GetTimeNowTicks(); - } + // Set the starting time at the end, to avoid recording the minor overhead + m_timeRegionStack.back().m_startTick = AZStd::GetTimeNowTicks(); + } - void CpuTimingLocalStorage::RegionStackPopBack() + void CpuTimingLocalStorage::RegionStackPopBack() + { + // Early out when the stack is empty, this might happen when the profiler was enabled while the thread encountered profiling markers + if (m_timeRegionStack.empty()) { - // Early out when the stack is empty, this might happen when the profiler was enabled while the thread encountered profiling markers - if (m_timeRegionStack.empty()) - { - return; - } + return; + } - // Get the end timestamp here, to avoid the minor overhead - const AZStd::sys_time_t endRegionTime = AZStd::GetTimeNowTicks(); + // Get the end timestamp here, to avoid the minor overhead + const AZStd::sys_time_t endRegionTime = AZStd::GetTimeNowTicks(); - AZ_Assert(!m_timeRegionStack.empty(), "Trying to pop an element in the stack, but it's empty."); - CachedTimeRegion back = m_timeRegionStack.back(); - m_timeRegionStack.pop_back(); + AZ_Assert(!m_timeRegionStack.empty(), "Trying to pop an element in the stack, but it's empty."); + CachedTimeRegion back = m_timeRegionStack.back(); + m_timeRegionStack.pop_back(); - // Set the ending time - back.m_endTick = endRegionTime; + // Set the ending time + back.m_endTick = endRegionTime; - // Decrement the stack - m_stackLevel--; + // Decrement the stack + m_stackLevel--; - // Add an entry to the cached region - AddCachedRegion(back); - } + // Add an entry to the cached region + AddCachedRegion(back); + } - // Gets called when region ends and all data is set - void CpuTimingLocalStorage::AddCachedRegion(const CachedTimeRegion& timeRegionCached) + // Gets called when region ends and all data is set + void CpuTimingLocalStorage::AddCachedRegion(const CachedTimeRegion& timeRegionCached) + { + if (m_hitSizeLimitMap[timeRegionCached.m_groupRegionName.m_regionName]) { - if (m_hitSizeLimitMap[timeRegionCached.m_groupRegionName.m_regionName]) - { - return; - } - // Add an entry to the cached region - m_cachedTimeRegions.push_back(timeRegionCached); - - // If the stack is empty, add it to the local cache map. Only gets called when the stack is empty - // NOTE: this is where the largest overhead will be, but due to it only being called when the stack is empty - // (i.e when the root region ended), this overhead won't affect any time regions. - // The exception being for functions that are being profiled and create/spawn threads that are also profiled. Unfortunately, in this - // case, the overhead of the profiled threads will be added to the main thread. - if (m_timeRegionStack.empty()) - { - AZStd::unique_lock lock(m_cachedTimeRegionMutex); + return; + } + // Add an entry to the cached region + m_cachedTimeRegions.push_back(timeRegionCached); + + // If the stack is empty, add it to the local cache map. Only gets called when the stack is empty + // NOTE: this is where the largest overhead will be, but due to it only being called when the stack is empty + // (i.e when the root region ended), this overhead won't affect any time regions. + // The exception being for functions that are being profiled and create/spawn threads that are also profiled. Unfortunately, in this + // case, the overhead of the profiled threads will be added to the main thread. + if (m_timeRegionStack.empty()) + { + AZStd::unique_lock lock(m_cachedTimeRegionMutex); - // Add the cached regions to the map - for (auto& cachedTimeRegion : m_cachedTimeRegions) + // Add the cached regions to the map + for (auto& cachedTimeRegion : m_cachedTimeRegions) + { + const AZStd::string regionName = cachedTimeRegion.m_groupRegionName.m_regionName; + AZStd::vector& regionVec = m_cachedTimeRegionMap[regionName]; + regionVec.push_back(cachedTimeRegion); + if (regionVec.size() >= TimeRegionStackSize) { - const AZStd::string regionName = cachedTimeRegion.m_groupRegionName.m_regionName; - AZStd::vector& regionVec = m_cachedTimeRegionMap[regionName]; - regionVec.push_back(cachedTimeRegion); - if (regionVec.size() >= TimeRegionStackSize) - { - m_hitSizeLimitMap.insert_or_assign(AZStd::move(regionName), true); - } + m_hitSizeLimitMap.insert_or_assign(AZStd::move(regionName), true); } - - // Clear the cached regions - m_cachedTimeRegions.clear(); } + + // Clear the cached regions + m_cachedTimeRegions.clear(); } + } - void CpuTimingLocalStorage::TryFlushCachedMap(CpuProfiler::ThreadTimeRegionMap& cachedTimeRegionMap) + void CpuTimingLocalStorage::TryFlushCachedMap(CpuProfiler::ThreadTimeRegionMap& cachedTimeRegionMap) + { + // Try to lock, if it's already in use (the cached regions in the array are being copied to the map) + // it'll show up in the next iteration when the user requests it. + if (m_cachedTimeRegionMutex.try_lock()) { - // Try to lock, if it's already in use (the cached regions in the array are being copied to the map) - // it'll show up in the next iteration when the user requests it. - if (m_cachedTimeRegionMutex.try_lock()) + // Only flush cached time regions if there are entries available + if (!m_cachedTimeRegionMap.empty()) { - // Only flush cached time regions if there are entries available - if (!m_cachedTimeRegionMap.empty()) - { - cachedTimeRegionMap = AZStd::move(m_cachedTimeRegionMap); - m_cachedTimeRegionMap.clear(); - m_hitSizeLimitMap.clear(); - } - m_cachedTimeRegionMutex.unlock(); + cachedTimeRegionMap = AZStd::move(m_cachedTimeRegionMap); + m_cachedTimeRegionMap.clear(); + m_hitSizeLimitMap.clear(); } + m_cachedTimeRegionMutex.unlock(); } + } - // --- CpuProfilingStatisticsSerializer --- + // --- CpuProfilingStatisticsSerializer --- - CpuProfilingStatisticsSerializer::CpuProfilingStatisticsSerializer(const AZStd::ring_buffer& continuousData) + CpuProfilingStatisticsSerializer::CpuProfilingStatisticsSerializer(const AZStd::ring_buffer& continuousData) + { + // Create serializable entries + for (const auto& timeRegionMap : continuousData) { - // Create serializable entries - for (const auto& timeRegionMap : continuousData) + for (const auto& [threadId, regionMap] : timeRegionMap) { - for (const auto& [threadId, regionMap] : timeRegionMap) + for (const auto& [regionName, regionVec] : regionMap) { - for (const auto& [regionName, regionVec] : regionMap) + for (const auto& region : regionVec) { - for (const auto& region : regionVec) - { - m_cpuProfilingStatisticsSerializerEntries.emplace_back(region, threadId); - } + m_cpuProfilingStatisticsSerializerEntries.emplace_back(region, threadId); } } } } + } - void CpuProfilingStatisticsSerializer::Reflect(AZ::ReflectContext* context) + void CpuProfilingStatisticsSerializer::Reflect(AZ::ReflectContext* context) + { + if (auto* serializeContext = azrtti_cast(context)) { - if (auto* serializeContext = azrtti_cast(context)) - { - serializeContext->Class() - ->Version(1) - ->Field("cpuProfilingStatisticsSerializerEntries", &CpuProfilingStatisticsSerializer::m_cpuProfilingStatisticsSerializerEntries) - ; - } - - CpuProfilingStatisticsSerializerEntry::Reflect(context); + serializeContext->Class() + ->Version(1) + ->Field("cpuProfilingStatisticsSerializerEntries", &CpuProfilingStatisticsSerializer::m_cpuProfilingStatisticsSerializerEntries) + ; } - // --- CpuProfilingStatisticsSerializerEntry --- + CpuProfilingStatisticsSerializerEntry::Reflect(context); + } - 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{}(threadId); - } + // --- CpuProfilingStatisticsSerializerEntry --- - void CpuProfilingStatisticsSerializer::CpuProfilingStatisticsSerializerEntry::Reflect(AZ::ReflectContext* context) + 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{}(threadId); + } + + void CpuProfilingStatisticsSerializer::CpuProfilingStatisticsSerializerEntry::Reflect(AZ::ReflectContext* context) + { + if (auto* serializeContext = azrtti_cast(context)) { - if (auto* serializeContext = azrtti_cast(context)) - { - serializeContext->Class() - ->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) - ->Field("threadId", &CpuProfilingStatisticsSerializerEntry::m_threadId) - ; - } + serializeContext->Class() + ->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) + ->Field("threadId", &CpuProfilingStatisticsSerializerEntry::m_threadId) + ; } - } // namespace RHI -} // namespace AZ + } +} // namespace Profiler diff --git a/Gems/Profiler/Code/Source/CpuProfilerImpl.h b/Gems/Profiler/Code/Source/CpuProfilerImpl.h index 29886625ea..b36494952f 100644 --- a/Gems/Profiler/Code/Source/CpuProfilerImpl.h +++ b/Gems/Profiler/Code/Source/CpuProfilerImpl.h @@ -20,169 +20,166 @@ #include -namespace AZ +namespace Profiler { - namespace RHI + //! Thread local class to keep track of the thread's cached time regions. + //! Each thread keeps track of its own time regions, which is communicated from the CpuProfilerImpl. + //! The CpuProfilerImpl is able to request the cached time regions from the CpuTimingLocalStorage. + class CpuTimingLocalStorage : + public AZStd::intrusive_refcount { - //! Thread local class to keep track of the thread's cached time regions. - //! Each thread keeps track of its own time regions, which is communicated from the CpuProfilerImpl. - //! The CpuProfilerImpl is able to request the cached time regions from the CpuTimingLocalStorage. - class CpuTimingLocalStorage : - public AZStd::intrusive_refcount - { - friend class CpuProfilerImpl; + friend class CpuProfilerImpl; - public: - AZ_CLASS_ALLOCATOR(CpuTimingLocalStorage, AZ::OSAllocator, 0); + public: + AZ_CLASS_ALLOCATOR(CpuTimingLocalStorage, AZ::OSAllocator, 0); - CpuTimingLocalStorage(); - ~CpuTimingLocalStorage(); + CpuTimingLocalStorage(); + ~CpuTimingLocalStorage(); - private: - // Maximum stack size - static constexpr uint32_t TimeRegionStackSize = 2048u; + private: + // Maximum stack size + static constexpr uint32_t TimeRegionStackSize = 2048u; - // Adds a region to the stack, gets called each time a region begins - void RegionStackPushBack(CachedTimeRegion& timeRegion); + // Adds a region to the stack, gets called each time a region begins + void RegionStackPushBack(CachedTimeRegion& timeRegion); - // Pops a region from the stack, gets called each time a region ends - void RegionStackPopBack(); + // Pops a region from the stack, gets called each time a region ends + void RegionStackPopBack(); - // Add a new cached time region. If the stack is empty, flush all entries to the cached map - void AddCachedRegion(const CachedTimeRegion& timeRegionCached); + // Add a new cached time region. If the stack is empty, flush all entries to the cached map + void AddCachedRegion(const CachedTimeRegion& timeRegionCached); - // Tries to flush the map to the passed parameter, only if the thread's mutex is unlocked - void TryFlushCachedMap(CpuProfiler::ThreadTimeRegionMap& cachedRegionMap); + // Tries to flush the map to the passed parameter, only if the thread's mutex is unlocked + void TryFlushCachedMap(CpuProfiler::ThreadTimeRegionMap& cachedRegionMap); - AZStd::thread_id m_executingThreadId; - // Keeps track of the current thread's stack depth - uint32_t m_stackLevel = 0u; + AZStd::thread_id m_executingThreadId; + // Keeps track of the current thread's stack depth + uint32_t m_stackLevel = 0u; - // Cached region map, will be flushed to the system's map when the system requests it - CpuProfiler::ThreadTimeRegionMap m_cachedTimeRegionMap; + // Cached region map, will be flushed to the system's map when the system requests it + CpuProfiler::ThreadTimeRegionMap m_cachedTimeRegionMap; - // Use fixed vectors to avoid re-allocating new elements - // Keeps track of the regions that added and removed using the macro - AZStd::fixed_vector m_timeRegionStack; + // Use fixed vectors to avoid re-allocating new elements + // Keeps track of the regions that added and removed using the macro + AZStd::fixed_vector m_timeRegionStack; - // Keeps track of regions that completed (i.e regions that was pushed and popped from the stack) - // Intermediate storage point for the CachedTimeRegions, when the stack is empty, all entries will be - // copied to the map. - AZStd::fixed_vector m_cachedTimeRegions; - AZStd::mutex m_cachedTimeRegionMutex; + // Keeps track of regions that completed (i.e regions that was pushed and popped from the stack) + // Intermediate storage point for the CachedTimeRegions, when the stack is empty, all entries will be + // copied to the map. + AZStd::fixed_vector m_cachedTimeRegions; + AZStd::mutex m_cachedTimeRegionMutex; - // Dirty flag which is set when the CpuProfiler's enabled state is set from false to true - AZStd::atomic_bool m_clearContainers = false; + // Dirty flag which is set when the CpuProfiler's enabled state is set from false to true + AZStd::atomic_bool m_clearContainers = false; - // When the thread is terminated, it will flag itself for deletion - AZStd::atomic_bool m_deleteFlag = false; + // When the thread is terminated, it will flag itself for deletion + AZStd::atomic_bool m_deleteFlag = false; - // Keep track of the regions that have hit the size limit so we don't have to lock to check - AZStd::map m_hitSizeLimitMap; - }; + // Keep track of the regions that have hit the size limit so we don't have to lock to check + AZStd::map m_hitSizeLimitMap; + }; - //! CpuProfiler will keep track of the registered threads, and - //! forwards the request to profile a region to the appropriate thread. The user is able to request all - //! cached regions, which are stored on a per thread frequency. - class CpuProfilerImpl final - : public AZ::Debug::Profiler - , public CpuProfiler - , public SystemTickBus::Handler - { - friend class CpuTimingLocalStorage; + //! CpuProfiler will keep track of the registered threads, and + //! forwards the request to profile a region to the appropriate thread. The user is able to request all + //! cached regions, which are stored on a per thread frequency. + class CpuProfilerImpl final + : public AZ::Debug::Profiler + , public CpuProfiler + , public SystemTickBus::Handler + { + friend class CpuTimingLocalStorage; - public: - AZ_TYPE_INFO(CpuProfilerImpl, "{10E9D394-FC83-4B45-B2B8-807C6BF07BF0}"); - AZ_CLASS_ALLOCATOR(CpuProfilerImpl, AZ::OSAllocator, 0); + public: + AZ_TYPE_INFO(CpuProfilerImpl, "{10E9D394-FC83-4B45-B2B8-807C6BF07BF0}"); + AZ_CLASS_ALLOCATOR(CpuProfilerImpl, AZ::OSAllocator, 0); - CpuProfilerImpl() = default; - ~CpuProfilerImpl() = default; + CpuProfilerImpl() = default; + ~CpuProfilerImpl() = default; - //! Registers the CpuProfilerImpl instance to the interface - void Init(); - //! Unregisters the CpuProfilerImpl instance from the interface - void Shutdown(); + //! Registers the CpuProfilerImpl instance to the interface + void Init(); + //! Unregisters the CpuProfilerImpl instance from the interface + void Shutdown(); - // SystemTickBus::Handler overrides - // When fired, the profiler collects all profiling data from registered threads and updates - // m_timeRegionMap so that the next frame has up-to-date profiling data. - void OnSystemTick() final override; + // SystemTickBus::Handler overrides + // When fired, the profiler collects all profiling data from registered threads and updates + // m_timeRegionMap so that the next frame has up-to-date profiling data. + void OnSystemTick() final override; - //! AZ::Debug::Profiler overrides... - void BeginRegion(const AZ::Debug::Budget* budget, const char* eventName) final override; - void EndRegion(const AZ::Debug::Budget* budget) final override; + //! AZ::Debug::Profiler overrides... + void BeginRegion(const AZ::Debug::Budget* budget, const char* eventName) final override; + void EndRegion(const AZ::Debug::Budget* budget) final override; - //! CpuProfiler overrides... - const TimeRegionMap& GetTimeRegionMap() const final override; - bool BeginContinuousCapture() final override; - bool EndContinuousCapture(AZStd::ring_buffer& flushTarget) final override; - bool IsContinuousCaptureInProgress() const final override; - void SetProfilerEnabled(bool enabled) final override; - bool IsProfilerEnabled() const final override; + //! CpuProfiler overrides... + const TimeRegionMap& GetTimeRegionMap() const final override; + bool BeginContinuousCapture() final override; + bool EndContinuousCapture(AZStd::ring_buffer& flushTarget) final override; + bool IsContinuousCaptureInProgress() const final override; + void SetProfilerEnabled(bool enabled) final override; + bool IsProfilerEnabled() const final override; - private: - static constexpr AZStd::size_t MaxFramesToSave = 2 * 60 * 120; // 2 minutes of 120fps - static constexpr AZStd::size_t MaxRegionStringPoolSize = 16384; // Max amount of unique strings to save in the pool before throwing warnings. + private: + static constexpr AZStd::size_t MaxFramesToSave = 2 * 60 * 120; // 2 minutes of 120fps + static constexpr AZStd::size_t MaxRegionStringPoolSize = 16384; // Max amount of unique strings to save in the pool before throwing warnings. - // Lazily create and register the local thread data - void RegisterThreadStorage(); + // Lazily create and register the local thread data + void RegisterThreadStorage(); - // ThreadId -> ThreadTimeRegionMap - // On the start of each frame, this map will be updated with the last frame's profiling data. - TimeRegionMap m_timeRegionMap; + // ThreadId -> ThreadTimeRegionMap + // On the start of each frame, this map will be updated with the last frame's profiling data. + TimeRegionMap m_timeRegionMap; - // Set of registered threads when created - AZStd::vector, AZ::OSStdAllocator> m_registeredThreads; - AZStd::mutex m_threadRegisterMutex; + // Set of registered threads when created + AZStd::vector, AZ::OSStdAllocator> m_registeredThreads; + AZStd::mutex m_threadRegisterMutex; - // Thread local storage, gets lazily allocated when a thread is created - static thread_local CpuTimingLocalStorage* ms_threadLocalStorage; + // Thread local storage, gets lazily allocated when a thread is created + static thread_local CpuTimingLocalStorage* ms_threadLocalStorage; - // Enable/Disables the threads from profiling - AZStd::atomic_bool m_enabled = false; + // Enable/Disables the threads from profiling + AZStd::atomic_bool m_enabled = false; - // This lock will only be contested when the CpuProfiler's Shutdown() method has been called - AZStd::shared_mutex m_shutdownMutex; + // This lock will only be contested when the CpuProfiler's Shutdown() method has been called + AZStd::shared_mutex m_shutdownMutex; - bool m_initialized = false; + bool m_initialized = false; - AZStd::mutex m_continuousCaptureEndingMutex; + AZStd::mutex m_continuousCaptureEndingMutex; - AZStd::atomic_bool m_continuousCaptureInProgress; + AZStd::atomic_bool m_continuousCaptureInProgress; - // Stores multiple frames of profiling data, size is controlled by MaxFramesToSave. Flushed when EndContinuousCapture is called. - // Ring buffer so that we can have fast append of new data + removal of old profiling data with good cache locality. - AZStd::ring_buffer m_continuousCaptureData; - }; + // Stores multiple frames of profiling data, size is controlled by MaxFramesToSave. Flushed when EndContinuousCapture is called. + // Ring buffer so that we can have fast append of new data + removal of old profiling data with good cache locality. + AZStd::ring_buffer m_continuousCaptureData; + }; - // Intermediate class to serialize Cpu TimedRegion data. - class CpuProfilingStatisticsSerializer + // Intermediate class to serialize Cpu TimedRegion data. + class CpuProfilingStatisticsSerializer + { + public: + class CpuProfilingStatisticsSerializerEntry { 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, 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}"); + AZ_TYPE_INFO(CpuProfilingStatisticsSerializer::CpuProfilingStatisticsSerializerEntry, "{26B78F65-EB96-46E2-BE7E-A1233880B225}"); static void Reflect(AZ::ReflectContext* context); - CpuProfilingStatisticsSerializer() = default; - CpuProfilingStatisticsSerializer(const AZStd::ring_buffer& continuousData); + CpuProfilingStatisticsSerializerEntry() = default; + CpuProfilingStatisticsSerializerEntry(const RHI::CachedTimeRegion& cachedTimeRegion, AZStd::thread_id threadId); - AZStd::vector m_cpuProfilingStatisticsSerializerEntries; + 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; }; - }; // namespace RHI -}; // namespace AZ + + AZ_TYPE_INFO(CpuProfilingStatisticsSerializer, "{D5B02946-0D27-474F-9A44-364C2706DD41}"); + static void Reflect(AZ::ReflectContext* context); + + CpuProfilingStatisticsSerializer() = default; + CpuProfilingStatisticsSerializer(const AZStd::ring_buffer& continuousData); + + AZStd::vector m_cpuProfilingStatisticsSerializerEntries; + }; +}; // namespace Profiler diff --git a/Gems/Profiler/Code/Source/ImGuiCpuProfiler.cpp b/Gems/Profiler/Code/Source/ImGuiCpuProfiler.cpp index daf6eca5c0..c9471157a0 100644 --- a/Gems/Profiler/Code/Source/ImGuiCpuProfiler.cpp +++ b/Gems/Profiler/Code/Source/ImGuiCpuProfiler.cpp @@ -25,138 +25,191 @@ #include -namespace AZ +namespace Profiler { - namespace Render + namespace CpuProfilerImGuiHelper { - namespace CpuProfilerImGuiHelper + inline float TicksToMs(double ticks) { - inline float TicksToMs(double ticks) + // Note: converting to microseconds integer before converting to milliseconds float + const AZStd::sys_time_t ticksPerSecond = AZStd::GetTimeTicksPerSecond(); + AZ_Assert(ticksPerSecond >= 1000, "Error in converting ticks to ms, expected ticksPerSecond >= 1000"); + return static_cast((ticks * 1000) / (ticksPerSecond / 1000)) / 1000.0f; + } + + inline float TicksToMs(AZStd::sys_time_t ticks) + { + return TicksToMs(static_cast(ticks)); + } + + using DeserializedCpuData = AZStd::vector; + inline Outcome LoadSavedCpuProfilingStatistics(const AZStd::string& capturePath) + { + auto* base = IO::FileIOBase::GetInstance(); + + char resolvedPath[IO::MaxPathLength]; + if (!base->ResolvePath(capturePath.c_str(), resolvedPath, IO::MaxPathLength)) { - // Note: converting to microseconds integer before converting to milliseconds float - const AZStd::sys_time_t ticksPerSecond = AZStd::GetTimeTicksPerSecond(); - AZ_Assert(ticksPerSecond >= 1000, "Error in converting ticks to ms, expected ticksPerSecond >= 1000"); - return static_cast((ticks * 1000) / (ticksPerSecond / 1000)) / 1000.0f; + return Failure(AZStd::string::format("Could not resolve the path to file %s, is the path correct?", resolvedPath)); } - inline float TicksToMs(AZStd::sys_time_t ticks) + u64 captureSizeBytes; + const IO::Result fileSizeResult = base->Size(resolvedPath, captureSizeBytes); + if (!fileSizeResult) { - return TicksToMs(static_cast(ticks)); + return Failure(AZStd::string::format("Could not read the size of file %s, is the path correct?", resolvedPath)); } - using DeserializedCpuData = AZStd::vector; - inline Outcome LoadSavedCpuProfilingStatistics(const AZStd::string& capturePath) + // 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) { - auto* base = IO::FileIOBase::GetInstance(); + return Failure(AZStd::string::format("Could not fopen file %s, is the path correct?\n", resolvedPath)); + } - 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)); - } + constexpr AZStd::size_t MaxBufSize = 65536; + const AZStd::size_t bufSize = AZStd::min(MaxBufSize, aznumeric_cast(captureSizeBytes)); + char* buf = reinterpret_cast(azmalloc(bufSize)); - 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)); - } + rapidjson::Document document; + rapidjson::FileReadStream inputStream(fp, buf, bufSize); + document.ParseStream(inputStream); - // 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)); - } + azfree(buf); + fclose(fp); - constexpr AZStd::size_t MaxBufSize = 65536; - const AZStd::size_t bufSize = AZStd::min(MaxBufSize, aznumeric_cast(captureSizeBytes)); - char* buf = reinterpret_cast(azmalloc(bufSize)); + 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)); + } - rapidjson::Document document; - rapidjson::FileReadStream inputStream(fp, buf, bufSize); - document.ParseStream(inputStream); + 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")); + } - azfree(buf); - fclose(fp); + AZ_TracePrintf("JsonUtils", "Successfully loaded JSON into memory.\n"); - 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)); - } + 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())); + } - 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 CPU profiling data with %zu profiling entries.\n", + serializer.m_cpuProfilingStatisticsSerializerEntries.size()); + + return Success(AZStd::move(serializer.m_cpuProfilingStatisticsSerializerEntries)); + } + } // namespace CpuProfilerImGuiHelper - 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()); + inline void ImGuiCpuProfiler::Draw(bool& keepDrawing) + { + // Cache the value to detect if it was changed by ImGui(user pressed 'x') + const bool cachedShowCpuProfiler = keepDrawing; + + const ImVec2 windowSize(900.0f, 600.0f); + ImGui::SetNextWindowSize(windowSize, ImGuiCond_Once); + if (ImGui::Begin("CPU Profiler", &keepDrawing, ImGuiWindowFlags_None)) + { + // Collect the last frame's profiling data + if (!m_paused) + { + // Update region map and cache the input cpu timing statistics when the profiling is not paused + CacheCpuTimingStatistics(); + + CollectFrameData(); + CullFrameData(); - return Success(AZStd::move(serializer.m_cpuProfilingStatisticsSerializerEntries)); + // Only listen to system ticks when the profiler is active + if (!SystemTickBus::Handler::BusIsConnected()) + { + SystemTickBus::Handler::BusConnect(); + } } - } // namespace CpuProfilerImGuiHelper + if (m_enableVisualizer) + { + DrawVisualizer(); + } + else + { + DrawStatisticsView(); + } + if (m_showFilePicker) + { + DrawFilePicker(); + } + } + ImGui::End(); - inline void ImGuiCpuProfiler::Draw(bool& keepDrawing) + if (m_captureToFile) { - // Cache the value to detect if it was changed by ImGui(user pressed 'x') - const bool cachedShowCpuProfiler = keepDrawing; + 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; - const ImVec2 windowSize(900.0f, 600.0f); - ImGui::SetNextWindowSize(windowSize, ImGuiCond_Once); - if (ImGui::Begin("CPU Profiler", &keepDrawing, ImGuiWindowFlags_None)) - { - // Collect the last frame's profiling data - if (!m_paused) - { - // Update region map and cache the input cpu timing statistics when the profiling is not paused - CacheCpuTimingStatistics(); + // Toggle if the bool isn't the same as the cached value + if (cachedShowCpuProfiler != keepDrawing) + { + AZ::RHI::CpuProfiler::Get()->SetProfilerEnabled(keepDrawing); + } + } - CollectFrameData(); - CullFrameData(); + inline void ImGuiCpuProfiler::DrawCommonHeader() + { + if (!m_lastCapturedFilePath.empty()) + { + ImGui::Text("Saved: %s", m_lastCapturedFilePath.c_str()); + } - // Only listen to system ticks when the profiler is active - if (!SystemTickBus::Handler::BusIsConnected()) - { - SystemTickBus::Handler::BusConnect(); - } - } + if (ImGui::Button(m_enableVisualizer ? "Swap to statistics" : "Swap to visualizer")) + { + m_enableVisualizer = !m_enableVisualizer; + } - if (m_enableVisualizer) - { - DrawVisualizer(); - } - else - { - DrawStatisticsView(); - } + 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); + } - if (m_showFilePicker) - { - DrawFilePicker(); - } - } - ImGui::End(); + ImGui::SameLine(); + if (ImGui::Button("Capture")) + { + m_captureToFile = true; + } - if (m_captureToFile) + ImGui::SameLine(); + bool isInProgress = RHI::CpuProfiler::Get()->IsContinuousCaptureInProgress(); + if (ImGui::Button(isInProgress ? "End" : "Begin")) + { + if (isInProgress) { AZStd::sys_time_t timeNow = AZStd::GetTimeNowSecond(); AZStd::string timeString; @@ -170,990 +223,934 @@ namespace AZ 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); + &AZ::Render::ProfilingCaptureRequestBus::Events::EndContinuousCpuProfilingCapture, frameDataFilePath); + m_paused = true; } - m_captureToFile = false; - // Toggle if the bool isn't the same as the cached value - if (cachedShowCpuProfiler != keepDrawing) + else { - AZ::RHI::CpuProfiler::Get()->SetProfilerEnabled(keepDrawing); + AZ::Render::ProfilingCaptureRequestBus::Broadcast( + &AZ::Render::ProfilingCaptureRequestBus::Events::BeginContinuousCpuProfilingCapture); } } - inline void ImGuiCpuProfiler::DrawCommonHeader() + ImGui::SameLine(); + if (ImGui::Button("Load file")) { - if (!m_lastCapturedFilePath.empty()) - { - ImGui::Text("Saved: %s", m_lastCapturedFilePath.c_str()); - } + m_showFilePicker = true; - if (ImGui::Button(m_enableVisualizer ? "Swap to statistics" : "Swap to visualizer")) - { - m_enableVisualizer = !m_enableVisualizer; - } + // 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"; - 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); - } + 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; + }); - ImGui::SameLine(); - if (ImGui::Button("Capture")) + // 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()); + }); + } + } + + inline void ImGuiCpuProfiler::DrawTable() + { + const auto flags = + ImGuiTableFlags_Borders | ImGuiTableFlags_Sortable | ImGuiTableFlags_Resizable | ImGuiTableFlags_Reorderable; + if (ImGui::BeginTable("FunctionStatisticsTable", 6, flags)) + { + // Table header setup + ImGui::TableSetupColumn("Group"); + ImGui::TableSetupColumn("Region"); + ImGui::TableSetupColumn("MTPC (ms)"); + ImGui::TableSetupColumn("Max (ms)"); + ImGui::TableSetupColumn("Invocations"); + ImGui::TableSetupColumn("Total (ms)"); + ImGui::TableHeadersRow(); + ImGui::TableNextColumn(); + + ImGuiTableSortSpecs* sortSpecs = ImGui::TableGetSortSpecs(); + if (sortSpecs && sortSpecs->SpecsDirty) { - m_captureToFile = true; + SortTable(sortSpecs); } - ImGui::SameLine(); - bool isInProgress = RHI::CpuProfiler::Get()->IsContinuousCaptureInProgress(); - if (ImGui::Button(isInProgress ? "End" : "Begin")) + // Draw all of the rows held in the GroupRegionMap + for (const auto* statistics : m_tableData) { - if (isInProgress) + if (!m_timedRegionFilter.PassFilter(statistics->m_groupName.c_str()) + && !m_timedRegionFilter.PassFilter(statistics->m_regionName.c_str())) { - 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::EndContinuousCpuProfilingCapture, frameDataFilePath); - m_paused = true; + continue; } - else - { - AZ::Render::ProfilingCaptureRequestBus::Broadcast( - &AZ::Render::ProfilingCaptureRequestBus::Events::BeginContinuousCpuProfilingCapture); - } - } + ImGui::Text("%s", statistics->m_groupName.c_str()); + const ImVec2 topLeftBound = ImGui::GetItemRectMin(); + ImGui::TableNextColumn(); - ImGui::SameLine(); - if (ImGui::Button("Load file")) - { - m_showFilePicker = true; + ImGui::Text("%s", statistics->m_regionName.c_str()); + ImGui::TableNextColumn(); - // 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"; + ImGui::Text("%.2f", CpuProfilerImGuiHelper::TicksToMs(statistics->m_runningAverageTicks)); + ImGui::TableNextColumn(); - 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; - }); + ImGui::Text("%.2f", CpuProfilerImGuiHelper::TicksToMs(statistics->m_maxTicks)); + ImGui::TableNextColumn(); - // 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()); - }); - } - } + ImGui::Text("%llu", statistics->m_invocationsLastFrame); + ImGui::TableNextColumn(); - inline void ImGuiCpuProfiler::DrawTable() - { - const auto flags = - ImGuiTableFlags_Borders | ImGuiTableFlags_Sortable | ImGuiTableFlags_Resizable | ImGuiTableFlags_Reorderable; - if (ImGui::BeginTable("FunctionStatisticsTable", 6, flags)) - { - // Table header setup - ImGui::TableSetupColumn("Group"); - ImGui::TableSetupColumn("Region"); - ImGui::TableSetupColumn("MTPC (ms)"); - ImGui::TableSetupColumn("Max (ms)"); - ImGui::TableSetupColumn("Invocations"); - ImGui::TableSetupColumn("Total (ms)"); - ImGui::TableHeadersRow(); + ImGui::Text("%.2f", CpuProfilerImGuiHelper::TicksToMs(statistics->m_lastFrameTotalTicks)); + const ImVec2 botRightBound = ImGui::GetItemRectMax(); ImGui::TableNextColumn(); - ImGuiTableSortSpecs* sortSpecs = ImGui::TableGetSortSpecs(); - if (sortSpecs && sortSpecs->SpecsDirty) + // NOTE: we are manually checking the bounds rather than using ImGui::IsItemHovered + Begin/EndGroup because + // ImGui reports incorrect bounds when using Begin/End group in the Tables API. + if (ImGui::IsWindowHovered() && ImGui::IsMouseHoveringRect(topLeftBound, botRightBound, false)) { - SortTable(sortSpecs); + ImGui::BeginTooltip(); + ImGui::Text("%s", statistics->GetExecutingThreadsLabel().c_str()); + ImGui::EndTooltip(); } + } + } + ImGui::EndTable(); + } - // Draw all of the rows held in the GroupRegionMap - for (const auto* statistics : m_tableData) - { - if (!m_timedRegionFilter.PassFilter(statistics->m_groupName.c_str()) - && !m_timedRegionFilter.PassFilter(statistics->m_regionName.c_str())) - { - continue; - } - - ImGui::Text("%s", statistics->m_groupName.c_str()); - const ImVec2 topLeftBound = ImGui::GetItemRectMin(); - ImGui::TableNextColumn(); + inline void ImGuiCpuProfiler::SortTable(ImGuiTableSortSpecs* sortSpecs) + { + const bool ascending = sortSpecs->Specs->SortDirection == ImGuiSortDirection_Ascending; + const ImS16 columnToSort = sortSpecs->Specs->ColumnIndex; - ImGui::Text("%s", statistics->m_regionName.c_str()); - ImGui::TableNextColumn(); + switch (columnToSort) + { + case (0): // Sort by group name + AZStd::sort(m_tableData.begin(), m_tableData.end(), TableRow::TableRowCompareFunctor(&TableRow::m_groupName, ascending)); + break; + case (1): // Sort by region name + AZStd::sort(m_tableData.begin(), m_tableData.end(), TableRow::TableRowCompareFunctor(&TableRow::m_regionName, ascending)); + break; + case (2): // Sort by average time + AZStd::sort(m_tableData.begin(), m_tableData.end(), TableRow::TableRowCompareFunctor(&TableRow::m_runningAverageTicks, ascending)); + break; + case (3): // Sort by max time + AZStd::sort(m_tableData.begin(), m_tableData.end(), TableRow::TableRowCompareFunctor(&TableRow::m_maxTicks, ascending)); + break; + case (4): // Sort by invocations + AZStd::sort(m_tableData.begin(), m_tableData.end(), TableRow::TableRowCompareFunctor(&TableRow::m_invocationsLastFrame, ascending)); + break; + case (5): // Sort by total time + AZStd::sort(m_tableData.begin(), m_tableData.end(), TableRow::TableRowCompareFunctor(&TableRow::m_lastFrameTotalTicks, ascending)); + break; + } + sortSpecs->SpecsDirty = false; + } - ImGui::Text("%.2f", CpuProfilerImGuiHelper::TicksToMs(statistics->m_runningAverageTicks)); - ImGui::TableNextColumn(); + inline void ImGuiCpuProfiler::DrawStatisticsView() + { + DrawCommonHeader(); - ImGui::Text("%.2f", CpuProfilerImGuiHelper::TicksToMs(statistics->m_maxTicks)); - ImGui::TableNextColumn(); + const auto ShowRow = [](const char* regionLabel, double duration) + { + ImGui::Text("%s", regionLabel); + ImGui::NextColumn(); - ImGui::Text("%llu", statistics->m_invocationsLastFrame); - ImGui::TableNextColumn(); + ImGui::Text("%.2f ms", CpuProfilerImGuiHelper::TicksToMs(duration)); + ImGui::NextColumn(); + }; - ImGui::Text("%.2f", CpuProfilerImGuiHelper::TicksToMs(statistics->m_lastFrameTotalTicks)); - const ImVec2 botRightBound = ImGui::GetItemRectMax(); - ImGui::TableNextColumn(); + if (ImGui::BeginChild("Statistics View", { 0, 0 }, true)) + { + // Set column settings. + ImGui::Columns(2, "view", false); + ImGui::SetColumnWidth(0, 660.0f); + ImGui::SetColumnWidth(1, 100.0f); - // NOTE: we are manually checking the bounds rather than using ImGui::IsItemHovered + Begin/EndGroup because - // ImGui reports incorrect bounds when using Begin/End group in the Tables API. - if (ImGui::IsWindowHovered() && ImGui::IsMouseHoveringRect(topLeftBound, botRightBound, false)) - { - ImGui::BeginTooltip(); - ImGui::Text("%s", statistics->GetExecutingThreadsLabel().c_str()); - ImGui::EndTooltip(); - } - } + for (const auto& queueStatistics : m_cpuTimingStatisticsWhenPause) + { + ShowRow(queueStatistics.m_name.c_str(), queueStatistics.m_executeDuration); } - ImGui::EndTable(); - } - inline void ImGuiCpuProfiler::SortTable(ImGuiTableSortSpecs* sortSpecs) - { - const bool ascending = sortSpecs->Specs->SortDirection == ImGuiSortDirection_Ascending; - const ImS16 columnToSort = sortSpecs->Specs->ColumnIndex; + ImGui::Separator(); + ImGui::Columns(1, "view", false); - switch (columnToSort) + m_timedRegionFilter.Draw("Filter"); + ImGui::SameLine(); + if (ImGui::Button("Clear Filter")) { - case (0): // Sort by group name - AZStd::sort(m_tableData.begin(), m_tableData.end(), TableRow::TableRowCompareFunctor(&TableRow::m_groupName, ascending)); - break; - case (1): // Sort by region name - AZStd::sort(m_tableData.begin(), m_tableData.end(), TableRow::TableRowCompareFunctor(&TableRow::m_regionName, ascending)); - break; - case (2): // Sort by average time - AZStd::sort(m_tableData.begin(), m_tableData.end(), TableRow::TableRowCompareFunctor(&TableRow::m_runningAverageTicks, ascending)); - break; - case (3): // Sort by max time - AZStd::sort(m_tableData.begin(), m_tableData.end(), TableRow::TableRowCompareFunctor(&TableRow::m_maxTicks, ascending)); - break; - case (4): // Sort by invocations - AZStd::sort(m_tableData.begin(), m_tableData.end(), TableRow::TableRowCompareFunctor(&TableRow::m_invocationsLastFrame, ascending)); - break; - case (5): // Sort by total time - AZStd::sort(m_tableData.begin(), m_tableData.end(), TableRow::TableRowCompareFunctor(&TableRow::m_lastFrameTotalTicks, ascending)); - break; + m_timedRegionFilter.Clear(); } - sortSpecs->SpecsDirty = false; + ImGui::SameLine(); + if (ImGui::Button("Reset Table")) + { + m_tableData.clear(); + m_groupRegionMap.clear(); + } + + DrawTable(); } + } - inline void ImGuiCpuProfiler::DrawStatisticsView() + inline void ImGuiCpuProfiler::DrawFilePicker() + { + ImGui::SetNextWindowSize({ 500, 200 }, ImGuiCond_Once); + if (ImGui::Begin("File Picker", &m_showFilePicker)) { - DrawCommonHeader(); - - const auto ShowRow = [](const char* regionLabel, double duration) + if (ImGui::Button("Load selected")) { - ImGui::Text("%s", regionLabel); - ImGui::NextColumn(); - - ImGui::Text("%.2f ms", CpuProfilerImGuiHelper::TicksToMs(duration)); - ImGui::NextColumn(); - }; + LoadFile(); + } - if (ImGui::BeginChild("Statistics View", { 0, 0 }, true)) + auto getter = [](void* vectorPointer, int idx, const char** out_text) -> bool { - // Set column settings. - ImGui::Columns(2, "view", false); - ImGui::SetColumnWidth(0, 660.0f); - ImGui::SetColumnWidth(1, 100.0f); - - for (const auto& queueStatistics : m_cpuTimingStatisticsWhenPause) - { - ShowRow(queueStatistics.m_name.c_str(), queueStatistics.m_executeDuration); - } - - ImGui::Separator(); - ImGui::Columns(1, "view", false); - - m_timedRegionFilter.Draw("Filter"); - ImGui::SameLine(); - if (ImGui::Button("Clear Filter")) - { - m_timedRegionFilter.Clear(); - } - ImGui::SameLine(); - if (ImGui::Button("Reset Table")) + const auto& pathVec = *static_cast*>(vectorPointer); + if (idx < 0 || idx >= pathVec.size()) { - m_tableData.clear(); - m_groupRegionMap.clear(); + return false; } + *out_text = pathVec[idx].c_str(); + return true; + }; - DrawTable(); - } + ImGui::SetNextItemWidth(ImGui::GetWindowContentRegionWidth()); + ImGui::ListBox("", &m_currentFileIndex, getter, &m_cachedCapturePaths, aznumeric_cast(m_cachedCapturePaths.size())); } + ImGui::End(); + } - inline void ImGuiCpuProfiler::DrawFilePicker() + inline void ImGuiCpuProfiler::LoadFile() + { + const IO::Path& pathToLoad = m_cachedCapturePaths[m_currentFileIndex]; + auto loadResult = CpuProfilerImGuiHelper::LoadSavedCpuProfilingStatistics(pathToLoad.String()); + if (!loadResult.IsSuccess()) { - ImGui::SetNextWindowSize({ 500, 200 }, ImGuiCond_Once); - if (ImGui::Begin("File Picker", &m_showFilePicker)) - { - if (ImGui::Button("Load selected")) - { - LoadFile(); - } + AZ_TracePrintf("ImGuiCpuProfiler", "%s", loadResult.GetError().c_str()); + return; + } - auto getter = [](void* vectorPointer, int idx, const char** out_text) -> bool - { - const auto& pathVec = *static_cast*>(vectorPointer); - if (idx < 0 || idx >= pathVec.size()) - { - return false; - } - *out_text = pathVec[idx].c_str(); - return true; - }; + AZStd::vector deserializedData = loadResult.TakeValue(); - ImGui::SetNextItemWidth(ImGui::GetWindowContentRegionWidth()); - ImGui::ListBox("", &m_currentFileIndex, getter, &m_cachedCapturePaths, aznumeric_cast(m_cachedCapturePaths.size())); - } - ImGui::End(); - } + // 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(); - inline void ImGuiCpuProfiler::LoadFile() + m_tableData.clear(); + m_groupRegionMap.clear(); + + for (const auto& entry : deserializedData) { - const IO::Path& pathToLoad = m_cachedCapturePaths[m_currentFileIndex]; - auto loadResult = CpuProfilerImGuiHelper::LoadSavedCpuProfilingStatistics(pathToLoad.String()); - if (!loadResult.IsSuccess()) + 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) { - AZ_TracePrintf("ImGuiCpuProfiler", "%s", loadResult.GetError().c_str()); - return; + m_frameEndTicks.push_back(entry.m_endTick); } - AZStd::vector 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(); + // 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); + } - m_tableData.clear(); - m_groupRegionMap.clear(); + // Update viewport bounds with some added UX fudge factor + m_viewportStartTick = deserializedData.back().m_startTick - 1000; + m_viewportEndTick = deserializedData.back().m_endTick + 1000; - for (const auto& entry : deserializedData) + // 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) { - 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()); + return lhs.m_startTick < rhs.m_startTick; + }); + } + } - const RHI::CachedTimeRegion newRegion(*groupRegionNameItr, entry.m_stackDepth, entry.m_startTick, entry.m_endTick); - m_savedData[entry.m_threadId].push_back(newRegion); + // -- CPU Visualizer -- + inline void ImGuiCpuProfiler::DrawVisualizer() + { + DrawCommonHeader(); - // 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); - } + // Options & Statistics + if (ImGui::BeginChild("Options and Statistics", { 0, 0 }, true)) + { + ImGui::Columns(3, "Options", true); + ImGui::SliderInt("Saved Frames", &m_framesToCollect, 10, 20000, "%d", ImGuiSliderFlags_AlwaysClamp | ImGuiSliderFlags_Logarithmic); + m_visualizerHighlightFilter.Draw("Find Region"); - // 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); - } + ImGui::NextColumn(); - // Update viewport bounds with some added UX fudge factor - m_viewportStartTick = deserializedData.back().m_startTick - 1000; - m_viewportEndTick = deserializedData.back().m_endTick + 1000; + ImGui::Text("Viewport width: %.3f ms", CpuProfilerImGuiHelper::TicksToMs(GetViewportTickWidth())); + ImGui::Text("Ticks [%lld , %lld]", m_viewportStartTick, m_viewportEndTick); + ImGui::Text("Recording %zu threads", m_savedData.size()); + ImGui::Text("%llu profiling events saved", m_savedRegionCount); - // 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; - }); - } - } + ImGui::NextColumn(); - // -- CPU Visualizer -- - inline void ImGuiCpuProfiler::DrawVisualizer() - { - DrawCommonHeader(); + ImGui::TextWrapped( + "Hold the right mouse button to move around. Zoom by scrolling the mouse wheel while holding ."); + } - // Options & Statistics - if (ImGui::BeginChild("Options and Statistics", { 0, 0 }, true)) - { - ImGui::Columns(3, "Options", true); - ImGui::SliderInt("Saved Frames", &m_framesToCollect, 10, 20000, "%d", ImGuiSliderFlags_AlwaysClamp | ImGuiSliderFlags_Logarithmic); - m_visualizerHighlightFilter.Draw("Find Region"); + ImGui::Columns(1, "FrameTimeColumn", true); - ImGui::NextColumn(); + if (ImGui::BeginChild("FrameTimeHistogram", { 0, 50 }, true, ImGuiWindowFlags_NoScrollbar)) + { + DrawFrameTimeHistogram(); + } + ImGui::EndChild(); - ImGui::Text("Viewport width: %.3f ms", CpuProfilerImGuiHelper::TicksToMs(GetViewportTickWidth())); - ImGui::Text("Ticks [%lld , %lld]", m_viewportStartTick, m_viewportEndTick); - ImGui::Text("Recording %zu threads", m_savedData.size()); - ImGui::Text("%llu profiling events saved", m_savedRegionCount); + ImGui::Columns(1, "RulerColumn", true); - ImGui::NextColumn(); + // Ruler + if (ImGui::BeginChild("Ruler", { 0, 30 }, true, ImGuiWindowFlags_NoNavFocus)) + { + DrawRuler(); + } + ImGui::EndChild(); - ImGui::TextWrapped( - "Hold the right mouse button to move around. Zoom by scrolling the mouse wheel while holding ."); - } - ImGui::Columns(1, "FrameTimeColumn", true); + ImGui::Columns(1, "TimelineColumn", true); - if (ImGui::BeginChild("FrameTimeHistogram", { 0, 50 }, true, ImGuiWindowFlags_NoScrollbar)) + // 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) { - DrawFrameTimeHistogram(); + --nextFrameBoundaryItr; } - ImGui::EndChild(); - - ImGui::Columns(1, "RulerColumn", true); + const AZStd::sys_time_t nextFrameBoundary = *nextFrameBoundaryItr; - // Ruler - if (ImGui::BeginChild("Ruler", { 0, 30 }, true, ImGuiWindowFlags_NoNavFocus)) + // 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()) { - DrawRuler(); + --startTickItr; } - ImGui::EndChild(); - - ImGui::Columns(1, "TimelineColumn", true); - - // Timeline - if (ImGui::BeginChild( - "Timeline", { 0, 0 }, true, ImGuiWindowFlags_AlwaysVerticalScrollbar | ImGuiWindowFlags_NoScrollWithMouse)) + // Main draw loop + u64 baseRow = 0; + for (const auto& [currentThreadId, singleThreadData] : m_savedData) { - // 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 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; + }); - // 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()) + if (regionItr == singleThreadData.end()) { - --startTickItr; + continue; } - // Main draw loop - u64 baseRow = 0; - for (const auto& [currentThreadId, singleThreadData] : m_savedData) + // Draw all of the blocks for a given thread/row + u64 maxDepth = 0; + while (regionItr != singleThreadData.end()) { - // 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; - }); - - if (regionItr == singleThreadData.end()) - { - continue; - } + const TimeRegion& region = *regionItr; - // Draw all of the blocks for a given thread/row - u64 maxDepth = 0; - while (regionItr != singleThreadData.end()) + // Early out if we have drawn all the onscreen regions + if (region.m_startTick > nextFrameBoundary) { - 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; + break; } + u64 targetRow = region.m_stackDepth + baseRow; + maxDepth = AZStd::max(aznumeric_cast(region.m_stackDepth), maxDepth); - // Draw UI details - DrawThreadLabel(baseRow, currentThreadId); - DrawThreadSeparator(baseRow, maxDepth); + DrawBlock(region, targetRow); - baseRow += maxDepth + 1; // Next draw loop should start one row down + ++regionItr; } - DrawFrameBoundaries(); + // Draw UI details + DrawThreadLabel(baseRow, currentThreadId); + DrawThreadSeparator(baseRow, maxDepth); - // Draw an invisible button to capture inputs - ImGui::InvisibleButton("Timeline Input", { ImGui::GetWindowContentRegionWidth(), baseRow * RowHeight }); + baseRow += maxDepth + 1; // Next draw loop should start one row down + } + + 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()) + // Controls + ImGuiIO& io = ImGui::GetIO(); + if (ImGui::IsWindowFocused() && ImGui::IsItemHovered()) + { + io.WantCaptureMouse = true; + if (ImGui::IsMouseDragging(ImGuiMouseButton_Right)) // Scrolling { - io.WantCaptureMouse = true; - if (ImGui::IsMouseDragging(ImGuiMouseButton_Right)) // Scrolling + const auto [deltaX, deltaY] = io.MouseDelta; + if (deltaX != 0 || deltaY != 0) { - 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); - } + // 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 mouseX = ImGui::GetMousePos().x; + } + else if (io.MouseWheel != 0 && io.KeyCtrl) // Zooming + { + // We want zooming to be relative to the mouse's current position + 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(); + // 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()); + 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); + // 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; - } + // 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::EndChild(); + } + + inline void ImGuiCpuProfiler::CacheCpuTimingStatistics() + { + using namespace AZ::Statistics; - inline void ImGuiCpuProfiler::CacheCpuTimingStatistics() + m_cpuTimingStatisticsWhenPause.clear(); + if (auto statsProfiler = AZ::Interface::Get(); statsProfiler) { - using namespace AZ::Statistics; + auto& rhiMetrics = statsProfiler->GetProfiler(AZ_CRC_CE("RHI")); - m_cpuTimingStatisticsWhenPause.clear(); - if (auto statsProfiler = AZ::Interface::Get(); statsProfiler) + const NamedRunningStatistic* frameTimeMetric = rhiMetrics.GetStatistic(AZ_CRC_CE("Frame to Frame Time")); + if (frameTimeMetric) { - auto& rhiMetrics = statsProfiler->GetProfiler(AZ_CRC_CE("RHI")); - - const NamedRunningStatistic* frameTimeMetric = rhiMetrics.GetStatistic(AZ_CRC_CE("Frame to Frame Time")); - if (frameTimeMetric) - { - m_frameToFrameTime = static_cast(frameTimeMetric->GetMostRecentSample()); - } + m_frameToFrameTime = static_cast(frameTimeMetric->GetMostRecentSample()); + } - AZStd::vector statistics; - rhiMetrics.GetStatsManager().GetAllStatistics(statistics); + AZStd::vector statistics; + rhiMetrics.GetStatsManager().GetAllStatistics(statistics); - for (NamedRunningStatistic* stat : statistics) - { - m_cpuTimingStatisticsWhenPause.push_back({ stat->GetName(), stat->GetMostRecentSample() }); - stat->Reset(); - } + for (NamedRunningStatistic* stat : statistics) + { + m_cpuTimingStatisticsWhenPause.push_back({ stat->GetName(), stat->GetMostRecentSample() }); + stat->Reset(); } } + } - 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. + 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. - // Get the latest TimeRegionMap - const RHI::CpuProfiler::TimeRegionMap& timeRegionMap = RHI::CpuProfiler::Get()->GetTimeRegionMap(); + // Get the latest TimeRegionMap + const RHI::CpuProfiler::TimeRegionMap& timeRegionMap = RHI::CpuProfiler::Get()->GetTimeRegionMap(); - m_viewportStartTick = AZStd::numeric_limits::max(); - m_viewportEndTick = AZStd::numeric_limits::lowest(); + m_viewportStartTick = AZStd::numeric_limits::max(); + m_viewportEndTick = AZStd::numeric_limits::lowest(); - // Iterate through the entire TimeRegionMap and copy the data since it will get deleted on the next frame - for (const auto& [threadId, singleThreadRegionMap] : timeRegionMap) + // 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{}(threadId); + // The profiler can sometime return threads without any profiling events when dropping threads, FIXME(ATOM-15949) + if (singleThreadRegionMap.size() == 0) { - const size_t threadIdHashed = AZStd::hash{}(threadId); - // The profiler can sometime return threads without any profiling events when dropping threads, FIXME(ATOM-15949) - if (singleThreadRegionMap.size() == 0) - { - continue; - } + continue; + } - // Now focus on just the data for the current thread - 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) + // Now focus on just the data for the current thread + 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) { - for (const TimeRegion& region : regionVec) - { - newVisualizerData.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; + // Also update the statistical view's data + const AZStd::string& groupName = region.m_groupRegionName.m_groupName; - if (!m_groupRegionMap[groupName].contains(regionName)) - { - m_groupRegionMap[groupName][regionName].m_groupName = groupName; - m_groupRegionMap[groupName][regionName].m_regionName = regionName; - m_tableData.push_back(&m_groupRegionMap[groupName][regionName]); - } - - m_groupRegionMap[groupName][regionName].RecordRegion(region, threadIdHashed); + if (!m_groupRegionMap[groupName].contains(regionName)) + { + m_groupRegionMap[groupName][regionName].m_groupName = groupName; + m_groupRegionMap[groupName][regionName].m_regionName = regionName; + m_tableData.push_back(&m_groupRegionMap[groupName][regionName]); } + + m_groupRegionMap[groupName][regionName].RecordRegion(region, threadIdHashed); } + } - // 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( - newVisualizerData.begin(), newVisualizerData.end(), - [](const TimeRegion& lhs, const TimeRegion& rhs) - { - return lhs.m_startTick < rhs.m_startTick; - }); + // 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( + 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(newVisualizerData.front().m_startTick, m_viewportStartTick); - m_viewportEndTick = AZStd::max(newVisualizerData.back().m_endTick, m_viewportEndTick); + // Use the latest frame's data as the new bounds of the viewport + m_viewportStartTick = AZStd::min(newVisualizerData.front().m_startTick, m_viewportStartTick); + m_viewportEndTick = AZStd::max(newVisualizerData.back().m_endTick, m_viewportEndTick); - m_savedRegionCount += newVisualizerData.size(); + m_savedRegionCount += newVisualizerData.size(); - // Move onto the end of the current thread's saved data, sorted order maintained - AZStd::vector& savedDataVec = m_savedData[threadIdHashed]; - savedDataVec.insert( - savedDataVec.end(), AZStd::make_move_iterator(newVisualizerData.begin()), AZStd::make_move_iterator(newVisualizerData.end())); - } + // Move onto the end of the current thread's saved data, sorted order maintained + AZStd::vector& savedDataVec = m_savedData[threadIdHashed]; + savedDataVec.insert( + savedDataVec.end(), AZStd::make_move_iterator(newVisualizerData.begin()), AZStd::make_move_iterator(newVisualizerData.end())); } + } - inline void ImGuiCpuProfiler::CullFrameData() - { - const AZStd::sys_time_t deleteBeforeTick = AZStd::GetTimeNowTicks() - m_frameToFrameTime * m_framesToCollect; - - // Remove old frame boundary data - auto firstBoundaryToKeepItr = AZStd::upper_bound(m_frameEndTicks.begin(), m_frameEndTicks.end(), deleteBeforeTick); - m_frameEndTicks.erase(m_frameEndTicks.begin(), firstBoundaryToKeepItr); + inline void ImGuiCpuProfiler::CullFrameData() + { + const AZStd::sys_time_t deleteBeforeTick = AZStd::GetTimeNowTicks() - m_frameToFrameTime * m_framesToCollect; - // Remove old region data for each thread - for (auto& [threadId, savedRegions] : m_savedData) - { - AZStd::size_t sizeBeforeRemove = savedRegions.size(); + // Remove old frame boundary data + auto firstBoundaryToKeepItr = AZStd::upper_bound(m_frameEndTicks.begin(), m_frameEndTicks.end(), deleteBeforeTick); + m_frameEndTicks.erase(m_frameEndTicks.begin(), firstBoundaryToKeepItr); - // 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( - savedRegions, - [deleteBeforeTick](const TimeRegion& region) - { - return region.m_startTick < deleteBeforeTick; - }); + // Remove old region data for each thread + for (auto& [threadId, savedRegions] : m_savedData) + { + AZStd::size_t sizeBeforeRemove = savedRegions.size(); - m_savedRegionCount -= sizeBeforeRemove - savedRegions.size(); + // Early out to avoid the linear erase_if call + if (savedRegions.size() >= 1 && savedRegions.at(0).m_startTick > deleteBeforeTick) + { + continue; } - // Remove any threads from the top-level map that no longer hold data + // 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( - m_savedData, - [](const auto& singleThreadDataEntry) + savedRegions, + [deleteBeforeTick](const TimeRegion& region) { - return singleThreadDataEntry.second.empty(); + return region.m_startTick < deleteBeforeTick; }); + + m_savedRegionCount -= sizeBeforeRemove - savedRegions.size(); } - inline void ImGuiCpuProfiler::DrawBlock(const TimeRegion& block, u64 targetRow) - { - // Don't draw anything if the user is searching for regions and this block doesn't pass the filter - if (!m_visualizerHighlightFilter.PassFilter(block.m_groupRegionName.m_regionName)) + // Remove any threads from the top-level map that no longer hold data + AZStd::erase_if( + m_savedData, + [](const auto& singleThreadDataEntry) { - return; - } + return singleThreadDataEntry.second.empty(); + }); + } - float wy = ImGui::GetWindowPos().y - ImGui::GetScrollY(); + inline void ImGuiCpuProfiler::DrawBlock(const TimeRegion& block, u64 targetRow) + { + // Don't draw anything if the user is searching for regions and this block doesn't pass the filter + if (!m_visualizerHighlightFilter.PassFilter(block.m_groupRegionName.m_regionName)) + { + return; + } - ImDrawList* drawList = ImGui::GetWindowDrawList(); + float wy = ImGui::GetWindowPos().y - ImGui::GetScrollY(); - const float startPixel = ConvertTickToPixelSpace(block.m_startTick, m_viewportStartTick, m_viewportEndTick); - const float endPixel = ConvertTickToPixelSpace(block.m_endTick, m_viewportStartTick, m_viewportEndTick); + ImDrawList* drawList = ImGui::GetWindowDrawList(); - if (endPixel - startPixel < 0.5f) - { - return; - } + 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 + 1}; - const ImVec2 endPoint = { endPixel, wy + (targetRow + 1) * RowHeight }; + if (endPixel - startPixel < 0.5f) + { + return; + } - const ImU32 blockColor = GetBlockColor(block); + const ImVec2 startPoint = { startPixel, wy + targetRow * RowHeight + 1}; + const ImVec2 endPoint = { endPixel, wy + (targetRow + 1) * RowHeight }; - 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); + const ImU32 blockColor = GetBlockColor(block); - // Draw the region name if possible - // If the block's current width is too small, we skip drawing the label. - const float regionPixelWidth = endPixel - startPixel; - const float maxCharWidth = ImGui::CalcTextSize("M").x; // M is usually the largest character in most fonts (see CSS em) - if (regionPixelWidth > maxCharWidth) // We can draw at least one character - { - const AZStd::string label = - AZStd::string::format("%s/ %s", block.m_groupRegionName.m_groupName, block.m_groupRegionName.m_regionName); - const float textWidth = ImGui::CalcTextSize(label.c_str()).x; + 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); - if (regionPixelWidth < textWidth) // Not enough space in the block to draw the whole name, draw clipped text. - { - const ImVec4 clipRect = { startPoint.x, startPoint.y, endPoint.x - maxCharWidth, endPoint.y }; + // Draw the region name if possible + // If the block's current width is too small, we skip drawing the label. + const float regionPixelWidth = endPixel - startPixel; + const float maxCharWidth = ImGui::CalcTextSize("M").x; // M is usually the largest character in most fonts (see CSS em) + if (regionPixelWidth > maxCharWidth) // We can draw at least one character + { + const AZStd::string label = + AZStd::string::format("%s/ %s", block.m_groupRegionName.m_groupName, block.m_groupRegionName.m_regionName); + const float textWidth = ImGui::CalcTextSize(label.c_str()).x; - // 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; + if (regionPixelWidth < textWidth) // Not enough space in the block to draw the whole name, draw clipped text. + { + const ImVec4 clipRect = { startPoint.x, startPoint.y, endPoint.x - maxCharWidth, endPoint.y }; - 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. - { - const float remainingWidth = regionPixelWidth - textWidth; - const float offset = remainingWidth * .5f; + // 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; - drawList->AddText({ startPoint.x + offset, startPoint.y }, IM_COL32_WHITE, label.c_str()); - } + ImGui::GetFont()->RenderText(drawList, fontSize, startPoint, IM_COL32_WHITE, clipRect, label.c_str(), 0); } - - // Tooltip and block highlighting - if (ImGui::IsMouseHoveringRect(startPoint, endPoint) && ImGui::IsWindowHovered()) + else // We have enough space to draw the entire label, draw and center text. { - // Go to the statistics view when a region is clicked - if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) - { - m_enableVisualizer = false; - const auto newFilter = AZStd::string(block.m_groupRegionName.m_regionName); - m_timedRegionFilter = ImGuiTextFilter(newFilter.c_str()); - m_timedRegionFilter.Build(); - } - // Hovering outline - drawList->AddRect(startPoint, endPoint, ImGui::GetColorU32({ 1, 1, 1, 1 }), 0.0, 0, 1.5); - - ImGui::BeginTooltip(); - ImGui::Text("%s::%s", block.m_groupRegionName.m_groupName, block.m_groupRegionName.m_regionName); - ImGui::Text("Execution time: %.3f ms", CpuProfilerImGuiHelper::TicksToMs(block.m_endTick - block.m_startTick)); - ImGui::Text("Ticks %lld => %lld", block.m_startTick, block.m_endTick); - ImGui::EndTooltip(); + const float remainingWidth = regionPixelWidth - textWidth; + const float offset = remainingWidth * .5f; + + drawList->AddText({ startPoint.x + offset, startPoint.y }, IM_COL32_WHITE, label.c_str()); } } - inline ImU32 ImGuiCpuProfiler::GetBlockColor(const TimeRegion& block) + // Tooltip and block highlighting + if (ImGui::IsMouseHoveringRect(startPoint, endPoint) && ImGui::IsWindowHovered()) { - // Use the GroupRegionName pointer a key into the cache, equal regions will have equal pointers - const GroupRegionName& key = block.m_groupRegionName; - if (auto iter = m_regionColorMap.find(key); iter != m_regionColorMap.end()) // Cache hit + // Go to the statistics view when a region is clicked + if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) { - return ImGui::GetColorU32(iter->second); + m_enableVisualizer = false; + const auto newFilter = AZStd::string(block.m_groupRegionName.m_regionName); + m_timedRegionFilter = ImGuiTextFilter(newFilter.c_str()); + m_timedRegionFilter.Build(); } - - // Cache miss, generate a new random color - AZ::SimpleLcgRandom rand(aznumeric_cast(AZStd::GetTimeNowTicks())); - const float r = AZStd::clamp(rand.GetRandomFloat(), .1f, .9f); - const float g = AZStd::clamp(rand.GetRandomFloat(), .1f, .9f); - const float b = AZStd::clamp(rand.GetRandomFloat(), .1f, .9f); - const ImVec4 randomColor = {r, g, b, .8}; - m_regionColorMap.emplace(key, randomColor); - return ImGui::GetColorU32(randomColor); + // Hovering outline + drawList->AddRect(startPoint, endPoint, ImGui::GetColorU32({ 1, 1, 1, 1 }), 0.0, 0, 1.5); + + ImGui::BeginTooltip(); + ImGui::Text("%s::%s", block.m_groupRegionName.m_groupName, block.m_groupRegionName.m_regionName); + ImGui::Text("Execution time: %.3f ms", CpuProfilerImGuiHelper::TicksToMs(block.m_endTick - block.m_startTick)); + ImGui::Text("Ticks %lld => %lld", block.m_startTick, block.m_endTick); + ImGui::EndTooltip(); } + } - inline void ImGuiCpuProfiler::DrawThreadSeparator(u64 baseRow, u64 maxDepth) + inline ImU32 ImGuiCpuProfiler::GetBlockColor(const TimeRegion& block) + { + // Use the GroupRegionName pointer a key into the cache, equal regions will have equal pointers + const GroupRegionName& key = block.m_groupRegionName; + if (auto iter = m_regionColorMap.find(key); iter != m_regionColorMap.end()) // Cache hit { - const ImU32 red = ImGui::GetColorU32({ 1, 0, 0, 1 }); + return ImGui::GetColorU32(iter->second); + } - auto [wx, wy] = ImGui::GetWindowPos(); - wy -= ImGui::GetScrollY(); - const float windowWidth = ImGui::GetWindowWidth(); - const float boundaryY = wy + (baseRow + maxDepth + 1) * RowHeight; + // Cache miss, generate a new random color + AZ::SimpleLcgRandom rand(aznumeric_cast(AZStd::GetTimeNowTicks())); + const float r = AZStd::clamp(rand.GetRandomFloat(), .1f, .9f); + const float g = AZStd::clamp(rand.GetRandomFloat(), .1f, .9f); + const float b = AZStd::clamp(rand.GetRandomFloat(), .1f, .9f); + const ImVec4 randomColor = {r, g, b, .8}; + m_regionColorMap.emplace(key, randomColor); + return ImGui::GetColorU32(randomColor); + } + + inline void ImGuiCpuProfiler::DrawThreadSeparator(u64 baseRow, u64 maxDepth) + { + const ImU32 red = ImGui::GetColorU32({ 1, 0, 0, 1 }); - ImGui::GetWindowDrawList()->AddLine({ wx, boundaryY }, { wx + windowWidth, boundaryY }, red, 1.0f); - } + auto [wx, wy] = ImGui::GetWindowPos(); + wy -= ImGui::GetScrollY(); + const float windowWidth = ImGui::GetWindowWidth(); + const float boundaryY = wy + (baseRow + maxDepth + 1) * RowHeight; - inline void ImGuiCpuProfiler::DrawThreadLabel(u64 baseRow, size_t threadId) - { - auto [wx, wy] = ImGui::GetWindowPos(); - wy -= ImGui::GetScrollY(); - const AZStd::string threadIdText = AZStd::string::format("Thread: %zu", threadId); + ImGui::GetWindowDrawList()->AddLine({ wx, boundaryY }, { wx + windowWidth, boundaryY }, red, 1.0f); + } - ImGui::GetWindowDrawList()->AddText({ wx + 10, wy + baseRow * RowHeight}, IM_COL32_WHITE, threadIdText.c_str()); - } + inline void ImGuiCpuProfiler::DrawThreadLabel(u64 baseRow, size_t threadId) + { + auto [wx, wy] = ImGui::GetWindowPos(); + wy -= ImGui::GetScrollY(); + const AZStd::string threadIdText = AZStd::string::format("Thread: %zu", threadId); - inline void ImGuiCpuProfiler::DrawFrameBoundaries() - { - ImDrawList* drawList = ImGui::GetWindowDrawList(); + ImGui::GetWindowDrawList()->AddText({ wx + 10, wy + baseRow * RowHeight}, IM_COL32_WHITE, threadIdText.c_str()); + } - const float wy = ImGui::GetWindowPos().y; - const float windowHeight = ImGui::GetWindowHeight(); - const ImU32 red = ImGui::GetColorU32({ 1, 0, 0, 1 }); + inline void ImGuiCpuProfiler::DrawFrameBoundaries() + { + ImDrawList* drawList = ImGui::GetWindowDrawList(); - // 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); + const float wy = ImGui::GetWindowPos().y; + const float windowHeight = ImGui::GetWindowHeight(); + const ImU32 red = ImGui::GetColorU32({ 1, 0, 0, 1 }); - while (endTickItr != m_frameEndTicks.end() && *endTickItr < m_viewportEndTick) - { - const float horizontalPixel = ConvertTickToPixelSpace(*endTickItr, m_viewportStartTick, m_viewportEndTick); - drawList->AddLine({ horizontalPixel, wy }, { horizontalPixel, wy + windowHeight }, red); - ++endTickItr; - } + // 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); + + while (endTickItr != m_frameEndTicks.end() && *endTickItr < m_viewportEndTick) + { + const float horizontalPixel = ConvertTickToPixelSpace(*endTickItr, m_viewportStartTick, m_viewportEndTick); + drawList->AddLine({ horizontalPixel, wy }, { horizontalPixel, wy + windowHeight }, red); + ++endTickItr; } + } - inline void ImGuiCpuProfiler::DrawRuler() + inline void ImGuiCpuProfiler::DrawRuler() + { + // Use a pair of iterators to go through all saved frame boundaries and draw ruler lines + auto lastFrameBoundaryItr = AZStd::lower_bound(m_frameEndTicks.begin(), m_frameEndTicks.end(), m_viewportStartTick); + auto nextFrameBoundaryItr = lastFrameBoundaryItr; + if (lastFrameBoundaryItr != m_frameEndTicks.begin()) { - // Use a pair of iterators to go through all saved frame boundaries and draw ruler lines - auto lastFrameBoundaryItr = AZStd::lower_bound(m_frameEndTicks.begin(), m_frameEndTicks.end(), m_viewportStartTick); - auto nextFrameBoundaryItr = lastFrameBoundaryItr; - if (lastFrameBoundaryItr != m_frameEndTicks.begin()) + --lastFrameBoundaryItr; + } + + const auto [wx, wy] = ImGui::GetWindowPos(); + ImDrawList* drawList = ImGui::GetWindowDrawList(); + + while (nextFrameBoundaryItr != m_frameEndTicks.end() && *lastFrameBoundaryItr <= m_viewportEndTick) + { + const AZStd::sys_time_t lastFrameBoundaryTick = *lastFrameBoundaryItr; + const AZStd::sys_time_t nextFrameBoundaryTick = *nextFrameBoundaryItr; + if (lastFrameBoundaryTick > m_viewportEndTick) { - --lastFrameBoundaryItr; + break; } - const auto [wx, wy] = ImGui::GetWindowPos(); - ImDrawList* drawList = ImGui::GetWindowDrawList(); + const float lastFrameBoundaryPixel = ConvertTickToPixelSpace(lastFrameBoundaryTick, m_viewportStartTick, m_viewportEndTick); + const float nextFrameBoundaryPixel = ConvertTickToPixelSpace(nextFrameBoundaryTick, m_viewportStartTick, m_viewportEndTick); - while (nextFrameBoundaryItr != m_frameEndTicks.end() && *lastFrameBoundaryItr <= m_viewportEndTick) - { - const AZStd::sys_time_t lastFrameBoundaryTick = *lastFrameBoundaryItr; - const AZStd::sys_time_t nextFrameBoundaryTick = *nextFrameBoundaryItr; - if (lastFrameBoundaryTick > m_viewportEndTick) - { - break; - } + const AZStd::string label = + AZStd::string::format("%.2f ms", CpuProfilerImGuiHelper::TicksToMs(nextFrameBoundaryTick - lastFrameBoundaryTick)); + const float labelWidth = ImGui::CalcTextSize(label.c_str()).x; - const float lastFrameBoundaryPixel = ConvertTickToPixelSpace(lastFrameBoundaryTick, m_viewportStartTick, m_viewportEndTick); - const float nextFrameBoundaryPixel = ConvertTickToPixelSpace(nextFrameBoundaryTick, m_viewportStartTick, m_viewportEndTick); + // The label can fit between the two boundaries, center it and draw + if (labelWidth <= nextFrameBoundaryPixel - lastFrameBoundaryPixel) + { + const float offset = (nextFrameBoundaryPixel - lastFrameBoundaryPixel - labelWidth) /2; + const float textBeginPixel = lastFrameBoundaryPixel + offset; + const float textEndPixel = textBeginPixel + labelWidth; - const AZStd::string label = - AZStd::string::format("%.2f ms", CpuProfilerImGuiHelper::TicksToMs(nextFrameBoundaryTick - lastFrameBoundaryTick)); - const float labelWidth = ImGui::CalcTextSize(label.c_str()).x; + const float verticalOffset = (ImGui::GetWindowHeight() - ImGui::GetFontSize()) / 2; - // The label can fit between the two boundaries, center it and draw - if (labelWidth <= nextFrameBoundaryPixel - lastFrameBoundaryPixel) - { - const float offset = (nextFrameBoundaryPixel - lastFrameBoundaryPixel - labelWidth) /2; - 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 + verticalOffset }, IM_COL32_WHITE, label.c_str()); - - // Left side - drawList->AddLine( - { lastFrameBoundaryPixel, wy + ImGui::GetWindowHeight() / 2 }, - { textBeginPixel - 5, wy + ImGui::GetWindowHeight() / 2}, - IM_COL32_WHITE); - - // Right side - drawList->AddLine( - { textEndPixel, wy + ImGui::GetWindowHeight()/2 }, - { nextFrameBoundaryPixel, wy + ImGui::GetWindowHeight()/2 }, - IM_COL32_WHITE); - } - else // Cannot fit inside, just draw a line between the two boundaries - { - drawList->AddLine( - { lastFrameBoundaryPixel, wy + ImGui::GetWindowHeight() / 2 }, - { nextFrameBoundaryPixel, wy + ImGui::GetWindowHeight() / 2 }, - IM_COL32_WHITE); - } + // Execution time label + drawList->AddText({ textBeginPixel, wy + verticalOffset }, IM_COL32_WHITE, label.c_str()); - // Left bound + // Left side drawList->AddLine( - { lastFrameBoundaryPixel, wy }, - { lastFrameBoundaryPixel, wy + ImGui::GetWindowHeight() }, + { lastFrameBoundaryPixel, wy + ImGui::GetWindowHeight() / 2 }, + { textBeginPixel - 5, wy + ImGui::GetWindowHeight() / 2}, IM_COL32_WHITE); - // Right bound + // Right side drawList->AddLine( - { nextFrameBoundaryPixel, wy }, - { nextFrameBoundaryPixel, wy + ImGui::GetWindowHeight() }, + { textEndPixel, wy + ImGui::GetWindowHeight()/2 }, + { nextFrameBoundaryPixel, wy + ImGui::GetWindowHeight()/2 }, + IM_COL32_WHITE); + } + else // Cannot fit inside, just draw a line between the two boundaries + { + drawList->AddLine( + { lastFrameBoundaryPixel, wy + ImGui::GetWindowHeight() / 2 }, + { nextFrameBoundaryPixel, wy + ImGui::GetWindowHeight() / 2 }, IM_COL32_WHITE); - - lastFrameBoundaryItr = nextFrameBoundaryItr; - ++nextFrameBoundaryItr; } - } - - inline void ImGuiCpuProfiler::DrawFrameTimeHistogram() - { - ImDrawList* drawList = ImGui::GetWindowDrawList(); - const auto [wx, wy] = ImGui::GetWindowPos(); - const ImU32 orange = ImGui::GetColorU32({ 1, .7, 0, 1 }); - const ImU32 red = ImGui::GetColorU32({ 1, 0, 0, 1 }); - - const AZStd::sys_time_t ticksPerSecond = AZStd::GetTimeTicksPerSecond(); - const AZStd::sys_time_t viewportCenter = m_viewportEndTick - (m_viewportEndTick - m_viewportStartTick) / 2; - const AZStd::sys_time_t leftHistogramBound = viewportCenter - ticksPerSecond; - const AZStd::sys_time_t rightHistogramBound = viewportCenter + ticksPerSecond; - // Draw frame limit lines + // Left bound drawList->AddLine( - { wx, wy + ImGui::GetWindowHeight() - MediumFrameTimeLimit }, - { wx + ImGui::GetWindowWidth(), wy + ImGui::GetWindowHeight() - MediumFrameTimeLimit }, - orange); + { lastFrameBoundaryPixel, wy }, + { lastFrameBoundaryPixel, wy + ImGui::GetWindowHeight() }, + IM_COL32_WHITE); + // Right bound drawList->AddLine( - { wx, wy + ImGui::GetWindowHeight() - HighFrameTimeLimit }, - { wx + ImGui::GetWindowWidth(), wy + ImGui::GetWindowHeight() - HighFrameTimeLimit }, - red); - - - // Draw viewport bound rectangle - const float leftViewportPixel = ConvertTickToPixelSpace(m_viewportStartTick, leftHistogramBound, rightHistogramBound); - const float rightViewportPixel = ConvertTickToPixelSpace(m_viewportEndTick, leftHistogramBound, rightHistogramBound); - const ImVec2 topLeftPos = { leftViewportPixel, wy }; - const ImVec2 botRightPos = { rightViewportPixel, wy + ImGui::GetWindowHeight() }; - const ImU32 gray = ImGui::GetColorU32({ 1, 1, 1, .3 }); - drawList->AddRectFilled(topLeftPos, botRightPos, gray); - - // Find the first onscreen frame execution time - auto frameEndTickItr = AZStd::lower_bound(m_frameEndTicks.begin(), m_frameEndTicks.end(), leftHistogramBound); - if (frameEndTickItr != m_frameEndTicks.begin()) - { - --frameEndTickItr; - } + { nextFrameBoundaryPixel, wy }, + { nextFrameBoundaryPixel, wy + ImGui::GetWindowHeight() }, + IM_COL32_WHITE); - // Since we only store the frame end ticks, we must calculate the execution times on the fly by comparing pairs of elements. - AZStd::sys_time_t lastFrameEndTick = *frameEndTickItr; - while (*frameEndTickItr < rightHistogramBound && ++frameEndTickItr != m_frameEndTicks.end()) - { - const AZStd::sys_time_t frameEndTick = *frameEndTickItr; + lastFrameBoundaryItr = nextFrameBoundaryItr; + ++nextFrameBoundaryItr; + } + } - const float framePixelPos = ConvertTickToPixelSpace(frameEndTick, leftHistogramBound, rightHistogramBound); - const float frameTimeMs = CpuProfilerImGuiHelper::TicksToMs(frameEndTick - lastFrameEndTick); + inline void ImGuiCpuProfiler::DrawFrameTimeHistogram() + { + ImDrawList* drawList = ImGui::GetWindowDrawList(); + const auto [wx, wy] = ImGui::GetWindowPos(); + const ImU32 orange = ImGui::GetColorU32({ 1, .7, 0, 1 }); + const ImU32 red = ImGui::GetColorU32({ 1, 0, 0, 1 }); + + const AZStd::sys_time_t ticksPerSecond = AZStd::GetTimeTicksPerSecond(); + const AZStd::sys_time_t viewportCenter = m_viewportEndTick - (m_viewportEndTick - m_viewportStartTick) / 2; + const AZStd::sys_time_t leftHistogramBound = viewportCenter - ticksPerSecond; + const AZStd::sys_time_t rightHistogramBound = viewportCenter + ticksPerSecond; + + // Draw frame limit lines + drawList->AddLine( + { wx, wy + ImGui::GetWindowHeight() - MediumFrameTimeLimit }, + { wx + ImGui::GetWindowWidth(), wy + ImGui::GetWindowHeight() - MediumFrameTimeLimit }, + orange); + + drawList->AddLine( + { wx, wy + ImGui::GetWindowHeight() - HighFrameTimeLimit }, + { wx + ImGui::GetWindowWidth(), wy + ImGui::GetWindowHeight() - HighFrameTimeLimit }, + red); + + + // Draw viewport bound rectangle + const float leftViewportPixel = ConvertTickToPixelSpace(m_viewportStartTick, leftHistogramBound, rightHistogramBound); + const float rightViewportPixel = ConvertTickToPixelSpace(m_viewportEndTick, leftHistogramBound, rightHistogramBound); + const ImVec2 topLeftPos = { leftViewportPixel, wy }; + const ImVec2 botRightPos = { rightViewportPixel, wy + ImGui::GetWindowHeight() }; + const ImU32 gray = ImGui::GetColorU32({ 1, 1, 1, .3 }); + drawList->AddRectFilled(topLeftPos, botRightPos, gray); + + // Find the first onscreen frame execution time + auto frameEndTickItr = AZStd::lower_bound(m_frameEndTicks.begin(), m_frameEndTicks.end(), leftHistogramBound); + if (frameEndTickItr != m_frameEndTicks.begin()) + { + --frameEndTickItr; + } - const ImVec2 lineBottom = { framePixelPos, ImGui::GetWindowHeight() + wy }; - const ImVec2 lineTop = { framePixelPos, ImGui::GetWindowHeight() + wy - frameTimeMs }; + // Since we only store the frame end ticks, we must calculate the execution times on the fly by comparing pairs of elements. + AZStd::sys_time_t lastFrameEndTick = *frameEndTickItr; + while (*frameEndTickItr < rightHistogramBound && ++frameEndTickItr != m_frameEndTicks.end()) + { + const AZStd::sys_time_t frameEndTick = *frameEndTickItr; - ImU32 lineColor = ImGui::GetColorU32({ .3, .3, .3, 1 }); // Gray - if (frameTimeMs > HighFrameTimeLimit) - { - lineColor = ImGui::GetColorU32({1, 0, 0, 1}); // Red - } - else if (frameTimeMs > MediumFrameTimeLimit) - { - lineColor = ImGui::GetColorU32({1, .7, 0, 1}); // Orange - } + const float framePixelPos = ConvertTickToPixelSpace(frameEndTick, leftHistogramBound, rightHistogramBound); + const float frameTimeMs = CpuProfilerImGuiHelper::TicksToMs(frameEndTick - lastFrameEndTick); - drawList->AddLine(lineBottom, lineTop, lineColor, 3.0); + const ImVec2 lineBottom = { framePixelPos, ImGui::GetWindowHeight() + wy }; + const ImVec2 lineTop = { framePixelPos, ImGui::GetWindowHeight() + wy - frameTimeMs }; - lastFrameEndTick = frameEndTick; + ImU32 lineColor = ImGui::GetColorU32({ .3, .3, .3, 1 }); // Gray + if (frameTimeMs > HighFrameTimeLimit) + { + lineColor = ImGui::GetColorU32({1, 0, 0, 1}); // Red } - - // Handle input - ImGui::InvisibleButton("HistogramInputCapture", { ImGui::GetWindowWidth(), ImGui::GetWindowHeight() }); - ImGuiIO& io = ImGui::GetIO(); - if (ImGui::IsItemClicked(ImGuiMouseButton_Left)) + else if (frameTimeMs > MediumFrameTimeLimit) { - const float mousePixelX = io.MousePos.x; - const float percentWindow = (mousePixelX - wx) / ImGui::GetWindowWidth(); - const AZStd::sys_time_t newViewportCenterTick = leftHistogramBound + - aznumeric_cast((rightHistogramBound - leftHistogramBound) * percentWindow); - - const AZStd::sys_time_t viewportWidth = GetViewportTickWidth(); - m_viewportEndTick = newViewportCenterTick + viewportWidth / 2; - m_viewportStartTick = newViewportCenterTick - viewportWidth / 2; + lineColor = ImGui::GetColorU32({1, .7, 0, 1}); // Orange } + + drawList->AddLine(lineBottom, lineTop, lineColor, 3.0); + + lastFrameEndTick = frameEndTick; } - inline AZStd::sys_time_t ImGuiCpuProfiler::GetViewportTickWidth() const + // Handle input + ImGui::InvisibleButton("HistogramInputCapture", { ImGui::GetWindowWidth(), ImGui::GetWindowHeight() }); + ImGuiIO& io = ImGui::GetIO(); + if (ImGui::IsItemClicked(ImGuiMouseButton_Left)) { - return m_viewportEndTick - m_viewportStartTick; + const float mousePixelX = io.MousePos.x; + const float percentWindow = (mousePixelX - wx) / ImGui::GetWindowWidth(); + const AZStd::sys_time_t newViewportCenterTick = leftHistogramBound + + aznumeric_cast((rightHistogramBound - leftHistogramBound) * percentWindow); + + const AZStd::sys_time_t viewportWidth = GetViewportTickWidth(); + m_viewportEndTick = newViewportCenterTick + viewportWidth / 2; + m_viewportStartTick = newViewportCenterTick - viewportWidth / 2; } + } + + inline AZStd::sys_time_t ImGuiCpuProfiler::GetViewportTickWidth() const + { + return m_viewportEndTick - m_viewportStartTick; + } - inline float ImGuiCpuProfiler::ConvertTickToPixelSpace(AZStd::sys_time_t tick, AZStd::sys_time_t leftBound, AZStd::sys_time_t rightBound) const + inline float ImGuiCpuProfiler::ConvertTickToPixelSpace(AZStd::sys_time_t tick, AZStd::sys_time_t leftBound, AZStd::sys_time_t rightBound) const + { + const float wx = ImGui::GetWindowPos().x; + const float tickSpaceShifted = aznumeric_cast(tick - leftBound); // This will be close to zero, so FP inaccuracy should not be too bad + const float tickSpaceNormalized = tickSpaceShifted / (rightBound - leftBound); + const float pixelSpace = tickSpaceNormalized * ImGui::GetWindowWidth() + wx; + return pixelSpace; + } + + // System tick bus overrides + inline void ImGuiCpuProfiler::OnSystemTick() + { + if (m_paused) { - const float wx = ImGui::GetWindowPos().x; - const float tickSpaceShifted = aznumeric_cast(tick - leftBound); // This will be close to zero, so FP inaccuracy should not be too bad - const float tickSpaceNormalized = tickSpaceShifted / (rightBound - leftBound); - const float pixelSpace = tickSpaceNormalized * ImGui::GetWindowWidth() + wx; - return pixelSpace; + SystemTickBus::Handler::BusDisconnect(); } - - // System tick bus overrides - inline void ImGuiCpuProfiler::OnSystemTick() + else { - if (m_paused) - { - SystemTickBus::Handler::BusDisconnect(); - } - else - { - m_frameEndTicks.push_back(AZStd::GetTimeNowTicks()); + m_frameEndTicks.push_back(AZStd::GetTimeNowTicks()); - for (auto& [groupName, regionMap] : m_groupRegionMap) + for (auto& [groupName, regionMap] : m_groupRegionMap) + { + for (auto& [regionName, row] : regionMap) { - for (auto& [regionName, row] : regionMap) - { - row.ResetPerFrameStatistics(); - } + row.ResetPerFrameStatistics(); } } } + } - // ---- TableRow impl ---- + // ---- TableRow impl ---- - inline void TableRow::RecordRegion(const AZ::RHI::CachedTimeRegion& region, size_t threadId) - { - const AZStd::sys_time_t deltaTime = region.m_endTick - region.m_startTick; - - // Update per frame statistics - ++m_invocationsLastFrame; - m_executingThreads.insert(threadId); - m_lastFrameTotalTicks += deltaTime; - m_maxTicks = AZStd::max(m_maxTicks, deltaTime); - - // Update aggregate statistics - m_runningAverageTicks = - aznumeric_cast((1.0 * (deltaTime + m_invocationsTotal * m_runningAverageTicks)) / (m_invocationsTotal + 1)); - ++m_invocationsTotal; - } + inline void TableRow::RecordRegion(const AZ::RHI::CachedTimeRegion& region, size_t threadId) + { + const AZStd::sys_time_t deltaTime = region.m_endTick - region.m_startTick; - inline void TableRow::ResetPerFrameStatistics() - { - m_invocationsLastFrame = 0; - m_executingThreads.clear(); - m_lastFrameTotalTicks = 0; - m_maxTicks = 0; - } + // Update per frame statistics + ++m_invocationsLastFrame; + m_executingThreads.insert(threadId); + m_lastFrameTotalTicks += deltaTime; + m_maxTicks = AZStd::max(m_maxTicks, deltaTime); + + // Update aggregate statistics + m_runningAverageTicks = + aznumeric_cast((1.0 * (deltaTime + m_invocationsTotal * m_runningAverageTicks)) / (m_invocationsTotal + 1)); + ++m_invocationsTotal; + } - inline AZStd::string TableRow::GetExecutingThreadsLabel() const + inline void TableRow::ResetPerFrameStatistics() + { + m_invocationsLastFrame = 0; + m_executingThreads.clear(); + m_lastFrameTotalTicks = 0; + m_maxTicks = 0; + } + + inline AZStd::string TableRow::GetExecutingThreadsLabel() const + { + auto threadString = AZStd::string::format("Executed in %zu threads\n", m_executingThreads.size()); + for (const auto& threadId : m_executingThreads) { - auto threadString = AZStd::string::format("Executed in %zu threads\n", m_executingThreads.size()); - for (const auto& threadId : m_executingThreads) - { - threadString.append(AZStd::string::format("Thread: %zu\n", threadId)); - } - return threadString; + threadString.append(AZStd::string::format("Thread: %zu\n", threadId)); } - } // namespace Render -} // namespace AZ + return threadString; + } +} // namespace Profiler diff --git a/Gems/Profiler/Code/Source/ImGuiCpuProfiler.h b/Gems/Profiler/Code/Source/ImGuiCpuProfiler.h index 4a326660dd..69de0b578a 100644 --- a/Gems/Profiler/Code/Source/ImGuiCpuProfiler.h +++ b/Gems/Profiler/Code/Source/ImGuiCpuProfiler.h @@ -15,217 +15,214 @@ #include -namespace AZ +namespace Profiler { - namespace Render + //! Stores all the data associated with a row in the table. + struct TableRow { - //! Stores all the data associated with a row in the table. - struct TableRow + template + struct TableRowCompareFunctor { - template - struct TableRowCompareFunctor + TableRowCompareFunctor(T memberPointer, bool isAscending) : m_memberPointer(memberPointer), m_ascending(isAscending){}; + + bool operator()(const TableRow* lhs, const TableRow* rhs) { - TableRowCompareFunctor(T memberPointer, bool isAscending) : m_memberPointer(memberPointer), m_ascending(isAscending){}; + return m_ascending ? lhs->*m_memberPointer < rhs->*m_memberPointer : lhs->*m_memberPointer > rhs->*m_memberPointer; + } - bool operator()(const TableRow* lhs, const TableRow* rhs) - { - return m_ascending ? lhs->*m_memberPointer < rhs->*m_memberPointer : lhs->*m_memberPointer > rhs->*m_memberPointer; - } + T m_memberPointer; + bool m_ascending; + }; - T m_memberPointer; - bool m_ascending; - }; + // Update running statistics with new region data + void RecordRegion(const AZ::RHI::CachedTimeRegion& region, size_t threadId); - // Update running statistics with new region data - void RecordRegion(const AZ::RHI::CachedTimeRegion& region, size_t threadId); + void ResetPerFrameStatistics(); - void ResetPerFrameStatistics(); + // Get a string of all threads that this region executed in during the last frame + AZStd::string GetExecutingThreadsLabel() const; - // Get a string of all threads that this region executed in during the last frame - AZStd::string GetExecutingThreadsLabel() const; - - AZStd::string m_groupName; - AZStd::string m_regionName; + AZStd::string m_groupName; + AZStd::string m_regionName; - // --- Per frame statistics --- + // --- Per frame statistics --- - u64 m_invocationsLastFrame = 0; + u64 m_invocationsLastFrame = 0; - // NOTE: set over unordered_set so the threads can be shown in increasing order in tooltip. - AZStd::set m_executingThreads; + // NOTE: set over unordered_set so the threads can be shown in increasing order in tooltip. + AZStd::set m_executingThreads; - AZStd::sys_time_t m_lastFrameTotalTicks = 0; + AZStd::sys_time_t m_lastFrameTotalTicks = 0; - // Maximum execution time of a region in the last frame. - AZStd::sys_time_t m_maxTicks = 0; + // Maximum execution time of a region in the last frame. + AZStd::sys_time_t m_maxTicks = 0; - // --- Aggregate statistics --- + // --- Aggregate statistics --- - u64 m_invocationsTotal = 0; + u64 m_invocationsTotal = 0; - // Running average of Mean Time Per Call - AZStd::sys_time_t m_runningAverageTicks = 0; - }; + // Running average of Mean Time Per Call + AZStd::sys_time_t m_runningAverageTicks = 0; + }; - //! ImGui widget for examining Atom CPU Profiling instrumentation. - //! Offers both a statistical view (with sorting and searching capability) and a visualizer - //! similar to RAD and other profiling tools. - class ImGuiCpuProfiler - : SystemTickBus::Handler - { - // Region Name -> statistical view row data - using RegionRowMap = AZStd::map; - // Group Name -> RegionRowMap - using GroupRegionMap = AZStd::map; + //! ImGui widget for examining Atom CPU Profiling instrumentation. + //! Offers both a statistical view (with sorting and searching capability) and a visualizer + //! similar to RAD and other profiling tools. + class ImGuiCpuProfiler + : SystemTickBus::Handler + { + // Region Name -> statistical view row data + using RegionRowMap = AZStd::map; + // Group Name -> RegionRowMap + using GroupRegionMap = AZStd::map; - using TimeRegion = AZ::RHI::CachedTimeRegion; - using GroupRegionName = AZ::RHI::CachedTimeRegion::GroupRegionName; + using TimeRegion = AZ::RHI::CachedTimeRegion; + using GroupRegionName = AZ::RHI::CachedTimeRegion::GroupRegionName; - public: - struct CpuTimingEntry - { - const AZStd::string& m_name; - double m_executeDuration; - }; + public: + struct CpuTimingEntry + { + const AZStd::string& m_name; + double m_executeDuration; + }; - ImGuiCpuProfiler() = default; - ~ImGuiCpuProfiler() = default; + ImGuiCpuProfiler() = default; + ~ImGuiCpuProfiler() = default; - //! Draws the overall CPU profiling window, defaults to the statistical view - void Draw(bool& keepDrawing); + //! Draws the overall CPU profiling window, defaults to the statistical view + void Draw(bool& keepDrawing); - private: - 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 + private: + 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 - //! Draws the statistical view of the CPU profiling data. - void DrawStatisticsView(); + //! 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(); + //! Callback invoked when the "Load File" button is pressed in the file picker. + void LoadFile(); - //! Draws the file picker window. - void DrawFilePicker(); + //! Draws the file picker window. + void DrawFilePicker(); - //! Draws the CPU profiling visualizer. - void DrawVisualizer(); + //! Draws the CPU profiling visualizer. + void DrawVisualizer(); - // Draw the shared header between the two windows. - void DrawCommonHeader(); + // Draw the shared header between the two windows. + void DrawCommonHeader(); - // Draw the region statistics table in the order specified by the pointers in m_tableData. - void DrawTable(); + // Draw the region statistics table in the order specified by the pointers in m_tableData. + void DrawTable(); - // Sort the table by a given column, rearranges the pointers in m_tableData. - void SortTable(ImGuiTableSortSpecs* sortSpecs); + // Sort the table by a given column, rearranges the pointers in m_tableData. + void SortTable(ImGuiTableSortSpecs* sortSpecs); - // gather the latest timing statistics - void CacheCpuTimingStatistics(); + // gather the latest timing statistics + void CacheCpuTimingStatistics(); - // Get the profiling data from the last frame, only called when the profiler is not paused. - void CollectFrameData(); + // Get the profiling data from the last frame, only called when the profiler is not paused. + void CollectFrameData(); - // Cull old data from internal storage, only called when profiler is not paused. - void CullFrameData(); + // Cull old data from internal storage, only called when profiler is not paused. + void CullFrameData(); - // Draws a single block onto the timeline into the specified row - void DrawBlock(const TimeRegion& block, u64 targetRow); + // Draws a single block onto the timeline into the specified row + void DrawBlock(const TimeRegion& block, u64 targetRow); - // Draw horizontal lines between threads in the timeline - void DrawThreadSeparator(u64 threadBoundary, u64 maxDepth); + // Draw horizontal lines between threads in the timeline + void DrawThreadSeparator(u64 threadBoundary, u64 maxDepth); - // Draw the "Thread XXXXX" label onto the viewport - void DrawThreadLabel(u64 baseRow, size_t threadId); + // Draw the "Thread XXXXX" label onto the viewport + void DrawThreadLabel(u64 baseRow, size_t threadId); - // Draw the vertical lines separating frames in the timeline - void DrawFrameBoundaries(); + // Draw the vertical lines separating frames in the timeline + void DrawFrameBoundaries(); - // Draw the ruler with frame time labels - void DrawRuler(); + // Draw the ruler with frame time labels + void DrawRuler(); - // Draw the frame time histogram - void DrawFrameTimeHistogram(); + // Draw the frame time histogram + void DrawFrameTimeHistogram(); - // Converts raw ticks to a pixel value suitable to give to ImDrawList, handles window scrolling - float ConvertTickToPixelSpace(AZStd::sys_time_t tick, AZStd::sys_time_t leftBound, AZStd::sys_time_t rightBound) const; + // Converts raw ticks to a pixel value suitable to give to ImDrawList, handles window scrolling + float ConvertTickToPixelSpace(AZStd::sys_time_t tick, AZStd::sys_time_t leftBound, AZStd::sys_time_t rightBound) const; - AZStd::sys_time_t GetViewportTickWidth() const; + AZStd::sys_time_t GetViewportTickWidth() const; - // Gets the color for a block using the GroupRegionName as a key into the cache. - // Generates a random ImU32 if the block does not yet have a color. - ImU32 GetBlockColor(const TimeRegion& block); + // Gets the color for a block using the GroupRegionName as a key into the cache. + // Generates a random ImU32 if the block does not yet have a color. + ImU32 GetBlockColor(const TimeRegion& block); - // System tick bus overrides - virtual void OnSystemTick() override; + // System tick bus overrides + virtual void OnSystemTick() override; - // --- Visualizer Members --- + // --- Visualizer Members --- - int m_framesToCollect = DefaultFramesToCollect; + int m_framesToCollect = DefaultFramesToCollect; - // Tally of the number of saved profiling events so far - u64 m_savedRegionCount = 0; + // Tally of the number of saved profiling events so far + u64 m_savedRegionCount = 0; - // Viewport tick bounds, these are used to convert tick space -> screen space and cull so we only draw onscreen objects - AZStd::sys_time_t m_viewportStartTick; - AZStd::sys_time_t m_viewportEndTick; + // Viewport tick bounds, these are used to convert tick space -> screen space and cull so we only draw onscreen objects + AZStd::sys_time_t m_viewportStartTick; + AZStd::sys_time_t m_viewportEndTick; - // Map to store each thread's TimeRegions, individual vectors are sorted by start tick - // 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> m_savedData; + // Map to store each thread's TimeRegions, individual vectors are sorted by start tick + // 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> m_savedData; - // Region color cache - AZStd::unordered_map m_regionColorMap; + // Region color cache + AZStd::unordered_map m_regionColorMap; - // Tracks the frame boundaries - AZStd::vector m_frameEndTicks = { INT64_MIN }; + // Tracks the frame boundaries + AZStd::vector m_frameEndTicks = { INT64_MIN }; - // Filter for highlighting regions on the visualizer - ImGuiTextFilter m_visualizerHighlightFilter; + // Filter for highlighting regions on the visualizer + ImGuiTextFilter m_visualizerHighlightFilter; - // --- Tabular view members --- + // --- Tabular view members --- - // ImGui filter used to filter TimedRegions. - ImGuiTextFilter m_timedRegionFilter; + // ImGui filter used to filter TimedRegions. + ImGuiTextFilter m_timedRegionFilter; - // Saves statistical view data organized by group name -> region name -> row data - GroupRegionMap m_groupRegionMap; + // Saves statistical view data organized by group name -> region name -> row data + GroupRegionMap m_groupRegionMap; - // Saves pointers to objects in m_groupRegionMap, order reflects table ordering. - // Non-owning, will be cleared when m_groupRegionMap is cleared. - AZStd::vector m_tableData; + // Saves pointers to objects in m_groupRegionMap, order reflects table ordering. + // Non-owning, will be cleared when m_groupRegionMap is cleared. + AZStd::vector m_tableData; - // Pause cpu profiling. The profiler will show the statistics of the last frame before pause. - bool m_paused = false; + // 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; + // 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; + // Toggle between the normal statistical view and the visual profiling view. + bool m_enableVisualizer = false; - // Last captured CPU timing statistics - AZStd::vector m_cpuTimingStatisticsWhenPause; - AZStd::sys_time_t m_frameToFrameTime{}; + // Last captured CPU timing statistics + AZStd::vector m_cpuTimingStatisticsWhenPause; + AZStd::sys_time_t m_frameToFrameTime{}; - AZStd::string m_lastCapturedFilePath; + AZStd::string m_lastCapturedFilePath; - bool m_showFilePicker = false; + bool m_showFilePicker = false; - // Cached file paths to previous traces on disk, sorted with the most recent trace at the front. - AZStd::vector m_cachedCapturePaths; + // Cached file paths to previous traces on disk, sorted with the most recent trace at the front. + AZStd::vector m_cachedCapturePaths; - // Index into the file picker, used to determine which file to load when "Load File" is pressed. - int m_currentFileIndex = 0; + // 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 m_deserializedStringPool; - AZStd::unordered_set m_deserializedGroupRegionNamePool; - }; - } // namespace Render -} // namespace AZ + // --- Loading capture state --- + AZStd::unordered_set m_deserializedStringPool; + AZStd::unordered_set m_deserializedGroupRegionNamePool; + }; +} // namespace Profiler #include "ImGuiCpuProfiler.inl"