diff --git a/AutomatedTesting/Gem/Code/enabled_gems.cmake b/AutomatedTesting/Gem/Code/enabled_gems.cmake index 30740a489d..f653a54505 100644 --- a/AutomatedTesting/Gem/Code/enabled_gems.cmake +++ b/AutomatedTesting/Gem/Code/enabled_gems.cmake @@ -54,4 +54,5 @@ set(ENABLED_GEMS AWSMetrics PrefabBuilder AudioSystem + Profiler ) diff --git a/Gems/Atom/Feature/Common/Code/Include/Atom/Feature/Utils/ProfilingCaptureBus.h b/Gems/Atom/Feature/Common/Code/Include/Atom/Feature/Utils/ProfilingCaptureBus.h index b22e82f456..707e3579a0 100644 --- a/Gems/Atom/Feature/Common/Code/Include/Atom/Feature/Utils/ProfilingCaptureBus.h +++ b/Gems/Atom/Feature/Common/Code/Include/Atom/Feature/Utils/ProfilingCaptureBus.h @@ -28,15 +28,6 @@ namespace AZ //! Dump the PipelineStatistics from passes to a json file. virtual bool CapturePassPipelineStatistics(const AZStd::string& outputFilePath) = 0; - //! Dump a single frame of Cpu profiling data - virtual bool CaptureCpuProfilingStatistics(const AZStd::string& outputFilePath) = 0; - - //! Start a multiframe capture of CPU profiling data. - virtual bool BeginContinuousCpuProfilingCapture() = 0; - - //! End and dump an in-progress continuous capture. - virtual bool EndContinuousCpuProfilingCapture(const AZStd::string& outputFilePath) = 0; - //! Dump the benchmark metadata to a json file. virtual bool CaptureBenchmarkMetadata(const AZStd::string& benchmarkName, const AZStd::string& outputFilePath) = 0; }; @@ -63,11 +54,6 @@ namespace AZ //! @param info The output file path or error information which depends on the return. virtual void OnCaptureQueryPipelineStatisticsFinished(bool result, const AZStd::string& info) = 0; - //! Notify when the current CpuProfilingStatistics capture is finished - //! @param result Set to true if it's finished successfully - //! @param info The output file path or error information which depends on the return. - virtual void OnCaptureCpuProfilingStatisticsFinished(bool result, const AZStd::string& info) = 0; - //! Notify when the current BenchmarkMetadata capture is finished //! @param result Set to true if it's finished successfully //! @param info The output file path or error information which depends on the return. diff --git a/Gems/Atom/Feature/Common/Code/Source/ProfilingCaptureSystemComponent.cpp b/Gems/Atom/Feature/Common/Code/Source/ProfilingCaptureSystemComponent.cpp index 4accbf0bba..66adfe9985 100644 --- a/Gems/Atom/Feature/Common/Code/Source/ProfilingCaptureSystemComponent.cpp +++ b/Gems/Atom/Feature/Common/Code/Source/ProfilingCaptureSystemComponent.cpp @@ -8,7 +8,6 @@ #include "ProfilingCaptureSystemComponent.h" -#include #include #include #include @@ -39,7 +38,6 @@ namespace AZ OnCaptureQueryTimestampFinished, OnCaptureCpuFrameTimeFinished, OnCaptureQueryPipelineStatisticsFinished, - OnCaptureCpuProfilingStatisticsFinished, OnCaptureBenchmarkMetadataFinished ); @@ -58,11 +56,6 @@ namespace AZ Call(FN_OnCaptureQueryPipelineStatisticsFinished, result, info); } - void OnCaptureCpuProfilingStatisticsFinished(bool result, const AZStd::string& info) override - { - Call(FN_OnCaptureCpuProfilingStatisticsFinished, result, info); - } - void OnCaptureBenchmarkMetadataFinished(bool result, const AZStd::string& info) override { Call(FN_OnCaptureBenchmarkMetadataFinished, result, info); @@ -358,7 +351,6 @@ namespace AZ ->Event("CapturePassTimestamp", &ProfilingCaptureRequestBus::Events::CapturePassTimestamp) ->Event("CaptureCpuFrameTime", &ProfilingCaptureRequestBus::Events::CaptureCpuFrameTime) ->Event("CapturePassPipelineStatistics", &ProfilingCaptureRequestBus::Events::CapturePassPipelineStatistics) - ->Event("CaptureCpuProfilingStatistics", &ProfilingCaptureRequestBus::Events::CaptureCpuProfilingStatistics) ->Event("CaptureBenchmarkMetadata", &ProfilingCaptureRequestBus::Events::CaptureBenchmarkMetadata) ; @@ -368,7 +360,6 @@ namespace AZ TimestampSerializer::Reflect(context); CpuFrameTimeSerializer::Reflect(context); PipelineStatisticsSerializer::Reflect(context); - RHI::CpuProfilingStatisticsSerializer::Reflect(context); BenchmarkMetadataSerializer::Reflect(context); } @@ -382,12 +373,6 @@ namespace AZ TickBus::Handler::BusDisconnect(); ProfilingCaptureRequestBus::Handler::BusDisconnect(); - - // Block deactivation until the IO thread has finished serializing the CPU data - if (m_cpuDataSerializationThread.joinable()) - { - m_cpuDataSerializationThread.join(); - } } bool ProfilingCaptureSystemComponent::CapturePassTimestamp(const AZStd::string& outputFilePath) @@ -442,16 +427,7 @@ namespace AZ bool ProfilingCaptureSystemComponent::CaptureCpuFrameTime(const AZStd::string& outputFilePath) { - AZ::RHI::RHISystemInterface::Get()->ModifyFrameSchedulerStatisticsFlags( - AZ::RHI::FrameSchedulerStatisticsFlags::GatherCpuTimingStatistics, true - ); - bool wasEnabled = RHI::CpuProfiler::Get()->IsProfilerEnabled(); - if (!wasEnabled) - { - RHI::CpuProfiler::Get()->SetProfilerEnabled(true); - } - - const bool captureStarted = m_cpuFrameTimeStatisticsCapture.StartCapture([outputFilePath, wasEnabled]() + const bool captureStarted = m_cpuFrameTimeStatisticsCapture.StartCapture([outputFilePath]() { JsonSerializerSettings serializationSettings; serializationSettings.m_keepDefaults = true; @@ -472,15 +448,6 @@ namespace AZ AZ_Warning("ProfilingCaptureSystemComponent", false, captureInfo.c_str()); } - // Disable the profiler again - if (!wasEnabled) - { - RHI::CpuProfiler::Get()->SetProfilerEnabled(false); - } - AZ::RHI::RHISystemInterface::Get()->ModifyFrameSchedulerStatisticsFlags( - AZ::RHI::FrameSchedulerStatisticsFlags::GatherCpuTimingStatistics, false - ); - // Notify listeners that the Cpu frame time statistics capture has finished. ProfilingCaptureNotificationBus::Broadcast(&ProfilingCaptureNotificationBus::Events::OnCaptureCpuFrameTimeFinished, saveResult.IsSuccess(), @@ -546,116 +513,6 @@ namespace AZ return captureStarted; } - bool SerializeCpuProfilingData(const AZStd::ring_buffer& data, AZStd::string outputFilePath, bool wasEnabled) - { - AZ_TracePrintf("ProfilingCaptureSystemComponent", "Beginning serialization of %zu frames of profiling data\n", data.size()); - JsonSerializerSettings serializationSettings; - serializationSettings.m_keepDefaults = true; - - RHI::CpuProfilingStatisticsSerializer serializer(data); - - const auto saveResult = JsonSerializationUtils::SaveObjectToFile(&serializer, - outputFilePath, (RHI::CpuProfilingStatisticsSerializer*)nullptr, &serializationSettings); - - AZStd::string captureInfo = outputFilePath; - if (!saveResult.IsSuccess()) - { - captureInfo = AZStd::string::format("Failed to save Cpu Profiling Statistics data to file '%s'. Error: %s", - outputFilePath.c_str(), - saveResult.GetError().c_str()); - AZ_Warning("ProfilingCaptureSystemComponent", false, captureInfo.c_str()); - } - else - { - AZ_Printf("ProfilingCaptureSystemComponent", "Cpu profiling statistics was saved to file [%s]\n", outputFilePath.c_str()); - } - - // Disable the profiler again - if (!wasEnabled) - { - RHI::CpuProfiler::Get()->SetProfilerEnabled(false); - } - - // Notify listeners that the pass' PipelineStatistics queries capture has finished. - ProfilingCaptureNotificationBus::Broadcast(&ProfilingCaptureNotificationBus::Events::OnCaptureCpuProfilingStatisticsFinished, - saveResult.IsSuccess(), - captureInfo); - return saveResult.IsSuccess(); - } - - bool ProfilingCaptureSystemComponent::CaptureCpuProfilingStatistics(const AZStd::string& outputFilePath) - { - // Start the cpu profiling - bool wasEnabled = RHI::CpuProfiler::Get()->IsProfilerEnabled(); - if (!wasEnabled) - { - RHI::CpuProfiler::Get()->SetProfilerEnabled(true); - } - - const bool captureStarted = m_cpuProfilingStatisticsCapture.StartCapture([outputFilePath, wasEnabled]() - { - // Blocking call for a single frame of data, avoid thread overhead - AZStd::ring_buffer singleFrameData(1); - singleFrameData.push_back(RHI::CpuProfiler::Get()->GetTimeRegionMap()); - SerializeCpuProfilingData(singleFrameData, outputFilePath, wasEnabled); - }); - - // Start the TickBus. - if (captureStarted) - { - TickBus::Handler::BusConnect(); - } - - return captureStarted; - } - - bool ProfilingCaptureSystemComponent::BeginContinuousCpuProfilingCapture() - { - return AZ::RHI::CpuProfiler::Get()->BeginContinuousCapture(); - } - - bool ProfilingCaptureSystemComponent::EndContinuousCpuProfilingCapture(const AZStd::string& outputFilePath) - { - bool expected = false; - if (m_cpuDataSerializationInProgress.compare_exchange_strong(expected, true)) - { - AZStd::ring_buffer captureResult; - const bool captureEnded = AZ::RHI::CpuProfiler::Get()->EndContinuousCapture(captureResult); - if (!captureEnded) - { - AZ_TracePrintf("ProfilingCaptureSystemComponent", "Could not end the continuous capture, is one in progress?\n"); - m_cpuDataSerializationInProgress.store(false); - return false; - } - - // cpuProfilingData could be 1GB+ once saved, so use an IO thread to write it to disk. - auto threadIoFunction = - [data = AZStd::move(captureResult), filePath = AZStd::string(outputFilePath), &flag = m_cpuDataSerializationInProgress]() - { - SerializeCpuProfilingData(data, filePath, true); - flag.store(false); - }; - - // If the thread object already exists (ex. we have already serialized data), join. This will not block since - // m_cpuDataSerializationInProgress was false, meaning the IO thread has already completed execution. - // TODO Use a reusable thread implementation over repeated creation + destruction of threads [ATOM-16214] - if (m_cpuDataSerializationThread.joinable()) - { - m_cpuDataSerializationThread.join(); - } - - auto thread = AZStd::thread(threadIoFunction); - m_cpuDataSerializationThread = AZStd::move(thread); - - return true; - } - - AZ_TracePrintf( - "ProfilingSystemCaptureComponent", - "Cannot end a continuous capture - another serialization is currently in progress\n"); - return false; - } - bool ProfilingCaptureSystemComponent::CaptureBenchmarkMetadata(const AZStd::string& benchmarkName, const AZStd::string& outputFilePath) { const bool captureStarted = m_benchmarkMetadataCapture.StartCapture([benchmarkName, outputFilePath]() @@ -734,11 +591,10 @@ namespace AZ m_timestampCapture.UpdateCapture(); m_cpuFrameTimeStatisticsCapture.UpdateCapture(); m_pipelineStatisticsCapture.UpdateCapture(); - m_cpuProfilingStatisticsCapture.UpdateCapture(); m_benchmarkMetadataCapture.UpdateCapture(); // Disconnect from the TickBus if all capture states are set to idle. - if (m_timestampCapture.IsIdle() && m_pipelineStatisticsCapture.IsIdle() && m_cpuProfilingStatisticsCapture.IsIdle() && m_benchmarkMetadataCapture.IsIdle() && m_cpuFrameTimeStatisticsCapture.IsIdle()) + if (m_timestampCapture.IsIdle() && m_pipelineStatisticsCapture.IsIdle() && m_benchmarkMetadataCapture.IsIdle() && m_cpuFrameTimeStatisticsCapture.IsIdle()) { TickBus::Handler::BusDisconnect(); } diff --git a/Gems/Atom/Feature/Common/Code/Source/ProfilingCaptureSystemComponent.h b/Gems/Atom/Feature/Common/Code/Source/ProfilingCaptureSystemComponent.h index 9f8a8a90c6..a9bb8c585f 100644 --- a/Gems/Atom/Feature/Common/Code/Source/ProfilingCaptureSystemComponent.h +++ b/Gems/Atom/Feature/Common/Code/Source/ProfilingCaptureSystemComponent.h @@ -70,9 +70,6 @@ namespace AZ bool CapturePassTimestamp(const AZStd::string& outputFilePath) override; bool CaptureCpuFrameTime(const AZStd::string& outputFilePath) override; bool CapturePassPipelineStatistics(const AZStd::string& outputFilePath) override; - bool CaptureCpuProfilingStatistics(const AZStd::string& outputFilePath) override; - bool BeginContinuousCpuProfilingCapture() override; - bool EndContinuousCpuProfilingCapture(const AZStd::string& outputFilePath) override; bool CaptureBenchmarkMetadata(const AZStd::string& benchmarkName, const AZStd::string& outputFilePath) override; private: @@ -86,13 +83,7 @@ namespace AZ DelayedQueryCaptureHelper m_timestampCapture; DelayedQueryCaptureHelper m_cpuFrameTimeStatisticsCapture; DelayedQueryCaptureHelper m_pipelineStatisticsCapture; - DelayedQueryCaptureHelper m_cpuProfilingStatisticsCapture; DelayedQueryCaptureHelper m_benchmarkMetadataCapture; - - // Flag passed by reference to the CPU profiling data serialization job, blocks new continuous capture requests when set. - AZStd::atomic_bool m_cpuDataSerializationInProgress = false; - - AZStd::thread m_cpuDataSerializationThread; }; } } diff --git a/Gems/Atom/RHI/Code/Include/Atom/RHI.Reflect/FrameSchedulerEnums.h b/Gems/Atom/RHI/Code/Include/Atom/RHI.Reflect/FrameSchedulerEnums.h index b0c99248f1..a47e186e3b 100644 --- a/Gems/Atom/RHI/Code/Include/Atom/RHI.Reflect/FrameSchedulerEnums.h +++ b/Gems/Atom/RHI/Code/Include/Atom/RHI.Reflect/FrameSchedulerEnums.h @@ -66,9 +66,6 @@ namespace AZ { None = 0, - //! Enables gathering of cpu timing statistics. - GatherCpuTimingStatistics = AZ_BIT(0), - //! Enables gathering of transient attachment statistics. GatherTransientAttachmentStatistics = AZ_BIT(2), diff --git a/Gems/Atom/RHI/Code/Include/Atom/RHI/CpuProfiler.h b/Gems/Atom/RHI/Code/Include/Atom/RHI/CpuProfiler.h deleted file mode 100644 index 70c5771b57..0000000000 --- a/Gems/Atom/RHI/Code/Include/Atom/RHI/CpuProfiler.h +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright (c) Contributors to the Open 3D Engine Project. - * For complete copyright and license terms please see the LICENSE at the root of this distribution. - * - * SPDX-License-Identifier: Apache-2.0 OR MIT - * - */ - -#pragma once - -#include -#include -#include -#include -#include -#include - -namespace AZ -{ - namespace RHI - { - //! 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 - { - GroupRegionName() = delete; - GroupRegionName(const char* const group, const char* const region); - - 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; - }; - - CachedTimeRegion() = default; - CachedTimeRegion(const GroupRegionName& groupRegionName); - CachedTimeRegion(const GroupRegionName& groupRegionName, uint16_t stackDepth, uint64_t startTick, uint64_t endTick); - - GroupRegionName m_groupRegionName{nullptr, nullptr}; - - uint16_t m_stackDepth = 0u; - AZStd::sys_time_t m_startTick = 0; - AZStd::sys_time_t m_endTick = 0; - }; - - //! Interface class of the CpuProfiler - class CpuProfiler - { - public: - using ThreadTimeRegionMap = AZStd::unordered_map>; - using TimeRegionMap = AZStd::unordered_map; - - AZ_RTTI(CpuProfiler, "{127C1D0B-BE05-4E18-A8F6-24F3EED2ECA6}"); - - CpuProfiler() = default; - virtual ~CpuProfiler() = default; - - AZ_DISABLE_COPY_MOVE(CpuProfiler); - - static CpuProfiler* Get(); - - //! Get the last frame's TimeRegionMap - virtual const TimeRegionMap& GetTimeRegionMap() const = 0; - - //! Begin a continuous capture. Blocks the profiler from being toggled off until EndContinuousCapture is called. - [[nodiscard]] virtual bool BeginContinuousCapture() = 0; - - //! Flush the CPU Profiler's saved data into the passed ring buffer . - [[nodiscard]] virtual bool EndContinuousCapture(AZStd::ring_buffer& flushTarget) = 0; - - virtual bool IsContinuousCaptureInProgress() const = 0; - - //! Enable/Disable the CpuProfiler - virtual void SetProfilerEnabled(bool enabled) = 0; - - virtual bool IsProfilerEnabled() const = 0 ; - }; - - } // namespace RPI -} // namespace AZ diff --git a/Gems/Atom/RHI/Code/Include/Atom/RHI/CpuProfilerImpl.h b/Gems/Atom/RHI/Code/Include/Atom/RHI/CpuProfilerImpl.h deleted file mode 100644 index 29886625ea..0000000000 --- a/Gems/Atom/RHI/Code/Include/Atom/RHI/CpuProfilerImpl.h +++ /dev/null @@ -1,188 +0,0 @@ -/* - * Copyright (c) Contributors to the Open 3D Engine Project. - * For complete copyright and license terms please see the LICENSE at the root of this distribution. - * - * SPDX-License-Identifier: Apache-2.0 OR MIT - * - */ -#pragma once - -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include - - -namespace AZ -{ - 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 - { - friend class CpuProfilerImpl; - - public: - AZ_CLASS_ALLOCATOR(CpuTimingLocalStorage, AZ::OSAllocator, 0); - - CpuTimingLocalStorage(); - ~CpuTimingLocalStorage(); - - 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); - - // 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); - - // 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; - - // 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; - - // 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; - - // 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; - }; - - //! 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); - - CpuProfilerImpl() = default; - ~CpuProfilerImpl() = default; - - //! 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; - - //! 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; - - 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(); - - // 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; - - // 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; - - // This lock will only be contested when the CpuProfiler's Shutdown() method has been called - AZStd::shared_mutex m_shutdownMutex; - - bool m_initialized = false; - - AZStd::mutex m_continuousCaptureEndingMutex; - - 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; - }; - - // Intermediate class to serialize Cpu TimedRegion data. - class CpuProfilingStatisticsSerializer - { - public: - class CpuProfilingStatisticsSerializerEntry - { - public: - AZ_TYPE_INFO(CpuProfilingStatisticsSerializer::CpuProfilingStatisticsSerializerEntry, "{26B78F65-EB96-46E2-BE7E-A1233880B225}"); - static void Reflect(AZ::ReflectContext* context); - - CpuProfilingStatisticsSerializerEntry() = default; - CpuProfilingStatisticsSerializerEntry(const RHI::CachedTimeRegion& cachedTimeRegion, 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}"); - static void Reflect(AZ::ReflectContext* context); - - CpuProfilingStatisticsSerializer() = default; - CpuProfilingStatisticsSerializer(const AZStd::ring_buffer& continuousData); - - AZStd::vector m_cpuProfilingStatisticsSerializerEntries; - }; - }; // namespace RHI -}; // namespace AZ diff --git a/Gems/Atom/RHI/Code/Include/Atom/RHI/RHISystem.h b/Gems/Atom/RHI/Code/Include/Atom/RHI/RHISystem.h index 52a44c0903..6d416b77fc 100644 --- a/Gems/Atom/RHI/Code/Include/Atom/RHI/RHISystem.h +++ b/Gems/Atom/RHI/Code/Include/Atom/RHI/RHISystem.h @@ -8,7 +8,6 @@ #pragma once -#include #include #include #include @@ -66,8 +65,6 @@ namespace AZ RHI::Ptr m_pipelineStateCache; RHI::FrameScheduler m_frameScheduler; RHI::FrameSchedulerCompileRequest m_compileRequest; - - RHI::CpuProfilerImpl m_cpuProfiler; }; } // namespace RPI } // namespace AZ diff --git a/Gems/Atom/RHI/Code/Source/RHI/CpuProfilerImpl.cpp b/Gems/Atom/RHI/Code/Source/RHI/CpuProfilerImpl.cpp deleted file mode 100644 index 826e0b6aa3..0000000000 --- a/Gems/Atom/RHI/Code/Source/RHI/CpuProfilerImpl.cpp +++ /dev/null @@ -1,448 +0,0 @@ -/* - * Copyright (c) Contributors to the Open 3D Engine Project. - * For complete copyright and license terms please see the LICENSE at the root of this distribution. - * - * SPDX-License-Identifier: Apache-2.0 OR MIT - * - */ - -#include - -#include -#include - -#include -#include -#include - -namespace AZ -{ - namespace RHI - { - thread_local CpuTimingLocalStorage* CpuProfilerImpl::ms_threadLocalStorage = nullptr; - - // --- CpuProfiler --- - - CpuProfiler* CpuProfiler::Get() - { - return Interface::Get(); - } - - // --- CachedTimeRegion --- - - 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; - } - - // --- GroupRegionName --- - - 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; - } - - bool CachedTimeRegion::GroupRegionName::operator==(const GroupRegionName& other) const - { - return (m_groupName == other.m_groupName) && (m_regionName == other.m_regionName); - } - - - // --- CpuProfilerImpl --- - - 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); - } - } - - void CpuProfilerImpl::Shutdown() - { - if (!m_initialized) - { - 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); - - 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(); - } - - 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()) - { - if (m_enabled) - { - // 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(); - } - } - - 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()) - { - // guard against enabling mid-marker - if (m_enabled && ms_threadLocalStorage != nullptr) - { - ms_threadLocalStorage->RegionStackPopBack(); - } - - m_shutdownMutex.unlock_shared(); - } - } - - const CpuProfiler::TimeRegionMap& CpuProfilerImpl::GetTimeRegionMap() const - { - return m_timeRegionMap; - } - - bool CpuProfilerImpl::BeginContinuousCapture() - { - 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"); - return false; - } - - bool CpuProfilerImpl::EndContinuousCapture(AZStd::ring_buffer& flushTarget) - { - 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; - } - - 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; - } - - // 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; - } - } - - bool CpuProfilerImpl::IsProfilerEnabled() const - { - return m_enabled; - } - - void CpuProfilerImpl::OnSystemTick() - { - if (!m_enabled) - { - return; - } - - if (m_continuousCaptureInProgress.load() && m_continuousCaptureEndingMutex.try_lock()) - { - 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(); - } - - 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); - } - - // 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(); - }); - - // Update our saved time regions to the last frame's collected data - m_timeRegionMap = AZStd::move(newMap); - } - - void CpuProfilerImpl::RegisterThreadStorage() - { - AZStd::unique_lock lock(m_threadRegisterMutex); - if (!ms_threadLocalStorage) - { - ms_threadLocalStorage = aznew CpuTimingLocalStorage(); - m_registeredThreads.emplace_back(ms_threadLocalStorage); - } - } - - // --- CpuTimingLocalStorage --- - - CpuTimingLocalStorage::CpuTimingLocalStorage() - { - m_executingThreadId = AZStd::this_thread::get_id(); - } - - CpuTimingLocalStorage::~CpuTimingLocalStorage() - { - m_deleteFlag = true; - } - - void CpuTimingLocalStorage::RegionStackPushBack(CachedTimeRegion& timeRegion) - { - // If it was (re)enabled, clear the lists first - if (m_clearContainers) - { - m_clearContainers = false; - - m_stackLevel = 0; - m_cachedTimeRegionMap.clear(); - m_timeRegionStack.clear(); - m_cachedTimeRegions.clear(); - } - - 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); - - // 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(); - } - - 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()) - { - return; - } - - // 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(); - - // Set the ending time - back.m_endTick = endRegionTime; - - // Decrement the stack - m_stackLevel--; - - // 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) - { - 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); - - // 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) - { - m_hitSizeLimitMap.insert_or_assign(AZStd::move(regionName), true); - } - } - - // Clear the cached regions - m_cachedTimeRegions.clear(); - } - } - - 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()) - { - // 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(); - } - } - - // --- CpuProfilingStatisticsSerializer --- - - CpuProfilingStatisticsSerializer::CpuProfilingStatisticsSerializer(const AZStd::ring_buffer& continuousData) - { - // Create serializable entries - for (const auto& timeRegionMap : continuousData) - { - for (const auto& [threadId, regionMap] : timeRegionMap) - { - for (const auto& [regionName, regionVec] : regionMap) - { - for (const auto& region : regionVec) - { - m_cpuProfilingStatisticsSerializerEntries.emplace_back(region, threadId); - } - } - } - } - } - - void CpuProfilingStatisticsSerializer::Reflect(AZ::ReflectContext* context) - { - if (auto* serializeContext = azrtti_cast(context)) - { - serializeContext->Class() - ->Version(1) - ->Field("cpuProfilingStatisticsSerializerEntries", &CpuProfilingStatisticsSerializer::m_cpuProfilingStatisticsSerializerEntries) - ; - } - - CpuProfilingStatisticsSerializerEntry::Reflect(context); - } - - // --- CpuProfilingStatisticsSerializerEntry --- - - 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)) - { - 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 diff --git a/Gems/Atom/RHI/Code/Source/RHI/FrameScheduler.cpp b/Gems/Atom/RHI/Code/Source/RHI/FrameScheduler.cpp index a15db9e24b..11ae78c69a 100644 --- a/Gems/Atom/RHI/Code/Source/RHI/FrameScheduler.cpp +++ b/Gems/Atom/RHI/Code/Source/RHI/FrameScheduler.cpp @@ -86,6 +86,8 @@ namespace AZ if (auto statsProfiler = AZ::Interface::Get(); statsProfiler) { + statsProfiler->ActivateProfiler(rhiMetricsId, true); + auto& rhiMetrics = statsProfiler->GetProfiler(rhiMetricsId); rhiMetrics.GetStatsManager().AddStatistic(frameTimeMetricId, frameTimeMetricName, /*units=*/"clocks", /*failIfExist=*/false); } @@ -602,14 +604,11 @@ namespace AZ double FrameScheduler::GetCpuFrameTime() const { - if (CheckBitsAny(m_compileRequest.m_statisticsFlags, FrameSchedulerStatisticsFlags::GatherCpuTimingStatistics)) + if (auto statsProfiler = AZ::Interface::Get(); statsProfiler) { - if (auto statsProfiler = AZ::Interface::Get(); statsProfiler) - { - auto& rhiMetrics = statsProfiler->GetProfiler(rhiMetricsId); - const auto* frameTimeStat = rhiMetrics.GetStatistic(frameTimeMetricId); - return (frameTimeStat->GetMostRecentSample() * 1000) / aznumeric_cast(AZStd::GetTimeTicksPerSecond()); - } + auto& rhiMetrics = statsProfiler->GetProfiler(rhiMetricsId); + const auto* frameTimeStat = rhiMetrics.GetStatistic(frameTimeMetricId); + return (frameTimeStat->GetMostRecentSample() * 1000) / aznumeric_cast(AZStd::GetTimeTicksPerSecond()); } return 0; } diff --git a/Gems/Atom/RHI/Code/Source/RHI/RHISystem.cpp b/Gems/Atom/RHI/Code/Source/RHI/RHISystem.cpp index b40ad3e11a..e7515bbf6e 100644 --- a/Gems/Atom/RHI/Code/Source/RHI/RHISystem.cpp +++ b/Gems/Atom/RHI/Code/Source/RHI/RHISystem.cpp @@ -39,8 +39,6 @@ namespace AZ void RHISystem::Init() { - m_cpuProfiler.Init(); - Ptr platformLimitsDescriptor = m_device->GetDescriptor().m_platformLimitsDescriptor; RHI::FrameSchedulerDescriptor frameSchedulerDescriptor; @@ -187,8 +185,6 @@ namespace AZ AZ_Assert(m_device->use_count()==1, "The ref count for Device is %i but it should be 1 here to ensure all the resources are released", m_device->use_count()); m_device = nullptr; } - - m_cpuProfiler.Shutdown(); } void RHISystem::FrameUpdate(FrameGraphCallback frameGraphCallback) diff --git a/Gems/Atom/RHI/Code/atom_rhi_public_files.cmake b/Gems/Atom/RHI/Code/atom_rhi_public_files.cmake index 338bae6386..60ae24ae9b 100644 --- a/Gems/Atom/RHI/Code/atom_rhi_public_files.cmake +++ b/Gems/Atom/RHI/Code/atom_rhi_public_files.cmake @@ -197,8 +197,5 @@ set(FILES Include/Atom/RHI/interval_map.h Include/Atom/RHI/ImageProperty.h Include/Atom/RHI/BufferProperty.h - Include/Atom/RHI/CpuProfiler.h - Include/Atom/RHI/CpuProfilerImpl.h - Source/RHI/CpuProfilerImpl.cpp Include/Atom/RHI/TagRegistry.h ) diff --git a/Gems/Atom/Tools/MaterialEditor/Code/Source/Viewport/PerformanceMonitorComponent.cpp b/Gems/Atom/Tools/MaterialEditor/Code/Source/Viewport/PerformanceMonitorComponent.cpp index c3bb13d1d2..563b2754df 100644 --- a/Gems/Atom/Tools/MaterialEditor/Code/Source/Viewport/PerformanceMonitorComponent.cpp +++ b/Gems/Atom/Tools/MaterialEditor/Code/Source/Viewport/PerformanceMonitorComponent.cpp @@ -9,7 +9,6 @@ #include #include -#include #include #include @@ -66,12 +65,6 @@ namespace MaterialEditor AZ_Error("PerformanceMonitorComponent", false, "Failed to find root pass."); } - AZ::RHI::RHISystemInterface::Get()->ModifyFrameSchedulerStatisticsFlags( - AZ::RHI::FrameSchedulerStatisticsFlags::GatherCpuTimingStatistics, - enabled); - - AZ::RHI::CpuProfiler::Get()->SetProfilerEnabled(enabled); - if (enabled) { ResetStats(); diff --git a/Gems/Atom/Utils/Code/Include/Atom/Utils/ImGuiCpuProfiler.h b/Gems/Atom/Utils/Code/Include/Atom/Utils/ImGuiCpuProfiler.h deleted file mode 100644 index 4a326660dd..0000000000 --- a/Gems/Atom/Utils/Code/Include/Atom/Utils/ImGuiCpuProfiler.h +++ /dev/null @@ -1,231 +0,0 @@ -/* - * Copyright (c) Contributors to the Open 3D Engine Project. - * For complete copyright and license terms please see the LICENSE at the root of this distribution. - * - * SPDX-License-Identifier: Apache-2.0 OR MIT - * - */ - -#pragma once - -#include -#include -#include - -#include - - -namespace AZ -{ - namespace Render - { - //! Stores all the data associated with a row in the table. - struct TableRow - { - template - struct TableRowCompareFunctor - { - TableRowCompareFunctor(T memberPointer, bool isAscending) : m_memberPointer(memberPointer), m_ascending(isAscending){}; - - 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; - }; - - // Update running statistics with new region data - void RecordRegion(const AZ::RHI::CachedTimeRegion& region, size_t threadId); - - void ResetPerFrameStatistics(); - - // 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; - - // --- Per frame statistics --- - - u64 m_invocationsLastFrame = 0; - - // 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; - - // Maximum execution time of a region in the last frame. - AZStd::sys_time_t m_maxTicks = 0; - - // --- Aggregate statistics --- - - u64 m_invocationsTotal = 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; - - using TimeRegion = AZ::RHI::CachedTimeRegion; - using GroupRegionName = AZ::RHI::CachedTimeRegion::GroupRegionName; - - public: - struct CpuTimingEntry - { - const AZStd::string& m_name; - double m_executeDuration; - }; - - ImGuiCpuProfiler() = default; - ~ImGuiCpuProfiler() = default; - - //! 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 - - //! Draws the statistical view of the CPU profiling data. - void DrawStatisticsView(); - - //! Callback invoked when the "Load File" button is pressed in the file picker. - void LoadFile(); - - //! Draws the file picker window. - void DrawFilePicker(); - - //! Draws the CPU profiling visualizer. - void DrawVisualizer(); - - // 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(); - - // Sort the table by a given column, rearranges the pointers in m_tableData. - void SortTable(ImGuiTableSortSpecs* sortSpecs); - - // 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(); - - // 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); - - // 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 vertical lines separating frames in the timeline - void DrawFrameBoundaries(); - - // Draw the ruler with frame time labels - void DrawRuler(); - - // 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; - - 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); - - // System tick bus overrides - virtual void OnSystemTick() override; - - // --- Visualizer Members --- - - int m_framesToCollect = DefaultFramesToCollect; - - // 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; - - // 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; - - // Tracks the frame boundaries - AZStd::vector m_frameEndTicks = { INT64_MIN }; - - // Filter for highlighting regions on the visualizer - ImGuiTextFilter m_visualizerHighlightFilter; - - // --- Tabular view members --- - - // 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 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; - - // 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; - - // Last captured CPU timing statistics - AZStd::vector m_cpuTimingStatisticsWhenPause; - AZStd::sys_time_t m_frameToFrameTime{}; - - AZStd::string m_lastCapturedFilePath; - - bool m_showFilePicker = false; - - // Cached file paths to previous traces on disk, sorted with the most recent trace at the front. - AZStd::vector m_cachedCapturePaths; - - // 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 - -#include "ImGuiCpuProfiler.inl" diff --git a/Gems/Atom/Utils/Code/Include/Atom/Utils/ImGuiCpuProfiler.inl b/Gems/Atom/Utils/Code/Include/Atom/Utils/ImGuiCpuProfiler.inl deleted file mode 100644 index daf6eca5c0..0000000000 --- a/Gems/Atom/Utils/Code/Include/Atom/Utils/ImGuiCpuProfiler.inl +++ /dev/null @@ -1,1159 +0,0 @@ -/* - * Copyright (c) Contributors to the Open 3D Engine Project. - * For complete copyright and license terms please see the LICENSE at the root of this distribution. - * - * SPDX-License-Identifier: Apache-2.0 OR MIT - * - */ - -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - - -namespace AZ -{ - namespace Render - { - namespace CpuProfilerImGuiHelper - { - 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)) - { - return Failure(AZStd::string::format("Could not resolve the path to file %s, is the path correct?", resolvedPath)); - } - - u64 captureSizeBytes; - const IO::Result fileSizeResult = base->Size(resolvedPath, captureSizeBytes); - if (!fileSizeResult) - { - return Failure(AZStd::string::format("Could not read the size of file %s, is the path correct?", resolvedPath)); - } - - // NOTE: this uses raw file pointers over the abstractions and utility functions provided by AZ::JsonSerializationUtils because - // saved profiling captures can be upwards of 400 MB. This necessitates a buffered approach to avoid allocating huge chunks of memory. - FILE* fp = nullptr; - azfopen(&fp, resolvedPath, "rb"); - if (!fp) - { - return Failure(AZStd::string::format("Could not fopen file %s, is the path correct?\n", resolvedPath)); - } - - constexpr AZStd::size_t MaxBufSize = 65536; - const AZStd::size_t bufSize = AZStd::min(MaxBufSize, aznumeric_cast(captureSizeBytes)); - char* buf = reinterpret_cast(azmalloc(bufSize)); - - rapidjson::Document document; - rapidjson::FileReadStream inputStream(fp, buf, bufSize); - document.ParseStream(inputStream); - - azfree(buf); - fclose(fp); - - if (document.HasParseError()) - { - const auto pe = document.GetParseError(); - return Failure(AZStd::string::format( - "Rapidjson could not parse the document with ParseErrorCode %u. See 3rdParty/rapidjson/error.h for definitions.\n", pe)); - } - - if (!document.IsObject() || !document.HasMember("ClassData")) - { - return Failure(AZStd::string::format( - "Error in loading saved capture: top-level object does not have a ClassData field. Did the serialization format change recently?\n")); - } - - AZ_TracePrintf("JsonUtils", "Successfully loaded JSON into memory.\n"); - - const auto& root = document["ClassData"]; - RHI::CpuProfilingStatisticsSerializer serializer; - const JsonSerializationResult::ResultCode deserializationResult = JsonSerialization::Load(serializer, root); - if (deserializationResult.GetProcessing() == JsonSerializationResult::Processing::Halted - || serializer.m_cpuProfilingStatisticsSerializerEntries.empty()) - { - return Failure(AZStd::string::format("Error in deserializing document: %s\n", deserializationResult.ToString(capturePath.c_str()).c_str())); - } - - AZ_TracePrintf("JsonUtils", "Successfully loaded CPU profiling data with %zu profiling entries.\n", - serializer.m_cpuProfilingStatisticsSerializerEntries.size()); - - return Success(AZStd::move(serializer.m_cpuProfilingStatisticsSerializerEntries)); - } - } // namespace CpuProfilerImGuiHelper - - - - inline void ImGuiCpuProfiler::Draw(bool& keepDrawing) - { - // 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(); - - // Only listen to system ticks when the profiler is active - if (!SystemTickBus::Handler::BusIsConnected()) - { - SystemTickBus::Handler::BusConnect(); - } - } - - if (m_enableVisualizer) - { - DrawVisualizer(); - } - else - { - DrawStatisticsView(); - } - - if (m_showFilePicker) - { - DrawFilePicker(); - } - } - ImGui::End(); - - if (m_captureToFile) - { - 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; - - // Toggle if the bool isn't the same as the cached value - if (cachedShowCpuProfiler != keepDrawing) - { - AZ::RHI::CpuProfiler::Get()->SetProfilerEnabled(keepDrawing); - } - } - - inline void ImGuiCpuProfiler::DrawCommonHeader() - { - if (!m_lastCapturedFilePath.empty()) - { - ImGui::Text("Saved: %s", m_lastCapturedFilePath.c_str()); - } - - if (ImGui::Button(m_enableVisualizer ? "Swap to statistics" : "Swap to visualizer")) - { - m_enableVisualizer = !m_enableVisualizer; - } - - 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); - } - - ImGui::SameLine(); - if (ImGui::Button("Capture")) - { - m_captureToFile = true; - } - - 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; - 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; - } - - else - { - AZ::Render::ProfilingCaptureRequestBus::Broadcast( - &AZ::Render::ProfilingCaptureRequestBus::Events::BeginContinuousCpuProfilingCapture); - } - } - - ImGui::SameLine(); - if (ImGui::Button("Load file")) - { - m_showFilePicker = true; - - // Only update the cached file list when opened so that we aren't making IO calls on every frame. - auto* base = AZ::IO::FileIOBase::GetInstance(); - const AZStd::string defaultSavedCapturePath = "@user@/CpuProfiler"; - - m_cachedCapturePaths.clear(); - base->FindFiles( - defaultSavedCapturePath.c_str(), "*.json", - [&paths = m_cachedCapturePaths](const char* path) -> bool - { - auto foundPath = IO::Path(path); - paths.push_back(foundPath); - return true; - }); - - // Sort by decreasing modification time (most recent at the top) - AZStd::sort(m_cachedCapturePaths.begin(), m_cachedCapturePaths.end(), - [&base](const IO::Path& lhs, const IO::Path& rhs) - { - return base->ModificationTime(lhs.c_str()) > base->ModificationTime(rhs.c_str()); - }); - } - } - - 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) - { - SortTable(sortSpecs); - } - - // 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(); - - ImGui::Text("%s", statistics->m_regionName.c_str()); - ImGui::TableNextColumn(); - - ImGui::Text("%.2f", CpuProfilerImGuiHelper::TicksToMs(statistics->m_runningAverageTicks)); - ImGui::TableNextColumn(); - - ImGui::Text("%.2f", CpuProfilerImGuiHelper::TicksToMs(statistics->m_maxTicks)); - ImGui::TableNextColumn(); - - ImGui::Text("%llu", statistics->m_invocationsLastFrame); - ImGui::TableNextColumn(); - - ImGui::Text("%.2f", CpuProfilerImGuiHelper::TicksToMs(statistics->m_lastFrameTotalTicks)); - const ImVec2 botRightBound = ImGui::GetItemRectMax(); - ImGui::TableNextColumn(); - - // 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(); - } - } - } - ImGui::EndTable(); - } - - inline void ImGuiCpuProfiler::SortTable(ImGuiTableSortSpecs* sortSpecs) - { - const bool ascending = sortSpecs->Specs->SortDirection == ImGuiSortDirection_Ascending; - const ImS16 columnToSort = sortSpecs->Specs->ColumnIndex; - - 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; - } - - inline void ImGuiCpuProfiler::DrawStatisticsView() - { - DrawCommonHeader(); - - const auto ShowRow = [](const char* regionLabel, double duration) - { - ImGui::Text("%s", regionLabel); - ImGui::NextColumn(); - - ImGui::Text("%.2f ms", CpuProfilerImGuiHelper::TicksToMs(duration)); - ImGui::NextColumn(); - }; - - 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); - - 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")) - { - m_tableData.clear(); - m_groupRegionMap.clear(); - } - - DrawTable(); - } - } - - inline void ImGuiCpuProfiler::DrawFilePicker() - { - ImGui::SetNextWindowSize({ 500, 200 }, ImGuiCond_Once); - if (ImGui::Begin("File Picker", &m_showFilePicker)) - { - if (ImGui::Button("Load selected")) - { - LoadFile(); - } - - auto getter = [](void* vectorPointer, int idx, const char** out_text) -> bool - { - const auto& pathVec = *static_cast*>(vectorPointer); - if (idx < 0 || idx >= pathVec.size()) - { - return false; - } - *out_text = pathVec[idx].c_str(); - return true; - }; - - ImGui::SetNextItemWidth(ImGui::GetWindowContentRegionWidth()); - ImGui::ListBox("", &m_currentFileIndex, getter, &m_cachedCapturePaths, aznumeric_cast(m_cachedCapturePaths.size())); - } - ImGui::End(); - } - - inline void ImGuiCpuProfiler::LoadFile() - { - const IO::Path& pathToLoad = m_cachedCapturePaths[m_currentFileIndex]; - auto loadResult = CpuProfilerImGuiHelper::LoadSavedCpuProfilingStatistics(pathToLoad.String()); - if (!loadResult.IsSuccess()) - { - AZ_TracePrintf("ImGuiCpuProfiler", "%s", loadResult.GetError().c_str()); - return; - } - - 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(); - - m_tableData.clear(); - m_groupRegionMap.clear(); - - for (const auto& entry : deserializedData) - { - const auto [groupNameItr, wasGroupNameInserted] = m_deserializedStringPool.emplace(entry.m_groupName.GetCStr()); - const auto [regionNameItr, wasRegionNameInserted] = m_deserializedStringPool.emplace(entry.m_regionName.GetCStr()); - const auto [groupRegionNameItr, wasGroupRegionNameInserted] = - m_deserializedGroupRegionNamePool.emplace(groupNameItr->c_str(), regionNameItr->c_str()); - - const RHI::CachedTimeRegion newRegion(*groupRegionNameItr, entry.m_stackDepth, entry.m_startTick, entry.m_endTick); - m_savedData[entry.m_threadId].push_back(newRegion); - - // Since we don't serialize the frame boundaries, we need to use the RPI's OnSystemTick event as a heuristic. - const static Name frameBoundaryName = Name("RPISystem: OnSystemTick"); - if (entry.m_regionName == frameBoundaryName) - { - m_frameEndTicks.push_back(entry.m_endTick); - } - - // Update running statistics - if (!m_groupRegionMap[*groupNameItr].contains(*regionNameItr)) - { - m_groupRegionMap[*groupNameItr][*regionNameItr].m_groupName = *groupNameItr; - m_groupRegionMap[*groupNameItr][*regionNameItr].m_regionName = *regionNameItr; - m_tableData.push_back(&m_groupRegionMap[*groupNameItr][*regionNameItr]); - } - m_groupRegionMap[*groupNameItr][*regionNameItr].RecordRegion(newRegion, entry.m_threadId); - } - - // Update viewport bounds with some added UX fudge factor - m_viewportStartTick = deserializedData.back().m_startTick - 1000; - m_viewportEndTick = deserializedData.back().m_endTick + 1000; - - // Invariant: each vector in m_savedData must be sorted so that we can efficiently cull region data. - for (auto& [threadId, singleThreadData] : m_savedData) - { - AZStd::sort(singleThreadData.begin(), singleThreadData.end(), - [](const TimeRegion& lhs, const TimeRegion& rhs) - { - return lhs.m_startTick < rhs.m_startTick; - }); - } - } - - // -- CPU Visualizer -- - inline void ImGuiCpuProfiler::DrawVisualizer() - { - DrawCommonHeader(); - - // 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::NextColumn(); - - 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::NextColumn(); - - ImGui::TextWrapped( - "Hold the right mouse button to move around. Zoom by scrolling the mouse wheel while holding ."); - } - - ImGui::Columns(1, "FrameTimeColumn", true); - - if (ImGui::BeginChild("FrameTimeHistogram", { 0, 50 }, true, ImGuiWindowFlags_NoScrollbar)) - { - DrawFrameTimeHistogram(); - } - ImGui::EndChild(); - - ImGui::Columns(1, "RulerColumn", true); - - // Ruler - if (ImGui::BeginChild("Ruler", { 0, 30 }, true, ImGuiWindowFlags_NoNavFocus)) - { - DrawRuler(); - } - ImGui::EndChild(); - - - ImGui::Columns(1, "TimelineColumn", true); - - // 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) - { - --nextFrameBoundaryItr; - } - const AZStd::sys_time_t nextFrameBoundary = *nextFrameBoundaryItr; - - // 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()) - { - --startTickItr; - } - - // Main draw loop - u64 baseRow = 0; - for (const auto& [currentThreadId, singleThreadData] : m_savedData) - { - // 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; - } - - // Draw all of the blocks for a given thread/row - u64 maxDepth = 0; - while (regionItr != singleThreadData.end()) - { - 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; - } - - // Draw UI details - DrawThreadLabel(baseRow, currentThreadId); - DrawThreadSeparator(baseRow, maxDepth); - - 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()) - { - io.WantCaptureMouse = true; - if (ImGui::IsMouseDragging(ImGuiMouseButton_Right)) // Scrolling - { - 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); - } - } - 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(); - - 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); - - // Avoid zooming too much, start tick should always be less than end tick - if (newStartTick < newEndTick) - { - m_viewportStartTick = newStartTick; - m_viewportEndTick = newEndTick; - } - } - } - } - ImGui::EndChild(); - } - - inline void ImGuiCpuProfiler::CacheCpuTimingStatistics() - { - using namespace AZ::Statistics; - - m_cpuTimingStatisticsWhenPause.clear(); - if (auto statsProfiler = AZ::Interface::Get(); statsProfiler) - { - 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()); - } - - AZStd::vector statistics; - rhiMetrics.GetStatsManager().GetAllStatistics(statistics); - - 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. - - // 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(); - - // 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) - { - 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) - { - for (const TimeRegion& region : regionVec) - { - newVisualizerData.push_back(region); // Copies - - // 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); - } - } - - // 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); - - 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())); - } - } - - 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); - - // Remove old region data for each thread - for (auto& [threadId, savedRegions] : m_savedData) - { - AZStd::size_t sizeBeforeRemove = savedRegions.size(); - - // Early out to avoid the linear erase_if call - if (savedRegions.size() >= 1 && savedRegions.at(0).m_startTick > deleteBeforeTick) - { - continue; - } - - // Use erase_if over plain upper_bound + erase to avoid repeated shifts. erase requires a shift of all elements to the right - // for each element that is erased, while erase_if squashes all removes into a single shift which significantly improves perf. - AZStd::erase_if( - savedRegions, - [deleteBeforeTick](const TimeRegion& region) - { - return region.m_startTick < deleteBeforeTick; - }); - - m_savedRegionCount -= sizeBeforeRemove - savedRegions.size(); - } - - // Remove any threads from the top-level map that no longer hold data - AZStd::erase_if( - m_savedData, - [](const auto& singleThreadDataEntry) - { - return singleThreadDataEntry.second.empty(); - }); - } - - 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; - } - - float wy = ImGui::GetWindowPos().y - ImGui::GetScrollY(); - - ImDrawList* drawList = ImGui::GetWindowDrawList(); - - const float startPixel = ConvertTickToPixelSpace(block.m_startTick, m_viewportStartTick, m_viewportEndTick); - const float endPixel = ConvertTickToPixelSpace(block.m_endTick, m_viewportStartTick, m_viewportEndTick); - - if (endPixel - startPixel < 0.5f) - { - return; - } - - const ImVec2 startPoint = { startPixel, wy + targetRow * RowHeight + 1}; - const ImVec2 endPoint = { endPixel, wy + (targetRow + 1) * RowHeight }; - - const ImU32 blockColor = GetBlockColor(block); - - drawList->AddRectFilled(startPoint, endPoint, blockColor, 0); - drawList->AddLine(startPoint, { endPixel, startPoint.y }, IM_COL32_BLACK, 0.5f); - drawList->AddLine({ startPixel, endPoint.y }, endPoint, IM_COL32_BLACK, 0.5f); - - // Draw the region name if possible - // If the block's current width is too small, we skip drawing the label. - 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; - - 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 }; - - // NOTE: RenderText calls do not automatically account for the global scale (which is modified at high DPI) - // so we must adjust for the scale manually. - const float scaleFactor = ImGui::GetIO().FontGlobalScale; - const float fontSize = ImGui::GetFont()->FontSize * scaleFactor; - - ImGui::GetFont()->RenderText(drawList, fontSize, startPoint, IM_COL32_WHITE, clipRect, label.c_str(), 0); - } - else // We have enough space to draw the entire label, draw and center text. - { - const float remainingWidth = regionPixelWidth - textWidth; - const float offset = remainingWidth * .5f; - - drawList->AddText({ startPoint.x + offset, startPoint.y }, IM_COL32_WHITE, label.c_str()); - } - } - - // Tooltip and block highlighting - if (ImGui::IsMouseHoveringRect(startPoint, endPoint) && ImGui::IsWindowHovered()) - { - // 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(); - } - } - - 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 - { - return ImGui::GetColorU32(iter->second); - } - - // 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 }); - - auto [wx, wy] = ImGui::GetWindowPos(); - wy -= ImGui::GetScrollY(); - const float windowWidth = ImGui::GetWindowWidth(); - const float boundaryY = wy + (baseRow + maxDepth + 1) * RowHeight; - - ImGui::GetWindowDrawList()->AddLine({ wx, boundaryY }, { wx + windowWidth, boundaryY }, red, 1.0f); - } - - 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()->AddText({ wx + 10, wy + baseRow * RowHeight}, IM_COL32_WHITE, threadIdText.c_str()); - } - - inline void ImGuiCpuProfiler::DrawFrameBoundaries() - { - ImDrawList* drawList = ImGui::GetWindowDrawList(); - - const float wy = ImGui::GetWindowPos().y; - const float windowHeight = ImGui::GetWindowHeight(); - const ImU32 red = ImGui::GetColorU32({ 1, 0, 0, 1 }); - - // 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() - { - // 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) - { - break; - } - - const float lastFrameBoundaryPixel = ConvertTickToPixelSpace(lastFrameBoundaryTick, m_viewportStartTick, m_viewportEndTick); - const float nextFrameBoundaryPixel = ConvertTickToPixelSpace(nextFrameBoundaryTick, m_viewportStartTick, m_viewportEndTick); - - const AZStd::string label = - AZStd::string::format("%.2f ms", CpuProfilerImGuiHelper::TicksToMs(nextFrameBoundaryTick - lastFrameBoundaryTick)); - const float labelWidth = ImGui::CalcTextSize(label.c_str()).x; - - // 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); - } - - // Left bound - drawList->AddLine( - { lastFrameBoundaryPixel, wy }, - { lastFrameBoundaryPixel, wy + ImGui::GetWindowHeight() }, - IM_COL32_WHITE); - - // Right bound - drawList->AddLine( - { nextFrameBoundaryPixel, wy }, - { nextFrameBoundaryPixel, wy + ImGui::GetWindowHeight() }, - 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 - 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; - } - - // 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; - - const float framePixelPos = ConvertTickToPixelSpace(frameEndTick, leftHistogramBound, rightHistogramBound); - const float frameTimeMs = CpuProfilerImGuiHelper::TicksToMs(frameEndTick - lastFrameEndTick); - - const ImVec2 lineBottom = { framePixelPos, ImGui::GetWindowHeight() + wy }; - const ImVec2 lineTop = { framePixelPos, ImGui::GetWindowHeight() + wy - frameTimeMs }; - - 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 - } - - drawList->AddLine(lineBottom, lineTop, lineColor, 3.0); - - lastFrameEndTick = frameEndTick; - } - - // Handle input - ImGui::InvisibleButton("HistogramInputCapture", { ImGui::GetWindowWidth(), ImGui::GetWindowHeight() }); - ImGuiIO& io = ImGui::GetIO(); - if (ImGui::IsItemClicked(ImGuiMouseButton_Left)) - { - 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 - { - 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) - { - SystemTickBus::Handler::BusDisconnect(); - } - else - { - m_frameEndTicks.push_back(AZStd::GetTimeNowTicks()); - - for (auto& [groupName, regionMap] : m_groupRegionMap) - { - for (auto& [regionName, row] : regionMap) - { - row.ResetPerFrameStatistics(); - } - } - } - } - - // ---- 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::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) - { - threadString.append(AZStd::string::format("Thread: %zu\n", threadId)); - } - return threadString; - } - } // namespace Render -} // namespace AZ diff --git a/Gems/Atom/Utils/Code/atom_utils_files.cmake b/Gems/Atom/Utils/Code/atom_utils_files.cmake index dd4654b738..c11c2e294d 100644 --- a/Gems/Atom/Utils/Code/atom_utils_files.cmake +++ b/Gems/Atom/Utils/Code/atom_utils_files.cmake @@ -9,8 +9,6 @@ set(FILES Include/Atom/Utils/DdsFile.h Include/Atom/Utils/ImageComparison.h - Include/Atom/Utils/ImGuiCpuProfiler.h - Include/Atom/Utils/ImGuiCpuProfiler.inl Include/Atom/Utils/ImGuiCullingDebug.h Include/Atom/Utils/ImGuiCullingDebug.inl Include/Atom/Utils/ImGuiGpuProfiler.h diff --git a/Gems/AtomLyIntegration/AtomImGuiTools/Code/Source/AtomImGuiToolsSystemComponent.cpp b/Gems/AtomLyIntegration/AtomImGuiTools/Code/Source/AtomImGuiToolsSystemComponent.cpp index b369ab2ee2..3f64e33e22 100644 --- a/Gems/AtomLyIntegration/AtomImGuiTools/Code/Source/AtomImGuiToolsSystemComponent.cpp +++ b/Gems/AtomLyIntegration/AtomImGuiTools/Code/Source/AtomImGuiToolsSystemComponent.cpp @@ -84,10 +84,6 @@ namespace AtomImGuiTools { m_imguiGpuProfiler.Draw(m_showGpuProfiler, AZ::RPI::PassSystemInterface::Get()->GetRootPass().get()); } - if (m_showCpuProfiler) - { - m_imguiCpuProfiler.Draw(m_showCpuProfiler); - } if (m_showTransientAttachmentProfiler) { auto* transientStats = AZ::RHI::RHISystemInterface::Get()->GetTransientAttachmentStatistics(); @@ -108,12 +104,6 @@ namespace AtomImGuiTools { ImGui::MenuItem("Pass Viewer", "", &m_showPassTree); ImGui::MenuItem("Gpu Profiler", "", &m_showGpuProfiler); - if (ImGui::MenuItem("Cpu Profiler", "", &m_showCpuProfiler)) - { - AZ::RHI::RHISystemInterface::Get()->ModifyFrameSchedulerStatisticsFlags( - AZ::RHI::FrameSchedulerStatisticsFlags::GatherCpuTimingStatistics, m_showCpuProfiler); - AZ::RHI::CpuProfiler::Get()->SetProfilerEnabled(m_showCpuProfiler); - } if (ImGui::MenuItem("Transient Attachment Profiler", "", &m_showTransientAttachmentProfiler)) { AZ::RHI::RHISystemInterface::Get()->ModifyFrameSchedulerStatisticsFlags( diff --git a/Gems/AtomLyIntegration/AtomImGuiTools/Code/Source/AtomImGuiToolsSystemComponent.h b/Gems/AtomLyIntegration/AtomImGuiTools/Code/Source/AtomImGuiToolsSystemComponent.h index 890322bda7..3df3fc7fe5 100644 --- a/Gems/AtomLyIntegration/AtomImGuiTools/Code/Source/AtomImGuiToolsSystemComponent.h +++ b/Gems/AtomLyIntegration/AtomImGuiTools/Code/Source/AtomImGuiToolsSystemComponent.h @@ -15,7 +15,6 @@ #if defined(IMGUI_ENABLED) #include #include -#include #include #include #include @@ -63,9 +62,6 @@ namespace AtomImGuiTools AZ::Render::ImGuiGpuProfiler m_imguiGpuProfiler; bool m_showGpuProfiler = false; - AZ::Render::ImGuiCpuProfiler m_imguiCpuProfiler; - bool m_showCpuProfiler = false; - AZ::Render::ImGuiTransientAttachmentProfiler m_imguiTransientAttachmentProfiler; bool m_showTransientAttachmentProfiler = false; diff --git a/Gems/AtomTressFX/Code/Rendering/HairFeatureProcessor.cpp b/Gems/AtomTressFX/Code/Rendering/HairFeatureProcessor.cpp index 7c8ce74f8e..7a317dc09c 100644 --- a/Gems/AtomTressFX/Code/Rendering/HairFeatureProcessor.cpp +++ b/Gems/AtomTressFX/Code/Rendering/HairFeatureProcessor.cpp @@ -14,7 +14,6 @@ #include #include #include -#include #include #include diff --git a/Gems/Profiler/CMakeLists.txt b/Gems/Profiler/CMakeLists.txt new file mode 100644 index 0000000000..2bb380fae3 --- /dev/null +++ b/Gems/Profiler/CMakeLists.txt @@ -0,0 +1,9 @@ +# +# Copyright (c) Contributors to the Open 3D Engine Project. +# For complete copyright and license terms please see the LICENSE at the root of this distribution. +# +# SPDX-License-Identifier: Apache-2.0 OR MIT +# +# + +add_subdirectory(Code) diff --git a/Gems/Profiler/Code/CMakeLists.txt b/Gems/Profiler/Code/CMakeLists.txt new file mode 100644 index 0000000000..6e4adca67c --- /dev/null +++ b/Gems/Profiler/Code/CMakeLists.txt @@ -0,0 +1,64 @@ +# +# Copyright (c) Contributors to the Open 3D Engine Project. +# For complete copyright and license terms please see the LICENSE at the root of this distribution. +# +# SPDX-License-Identifier: Apache-2.0 OR MIT +# +# + +# data portion +ly_add_target( + NAME Profiler.Static STATIC + NAMESPACE Gem + FILES_CMAKE + profiler_files.cmake + INCLUDE_DIRECTORIES + PUBLIC + Include + PRIVATE + Source + BUILD_DEPENDENCIES + PUBLIC + AZ::AzCore + AZ::AzFramework +) + +ly_add_target( + NAME Profiler ${PAL_TRAIT_MONOLITHIC_DRIVEN_MODULE_TYPE} + NAMESPACE Gem + FILES_CMAKE + profiler_shared_files.cmake + INCLUDE_DIRECTORIES + PUBLIC + Include + PRIVATE + Source + BUILD_DEPENDENCIES + PRIVATE + Gem::Profiler.Static +) + +ly_create_alias(NAME Profiler.Servers NAMESPACE Gem TARGETS Gem::Profiler) +ly_create_alias(NAME Profiler.Builders NAMESPACE Gem TARGETS Gem::Profiler) + +# visualization portion +ly_add_target( + NAME ProfilerImGui ${PAL_TRAIT_MONOLITHIC_DRIVEN_MODULE_TYPE} + NAMESPACE Gem + FILES_CMAKE + profiler_imgui_shared_files.cmake + INCLUDE_DIRECTORIES + PUBLIC + Include + PRIVATE + Source + BUILD_DEPENDENCIES + PRIVATE + Gem::Profiler.Static + Gem::ImGui.imguilib + RUNTIME_DEPENDENCIES + Gem::ImGui.imguilib +) + +ly_create_alias(NAME Profiler.Clients NAMESPACE Gem TARGETS Gem::ProfilerImGui) +ly_create_alias(NAME Profiler.Tools NAMESPACE Gem TARGETS Gem::ProfilerImGui) diff --git a/Gems/Profiler/Code/Include/Profiler/ProfilerBus.h b/Gems/Profiler/Code/Include/Profiler/ProfilerBus.h new file mode 100644 index 0000000000..22352185d3 --- /dev/null +++ b/Gems/Profiler/Code/Include/Profiler/ProfilerBus.h @@ -0,0 +1,59 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ +#pragma once + +#include +#include +#include + +namespace Profiler +{ + class ProfilerRequests + { + public: + AZ_RTTI(ProfilerRequests, "{3757c4e5-1941-457c-85ae-16305e17a4c6}"); + virtual ~ProfilerRequests() = default; + + //! Enable/Disable the CpuProfiler + virtual void SetProfilerEnabled(bool enabled) = 0; + + //! Dump a single frame of Cpu profiling data + virtual bool CaptureCpuProfilingStatistics(const AZStd::string& outputFilePath) = 0; + + //! Start a multiframe capture of CPU profiling data. + virtual bool BeginContinuousCpuProfilingCapture() = 0; + + //! End and dump an in-progress continuous capture. + virtual bool EndContinuousCpuProfilingCapture(const AZStd::string& outputFilePath) = 0; + }; + + class ProfilerBusTraits + : public AZ::EBusTraits + { + public: + // EBusTraits overrides + static constexpr AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Single; + static constexpr AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::Single; + }; + + class ProfilerNotifications + : public AZ::EBusTraits + { + public: + virtual ~ProfilerNotifications() = default; + + //! Notify when the current CpuProfilingStatistics capture is finished + //! @param result Set to true if it's finished successfully + //! @param info The output file path or error information which depends on the return. + virtual void OnCaptureCpuProfilingStatisticsFinished(bool result, const AZStd::string& info) = 0; + }; + + using ProfilerInterface = AZ::Interface; + using ProfilerRequestBus = AZ::EBus; + using ProfilerNotificationBus = AZ::EBus; +} // namespace Profiler diff --git a/Gems/Profiler/Code/Include/Profiler/ProfilerImGuiBus.h b/Gems/Profiler/Code/Include/Profiler/ProfilerImGuiBus.h new file mode 100644 index 0000000000..b6a69ae7b5 --- /dev/null +++ b/Gems/Profiler/Code/Include/Profiler/ProfilerImGuiBus.h @@ -0,0 +1,27 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ +#pragma once + +#include +#include + +namespace Profiler +{ + class ProfilerImGuiRequests + { + public: + AZ_RTTI(ProfilerImGuiRequests, "{E0443400-D108-4D3F-8FF5-4F076FCF6D13}"); + virtual ~ProfilerImGuiRequests() = default; + + // special request to render the CPU profiler window in a non-standard way + // e.g not through ImGuiUpdateListenerBus::OnImGuiUpdate + virtual void ShowCpuProfilerWindow(bool& keepDrawing) = 0; + }; + + using ProfilerImGuiInterface = AZ::Interface; +} // namespace Profiler diff --git a/Gems/Profiler/Code/Source/CpuProfiler.h b/Gems/Profiler/Code/Source/CpuProfiler.h new file mode 100644 index 0000000000..23130efa63 --- /dev/null +++ b/Gems/Profiler/Code/Source/CpuProfiler.h @@ -0,0 +1,83 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace Profiler +{ + //! 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 + { + GroupRegionName() = delete; + GroupRegionName(const char* const group, const char* const region); + + 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; + }; + + CachedTimeRegion() = default; + explicit CachedTimeRegion(const GroupRegionName& groupRegionName); + CachedTimeRegion(const GroupRegionName& groupRegionName, uint16_t stackDepth, uint64_t startTick, uint64_t endTick); + + GroupRegionName m_groupRegionName{nullptr, nullptr}; + + uint16_t m_stackDepth = 0u; + AZStd::sys_time_t m_startTick = 0; + AZStd::sys_time_t m_endTick = 0; + }; + + //! Interface class of the CpuProfiler + class CpuProfiler + { + public: + using ThreadTimeRegionMap = AZStd::unordered_map>; + using TimeRegionMap = AZStd::unordered_map; + + AZ_RTTI(CpuProfiler, "{127C1D0B-BE05-4E18-A8F6-24F3EED2ECA6}"); + + CpuProfiler() = default; + virtual ~CpuProfiler() = default; + + AZ_DISABLE_COPY_MOVE(CpuProfiler); + + static CpuProfiler* Get(); + + //! Get the last frame's TimeRegionMap + virtual const TimeRegionMap& GetTimeRegionMap() const = 0; + + //! Begin a continuous capture. Blocks the profiler from being toggled off until EndContinuousCapture is called. + [[nodiscard]] virtual bool BeginContinuousCapture() = 0; + + //! Flush the CPU Profiler's saved data into the passed ring buffer . + [[nodiscard]] virtual bool EndContinuousCapture(AZStd::ring_buffer& flushTarget) = 0; + + virtual bool IsContinuousCaptureInProgress() const = 0; + + //! Enable/Disable the CpuProfiler + virtual void SetProfilerEnabled(bool enabled) = 0; + + virtual bool IsProfilerEnabled() const = 0 ; + }; +} // namespace Profiler diff --git a/Gems/Profiler/Code/Source/CpuProfilerImpl.cpp b/Gems/Profiler/Code/Source/CpuProfilerImpl.cpp new file mode 100644 index 0000000000..c88afdecd0 --- /dev/null +++ b/Gems/Profiler/Code/Source/CpuProfilerImpl.cpp @@ -0,0 +1,437 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#include + +#include +#include +#include +#include +#include + +namespace Profiler +{ + thread_local CpuTimingLocalStorage* CpuProfilerImpl::ms_threadLocalStorage = nullptr; + + // --- CpuProfiler --- + + CpuProfiler* CpuProfiler::Get() + { + return AZ::Interface::Get(); + } + + // --- CachedTimeRegion --- + + 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; + } + + // --- GroupRegionName --- + + 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; + } + + bool CachedTimeRegion::GroupRegionName::operator==(const GroupRegionName& other) const + { + return (m_groupName == other.m_groupName) && (m_regionName == other.m_regionName); + } + + + // --- CpuProfilerImpl --- + + void CpuProfilerImpl::Init() + { + AZ::Interface::Register(this); + AZ::Interface::Register(this); + m_initialized = true; + AZ::SystemTickBus::Handler::BusConnect(); + m_continuousCaptureData.set_capacity(10); + } + + void CpuProfilerImpl::Shutdown() + { + if (!m_initialized) + { + return; + } + // When this call is made, no more thread profiling calls can be performed anymore + AZ::Interface::Unregister(this); + AZ::Interface::Unregister(this); + + // Wait for the remaining threads that might still be processing its profiling calls + AZStd::unique_lock shutdownLock(m_shutdownMutex); + + m_enabled = false; + + // Cleanup all TLS + m_registeredThreads.clear(); + m_timeRegionMap.clear(); + m_initialized = false; + m_continuousCaptureInProgress.store(false); + m_continuousCaptureData.clear(); + AZ::SystemTickBus::Handler::BusDisconnect(); + } + + 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()) + { + if (m_enabled) + { + // 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(); + } + } + + 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()) + { + // guard against enabling mid-marker + if (m_enabled && ms_threadLocalStorage != nullptr) + { + ms_threadLocalStorage->RegionStackPopBack(); + } + + m_shutdownMutex.unlock_shared(); + } + } + + const CpuProfiler::TimeRegionMap& CpuProfilerImpl::GetTimeRegionMap() const + { + return m_timeRegionMap; + } + + bool CpuProfilerImpl::BeginContinuousCapture() + { + 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"); + return false; + } + + bool CpuProfilerImpl::EndContinuousCapture(AZStd::ring_buffer& flushTarget) + { + 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; + } + + 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; + } + + // 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; + } + } + + bool CpuProfilerImpl::IsProfilerEnabled() const + { + return m_enabled; + } + + void CpuProfilerImpl::OnSystemTick() + { + if (!m_enabled) + { + return; + } + + if (m_continuousCaptureInProgress.load() && m_continuousCaptureEndingMutex.try_lock()) + { + 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(); + } + + 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); + } + + // 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 AZStd::intrusive_ptr& thread) + { + return thread->m_deleteFlag.load(); + }); + + // Update our saved time regions to the last frame's collected data + m_timeRegionMap = AZStd::move(newMap); + } + + void CpuProfilerImpl::RegisterThreadStorage() + { + AZStd::unique_lock lock(m_threadRegisterMutex); + if (!ms_threadLocalStorage) + { + ms_threadLocalStorage = aznew CpuTimingLocalStorage(); + m_registeredThreads.emplace_back(ms_threadLocalStorage); + } + } + + // --- CpuTimingLocalStorage --- + + CpuTimingLocalStorage::CpuTimingLocalStorage() + { + m_executingThreadId = AZStd::this_thread::get_id(); + } + + CpuTimingLocalStorage::~CpuTimingLocalStorage() + { + m_deleteFlag = true; + } + + void CpuTimingLocalStorage::RegionStackPushBack(CachedTimeRegion& timeRegion) + { + // If it was (re)enabled, clear the lists first + if (m_clearContainers) + { + m_clearContainers = false; + + m_stackLevel = 0; + m_cachedTimeRegionMap.clear(); + m_timeRegionStack.clear(); + m_cachedTimeRegions.clear(); + } + + timeRegion.m_stackDepth = aznumeric_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); + + // 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(); + } + + 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()) + { + return; + } + + // 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(); + + // Set the ending time + back.m_endTick = endRegionTime; + + // Decrement the stack + m_stackLevel--; + + // 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) + { + 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); + + // 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) + { + m_hitSizeLimitMap.insert_or_assign(AZStd::move(regionName), true); + } + } + + // Clear the cached regions + m_cachedTimeRegions.clear(); + } + } + + 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()) + { + // 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(); + } + } + + // --- CpuProfilingStatisticsSerializer --- + + CpuProfilingStatisticsSerializer::CpuProfilingStatisticsSerializer(const AZStd::ring_buffer& continuousData) + { + // Create serializable entries + for (const auto& timeRegionMap : continuousData) + { + for (const auto& [threadId, regionMap] : timeRegionMap) + { + for (const auto& [regionName, regionVec] : regionMap) + { + for (const auto& region : regionVec) + { + m_cpuProfilingStatisticsSerializerEntries.emplace_back(region, threadId); + } + } + } + } + } + + void CpuProfilingStatisticsSerializer::Reflect(AZ::ReflectContext* context) + { + if (auto* serializeContext = azrtti_cast(context)) + { + serializeContext->Class() + ->Version(1) + ->Field("cpuProfilingStatisticsSerializerEntries", &CpuProfilingStatisticsSerializer::m_cpuProfilingStatisticsSerializerEntries); + } + + CpuProfilingStatisticsSerializerEntry::Reflect(context); + } + + // --- CpuProfilingStatisticsSerializerEntry --- + + CpuProfilingStatisticsSerializer::CpuProfilingStatisticsSerializerEntry::CpuProfilingStatisticsSerializerEntry( + const 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)) + { + 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 Profiler diff --git a/Gems/Profiler/Code/Source/CpuProfilerImpl.h b/Gems/Profiler/Code/Source/CpuProfilerImpl.h new file mode 100644 index 0000000000..1046b72cff --- /dev/null +++ b/Gems/Profiler/Code/Source/CpuProfilerImpl.h @@ -0,0 +1,183 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ +#pragma once + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Profiler +{ + //! 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; + + public: + AZ_CLASS_ALLOCATOR(CpuTimingLocalStorage, AZ::OSAllocator, 0); + + CpuTimingLocalStorage(); + ~CpuTimingLocalStorage(); + + 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); + + // 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); + + // 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; + + // 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; + + // 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; + + // 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; + }; + + //! 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 AZ::SystemTickBus::Handler + { + friend class CpuTimingLocalStorage; + + public: + AZ_TYPE_INFO(CpuProfilerImpl, "{10E9D394-FC83-4B45-B2B8-807C6BF07BF0}"); + AZ_CLASS_ALLOCATOR(CpuProfilerImpl, AZ::OSAllocator, 0); + + CpuProfilerImpl() = default; + ~CpuProfilerImpl() = default; + + //! Registers the CpuProfilerImpl instance to the interface + void Init(); + //! Unregisters the CpuProfilerImpl instance from the interface + void Shutdown(); + + // AZ::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; + + //! 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. + + // 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; + + // 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; + + // 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; + + bool m_initialized = false; + + AZStd::mutex m_continuousCaptureEndingMutex; + + 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; + }; + + // Intermediate class to serialize Cpu TimedRegion data. + class CpuProfilingStatisticsSerializer + { + public: + class CpuProfilingStatisticsSerializerEntry + { + public: + AZ_TYPE_INFO(CpuProfilingStatisticsSerializer::CpuProfilingStatisticsSerializerEntry, "{26B78F65-EB96-46E2-BE7E-A1233880B225}"); + static void Reflect(AZ::ReflectContext* context); + + CpuProfilingStatisticsSerializerEntry() = default; + CpuProfilingStatisticsSerializerEntry(const CachedTimeRegion& cachedTimeRegion, AZStd::thread_id threadId); + + AZ::Name m_groupName; + AZ::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}"); + 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 new file mode 100644 index 0000000000..3f364f99e0 --- /dev/null +++ b/Gems/Profiler/Code/Source/ImGuiCpuProfiler.cpp @@ -0,0 +1,1150 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#if defined(IMGUI_ENABLED) + +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Profiler +{ + static constexpr const char* defaultSaveLocation = "@user@/Profiler"; + + namespace CpuProfilerImGuiHelper + { + 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; + } + + float TicksToMs(AZStd::sys_time_t ticks) + { + return TicksToMs(static_cast(ticks)); + } + + using DeserializedCpuData = AZStd::vector; + + AZ::Outcome LoadSavedCpuProfilingStatistics(const char* capturePath) + { + auto* base = AZ::IO::FileIOBase::GetInstance(); + + char resolvedPath[AZ::IO::MaxPathLength]; + if (!base->ResolvePath(capturePath, resolvedPath, AZ::IO::MaxPathLength)) + { + return AZ::Failure(AZStd::string::format("Could not resolve the path to file %s, is the path correct?", resolvedPath)); + } + + AZ::u64 captureSizeBytes; + const AZ::IO::Result fileSizeResult = base->Size(resolvedPath, captureSizeBytes); + if (!fileSizeResult) + { + return AZ::Failure(AZStd::string::format("Could not read the size of file %s, is the path correct?", resolvedPath)); + } + + // NOTE: this uses raw file pointers over the abstractions and utility functions provided by AZ::JsonSerializationUtils because + // saved profiling captures can be upwards of 400 MB. This necessitates a buffered approach to avoid allocating huge chunks of memory. + FILE* fp = nullptr; + azfopen(&fp, resolvedPath, "rb"); + if (!fp) + { + return AZ::Failure(AZStd::string::format("Could not fopen file %s, is the path correct?\n", resolvedPath)); + } + + constexpr AZStd::size_t MaxBufSize = 65536; + const AZStd::size_t bufSize = AZStd::min(MaxBufSize, aznumeric_cast(captureSizeBytes)); + char* buf = reinterpret_cast(azmalloc(bufSize)); + + rapidjson::Document document; + rapidjson::FileReadStream inputStream(fp, buf, bufSize); + document.ParseStream(inputStream); + + azfree(buf); + fclose(fp); + + if (document.HasParseError()) + { + const auto pe = document.GetParseError(); + return AZ::Failure(AZStd::string::format( + "Rapidjson could not parse the document with ParseErrorCode %u. See 3rdParty/rapidjson/error.h for definitions.\n", pe)); + } + + if (!document.IsObject() || !document.HasMember("ClassData")) + { + return AZ::Failure(AZStd::string::format( + "Error in loading saved capture: top-level object does not have a ClassData field. Did the serialization format change recently?\n")); + } + + AZ_TracePrintf("JsonUtils", "Successfully loaded JSON into memory.\n"); + + const auto& root = document["ClassData"]; + CpuProfilingStatisticsSerializer serializer; + const AZ::JsonSerializationResult::ResultCode deserializationResult = AZ::JsonSerialization::Load(serializer, root); + if (deserializationResult.GetProcessing() == AZ::JsonSerializationResult::Processing::Halted + || serializer.m_cpuProfilingStatisticsSerializerEntries.empty()) + { + return AZ::Failure(AZStd::string::format("Error in deserializing document: %s\n", deserializationResult.ToString(capturePath).c_str())); + } + + AZ_TracePrintf("JsonUtils", "Successfully loaded CPU profiling data with %zu profiling entries.\n", + serializer.m_cpuProfilingStatisticsSerializerEntries.size()); + + return AZ::Success(AZStd::move(serializer.m_cpuProfilingStatisticsSerializerEntries)); + } + } // namespace CpuProfilerImGuiHelper + + 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(); + + // Only listen to system ticks when the profiler is active + if (!AZ::SystemTickBus::Handler::BusIsConnected()) + { + AZ::SystemTickBus::Handler::BusConnect(); + } + } + + if (m_enableVisualizer) + { + DrawVisualizer(); + } + else + { + DrawStatisticsView(); + } + + if (m_showFilePicker) + { + DrawFilePicker(); + } + } + ImGui::End(); + + if (m_captureToFile) + { + AZStd::string timeString; + AZStd::to_string(timeString, AZStd::GetTimeNowSecond()); + + const AZStd::string frameDataFilePath = AZStd::string::format("%s/cpu_single_%s.json", defaultSaveLocation, timeString.c_str()); + + char resolvedPath[AZ::IO::MaxPathLength]; + AZ::IO::FileIOBase::GetInstance()->ResolvePath(frameDataFilePath.c_str(), resolvedPath, AZ::IO::MaxPathLength); + m_lastCapturedFilePath = resolvedPath; + + ProfilerRequestBus::Broadcast(&ProfilerRequestBus::Events::CaptureCpuProfilingStatistics, frameDataFilePath); + } + m_captureToFile = false; + + // Toggle if the bool isn't the same as the cached value + if (cachedShowCpuProfiler != keepDrawing) + { + CpuProfiler::Get()->SetProfilerEnabled(keepDrawing); + } + } + + void ImGuiCpuProfiler::DrawCommonHeader() + { + if (!m_lastCapturedFilePath.empty()) + { + ImGui::Text("Saved: %s", m_lastCapturedFilePath.c_str()); + } + + if (ImGui::Button(m_enableVisualizer ? "Swap to statistics" : "Swap to visualizer")) + { + m_enableVisualizer = !m_enableVisualizer; + } + + ImGui::SameLine(); + m_paused = !CpuProfiler::Get()->IsProfilerEnabled(); + if (ImGui::Button(m_paused ? "Resume" : "Pause")) + { + m_paused = !m_paused; + CpuProfiler::Get()->SetProfilerEnabled(!m_paused); + } + + ImGui::SameLine(); + if (ImGui::Button("Capture")) + { + m_captureToFile = true; + } + + ImGui::SameLine(); + bool isInProgress = CpuProfiler::Get()->IsContinuousCaptureInProgress(); + if (ImGui::Button(isInProgress ? "End" : "Begin")) + { + if (isInProgress) + { + AZStd::string timeString; + AZStd::to_string(timeString, AZStd::GetTimeNowSecond()); + + const AZStd::string frameDataFilePath = AZStd::string::format("%s/cpu_multi_%s.json", defaultSaveLocation, timeString.c_str()); + + char resolvedPath[AZ::IO::MaxPathLength]; + AZ::IO::FileIOBase::GetInstance()->ResolvePath(frameDataFilePath.c_str(), resolvedPath, AZ::IO::MaxPathLength); + m_lastCapturedFilePath = resolvedPath; + + ProfilerRequestBus::Broadcast(&ProfilerRequestBus::Events::EndContinuousCpuProfilingCapture, frameDataFilePath); + + m_paused = true; + } + else + { + ProfilerRequestBus::Broadcast(&ProfilerRequestBus::Events::BeginContinuousCpuProfilingCapture); + } + } + + ImGui::SameLine(); + if (ImGui::Button("Load file")) + { + m_showFilePicker = true; + + // Only update the cached file list when opened so that we aren't making IO calls on every frame. + m_cachedCapturePaths.clear(); + + auto* base = AZ::IO::FileIOBase::GetInstance(); + base->FindFiles(defaultSaveLocation, "*.json", + [&paths = m_cachedCapturePaths](const char* path) -> bool + { + auto foundPath = AZ::IO::Path(path); + paths.push_back(foundPath); + return true; + }); + + // Sort by decreasing modification time (most recent at the top) + AZStd::sort(m_cachedCapturePaths.begin(), m_cachedCapturePaths.end(), + [&base](const AZ::IO::Path& lhs, const AZ::IO::Path& rhs) + { + return base->ModificationTime(lhs.c_str()) > base->ModificationTime(rhs.c_str()); + }); + } + } + + 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) + { + SortTable(sortSpecs); + } + + // 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(); + + ImGui::Text("%s", statistics->m_regionName.c_str()); + ImGui::TableNextColumn(); + + ImGui::Text("%.2f", CpuProfilerImGuiHelper::TicksToMs(statistics->m_runningAverageTicks)); + ImGui::TableNextColumn(); + + ImGui::Text("%.2f", CpuProfilerImGuiHelper::TicksToMs(statistics->m_maxTicks)); + ImGui::TableNextColumn(); + + ImGui::Text("%llu", statistics->m_invocationsLastFrame); + ImGui::TableNextColumn(); + + ImGui::Text("%.2f", CpuProfilerImGuiHelper::TicksToMs(statistics->m_lastFrameTotalTicks)); + const ImVec2 botRightBound = ImGui::GetItemRectMax(); + ImGui::TableNextColumn(); + + // 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(); + } + } + } + ImGui::EndTable(); + } + + void ImGuiCpuProfiler::SortTable(ImGuiTableSortSpecs* sortSpecs) + { + const bool ascending = sortSpecs->Specs->SortDirection == ImGuiSortDirection_Ascending; + const ImS16 columnToSort = sortSpecs->Specs->ColumnIndex; + + 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; + } + + void ImGuiCpuProfiler::DrawStatisticsView() + { + DrawCommonHeader(); + + const auto ShowRow = [](const char* regionLabel, double duration) + { + ImGui::Text("%s", regionLabel); + ImGui::NextColumn(); + + ImGui::Text("%.2f ms", CpuProfilerImGuiHelper::TicksToMs(duration)); + ImGui::NextColumn(); + }; + + 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); + + 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")) + { + m_tableData.clear(); + m_groupRegionMap.clear(); + } + + DrawTable(); + } + } + + void ImGuiCpuProfiler::DrawFilePicker() + { + ImGui::SetNextWindowSize({ 500, 200 }, ImGuiCond_Once); + if (ImGui::Begin("File Picker", &m_showFilePicker)) + { + if (ImGui::Button("Load selected")) + { + LoadFile(); + } + + auto getter = [](void* vectorPointer, int idx, const char** out_text) -> bool + { + const auto& pathVec = *static_cast*>(vectorPointer); + if (idx < 0 || idx >= pathVec.size()) + { + return false; + } + *out_text = pathVec[idx].c_str(); + return true; + }; + + ImGui::SetNextItemWidth(ImGui::GetWindowContentRegionWidth()); + ImGui::ListBox("", &m_currentFileIndex, getter, &m_cachedCapturePaths, aznumeric_cast(m_cachedCapturePaths.size())); + } + ImGui::End(); + } + + void ImGuiCpuProfiler::LoadFile() + { + const AZ::IO::Path& pathToLoad = m_cachedCapturePaths[m_currentFileIndex]; + auto loadResult = CpuProfilerImGuiHelper::LoadSavedCpuProfilingStatistics(pathToLoad.c_str()); + if (!loadResult.IsSuccess()) + { + AZ_TracePrintf("ImGuiCpuProfiler", "%s", loadResult.GetError().c_str()); + return; + } + + AZStd::vector deserializedData = loadResult.TakeValue(); + + // Clear visualizer and statistics view state + m_savedRegionCount = deserializedData.size(); + m_savedData.clear(); + m_paused = true; + + CpuProfiler::Get()->SetProfilerEnabled(false); + m_frameEndTicks.clear(); + + m_tableData.clear(); + m_groupRegionMap.clear(); + + for (const auto& entry : deserializedData) + { + const auto [groupNameItr, wasGroupNameInserted] = m_deserializedStringPool.emplace(entry.m_groupName.GetCStr()); + const auto [regionNameItr, wasRegionNameInserted] = m_deserializedStringPool.emplace(entry.m_regionName.GetCStr()); + const auto [groupRegionNameItr, wasGroupRegionNameInserted] = + m_deserializedGroupRegionNamePool.emplace(groupNameItr->c_str(), regionNameItr->c_str()); + + const 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 AZ::Name frameBoundaryName = AZ::Name("RPISystem: OnSystemTick"); + if (entry.m_regionName == frameBoundaryName) + { + m_frameEndTicks.push_back(entry.m_endTick); + } + + // Update running statistics + if (!m_groupRegionMap[*groupNameItr].contains(*regionNameItr)) + { + m_groupRegionMap[*groupNameItr][*regionNameItr].m_groupName = *groupNameItr; + m_groupRegionMap[*groupNameItr][*regionNameItr].m_regionName = *regionNameItr; + m_tableData.push_back(&m_groupRegionMap[*groupNameItr][*regionNameItr]); + } + m_groupRegionMap[*groupNameItr][*regionNameItr].RecordRegion(newRegion, entry.m_threadId); + } + + // Update viewport bounds with some added UX fudge factor + m_viewportStartTick = deserializedData.back().m_startTick - 1000; + m_viewportEndTick = deserializedData.back().m_endTick + 1000; + + // Invariant: each vector in m_savedData must be sorted so that we can efficiently cull region data. + for (auto& [threadId, singleThreadData] : m_savedData) + { + AZStd::sort(singleThreadData.begin(), singleThreadData.end(), + [](const TimeRegion& lhs, const TimeRegion& rhs) + { + return lhs.m_startTick < rhs.m_startTick; + }); + } + } + + // -- CPU Visualizer -- + void ImGuiCpuProfiler::DrawVisualizer() + { + DrawCommonHeader(); + + // 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::NextColumn(); + + 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::NextColumn(); + + ImGui::TextWrapped( + "Hold the right mouse button to move around. Zoom by scrolling the mouse wheel while holding ."); + } + + ImGui::Columns(1, "FrameTimeColumn", true); + + if (ImGui::BeginChild("FrameTimeHistogram", { 0, 50 }, true, ImGuiWindowFlags_NoScrollbar)) + { + DrawFrameTimeHistogram(); + } + ImGui::EndChild(); + + ImGui::Columns(1, "RulerColumn", true); + + // Ruler + if (ImGui::BeginChild("Ruler", { 0, 30 }, true, ImGuiWindowFlags_NoNavFocus)) + { + DrawRuler(); + } + ImGui::EndChild(); + + + ImGui::Columns(1, "TimelineColumn", true); + + // 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) + { + --nextFrameBoundaryItr; + } + const AZStd::sys_time_t nextFrameBoundary = *nextFrameBoundaryItr; + + // 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()) + { + --startTickItr; + } + + // Main draw loop + AZ::u64 baseRow = 0; + for (const auto& [currentThreadId, singleThreadData] : m_savedData) + { + // 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; + } + + // Draw all of the blocks for a given thread/row + AZ::u64 maxDepth = 0; + while (regionItr != singleThreadData.end()) + { + const TimeRegion& region = *regionItr; + + // Early out if we have drawn all the onscreen regions + if (region.m_startTick > nextFrameBoundary) + { + break; + } + AZ::u64 targetRow = region.m_stackDepth + baseRow; + maxDepth = AZStd::max(aznumeric_cast(region.m_stackDepth), maxDepth); + + DrawBlock(region, targetRow); + + ++regionItr; + } + + // Draw UI details + DrawThreadLabel(baseRow, currentThreadId); + DrawThreadSeparator(baseRow, maxDepth); + + 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()) + { + io.WantCaptureMouse = true; + if (ImGui::IsMouseDragging(ImGuiMouseButton_Right)) // Scrolling + { + 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); + } + } + 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(); + + 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); + + // Avoid zooming too much, start tick should always be less than end tick + if (newStartTick < newEndTick) + { + m_viewportStartTick = newStartTick; + m_viewportEndTick = newEndTick; + } + } + } + } + ImGui::EndChild(); + } + + void ImGuiCpuProfiler::CacheCpuTimingStatistics() + { + using namespace AZ::Statistics; + + m_cpuTimingStatisticsWhenPause.clear(); + if (auto statsProfiler = AZ::Interface::Get(); statsProfiler) + { + 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()); + } + + AZStd::vector statistics; + rhiMetrics.GetStatsManager().GetAllStatistics(statistics); + + for (NamedRunningStatistic* stat : statistics) + { + m_cpuTimingStatisticsWhenPause.push_back({ stat->GetName(), stat->GetMostRecentSample() }); + stat->Reset(); + } + } + } + + 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 CpuProfiler::TimeRegionMap& timeRegionMap = CpuProfiler::Get()->GetTimeRegionMap(); + + 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) + { + 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; + } + + // 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) + { + newVisualizerData.push_back(region); // Copies + + // 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); + } + } + + // 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); + + 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())); + } + } + + 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); + + // Remove old region data for each thread + for (auto& [threadId, savedRegions] : m_savedData) + { + AZStd::size_t sizeBeforeRemove = savedRegions.size(); + + // Early out to avoid the linear erase_if call + if (savedRegions.size() >= 1 && savedRegions.at(0).m_startTick > deleteBeforeTick) + { + continue; + } + + // Use erase_if over plain upper_bound + erase to avoid repeated shifts. erase requires a shift of all elements to the right + // for each element that is erased, while erase_if squashes all removes into a single shift which significantly improves perf. + AZStd::erase_if( + savedRegions, + [deleteBeforeTick](const TimeRegion& region) + { + return region.m_startTick < deleteBeforeTick; + }); + + m_savedRegionCount -= sizeBeforeRemove - savedRegions.size(); + } + + // Remove any threads from the top-level map that no longer hold data + AZStd::erase_if( + m_savedData, + [](const auto& singleThreadDataEntry) + { + return singleThreadDataEntry.second.empty(); + }); + } + + void ImGuiCpuProfiler::DrawBlock(const TimeRegion& block, AZ::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; + } + + float wy = ImGui::GetWindowPos().y - ImGui::GetScrollY(); + + ImDrawList* drawList = ImGui::GetWindowDrawList(); + + const float startPixel = ConvertTickToPixelSpace(block.m_startTick, m_viewportStartTick, m_viewportEndTick); + const float endPixel = ConvertTickToPixelSpace(block.m_endTick, m_viewportStartTick, m_viewportEndTick); + + if (endPixel - startPixel < 0.5f) + { + return; + } + + const ImVec2 startPoint = { startPixel, wy + targetRow * RowHeight + 1}; + const ImVec2 endPoint = { endPixel, wy + (targetRow + 1) * RowHeight }; + + const ImU32 blockColor = GetBlockColor(block); + + drawList->AddRectFilled(startPoint, endPoint, blockColor, 0); + drawList->AddLine(startPoint, { endPixel, startPoint.y }, IM_COL32_BLACK, 0.5f); + drawList->AddLine({ startPixel, endPoint.y }, endPoint, IM_COL32_BLACK, 0.5f); + + // Draw the region name if possible + // If the block's current width is too small, we skip drawing the label. + 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; + + 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 }; + + // NOTE: RenderText calls do not automatically account for the global scale (which is modified at high DPI) + // so we must adjust for the scale manually. + const float scaleFactor = ImGui::GetIO().FontGlobalScale; + const float fontSize = ImGui::GetFont()->FontSize * scaleFactor; + + ImGui::GetFont()->RenderText(drawList, fontSize, startPoint, IM_COL32_WHITE, clipRect, label.c_str(), 0); + } + else // We have enough space to draw the entire label, draw and center text. + { + const float remainingWidth = regionPixelWidth - textWidth; + const float offset = remainingWidth * .5f; + + drawList->AddText({ startPoint.x + offset, startPoint.y }, IM_COL32_WHITE, label.c_str()); + } + } + + // Tooltip and block highlighting + if (ImGui::IsMouseHoveringRect(startPoint, endPoint) && ImGui::IsWindowHovered()) + { + // 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(); + } + } + + 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 + { + return ImGui::GetColorU32(iter->second); + } + + // 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); + } + + void ImGuiCpuProfiler::DrawThreadSeparator(AZ::u64 baseRow, AZ::u64 maxDepth) + { + const ImU32 red = ImGui::GetColorU32({ 1, 0, 0, 1 }); + + auto [wx, wy] = ImGui::GetWindowPos(); + wy -= ImGui::GetScrollY(); + const float windowWidth = ImGui::GetWindowWidth(); + const float boundaryY = wy + (baseRow + maxDepth + 1) * RowHeight; + + ImGui::GetWindowDrawList()->AddLine({ wx, boundaryY }, { wx + windowWidth, boundaryY }, red, 1.0f); + } + + void ImGuiCpuProfiler::DrawThreadLabel(AZ::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()->AddText({ wx + 10, wy + baseRow * RowHeight}, IM_COL32_WHITE, threadIdText.c_str()); + } + + void ImGuiCpuProfiler::DrawFrameBoundaries() + { + ImDrawList* drawList = ImGui::GetWindowDrawList(); + + const float wy = ImGui::GetWindowPos().y; + const float windowHeight = ImGui::GetWindowHeight(); + const ImU32 red = ImGui::GetColorU32({ 1, 0, 0, 1 }); + + // 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; + } + } + + 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()) + { + --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) + { + break; + } + + const float lastFrameBoundaryPixel = ConvertTickToPixelSpace(lastFrameBoundaryTick, m_viewportStartTick, m_viewportEndTick); + const float nextFrameBoundaryPixel = ConvertTickToPixelSpace(nextFrameBoundaryTick, m_viewportStartTick, m_viewportEndTick); + + const AZStd::string label = + AZStd::string::format("%.2f ms", CpuProfilerImGuiHelper::TicksToMs(nextFrameBoundaryTick - lastFrameBoundaryTick)); + const float labelWidth = ImGui::CalcTextSize(label.c_str()).x; + + // 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); + } + + // Left bound + drawList->AddLine( + { lastFrameBoundaryPixel, wy }, + { lastFrameBoundaryPixel, wy + ImGui::GetWindowHeight() }, + IM_COL32_WHITE); + + // Right bound + drawList->AddLine( + { nextFrameBoundaryPixel, wy }, + { nextFrameBoundaryPixel, wy + ImGui::GetWindowHeight() }, + IM_COL32_WHITE); + + lastFrameBoundaryItr = nextFrameBoundaryItr; + ++nextFrameBoundaryItr; + } + } + + 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; + } + + // 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; + + const float framePixelPos = ConvertTickToPixelSpace(frameEndTick, leftHistogramBound, rightHistogramBound); + const float frameTimeMs = CpuProfilerImGuiHelper::TicksToMs(frameEndTick - lastFrameEndTick); + + const ImVec2 lineBottom = { framePixelPos, ImGui::GetWindowHeight() + wy }; + const ImVec2 lineTop = { framePixelPos, ImGui::GetWindowHeight() + wy - frameTimeMs }; + + 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 + } + + drawList->AddLine(lineBottom, lineTop, lineColor, 3.0); + + lastFrameEndTick = frameEndTick; + } + + // Handle input + ImGui::InvisibleButton("HistogramInputCapture", { ImGui::GetWindowWidth(), ImGui::GetWindowHeight() }); + ImGuiIO& io = ImGui::GetIO(); + if (ImGui::IsItemClicked(ImGuiMouseButton_Left)) + { + 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; + } + } + + AZStd::sys_time_t ImGuiCpuProfiler::GetViewportTickWidth() const + { + return m_viewportEndTick - m_viewportStartTick; + } + + 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 + void ImGuiCpuProfiler::OnSystemTick() + { + if (m_paused) + { + AZ::SystemTickBus::Handler::BusDisconnect(); + } + else + { + m_frameEndTicks.push_back(AZStd::GetTimeNowTicks()); + + for (auto& [groupName, regionMap] : m_groupRegionMap) + { + for (auto& [regionName, row] : regionMap) + { + row.ResetPerFrameStatistics(); + } + } + } + } + + // ---- TableRow impl ---- + + void TableRow::RecordRegion(const 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; + } + + void TableRow::ResetPerFrameStatistics() + { + m_invocationsLastFrame = 0; + m_executingThreads.clear(); + m_lastFrameTotalTicks = 0; + m_maxTicks = 0; + } + + AZStd::string TableRow::GetExecutingThreadsLabel() const + { + 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; + } +} // namespace Profiler + +#endif // defined(IMGUI_ENABLED) diff --git a/Gems/Profiler/Code/Source/ImGuiCpuProfiler.h b/Gems/Profiler/Code/Source/ImGuiCpuProfiler.h new file mode 100644 index 0000000000..2c6a3e470a --- /dev/null +++ b/Gems/Profiler/Code/Source/ImGuiCpuProfiler.h @@ -0,0 +1,234 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#pragma once + +#if defined(IMGUI_ENABLED) + +#include + +#include +#include +#include +#include +#include +#include + +#include + +namespace Profiler +{ + //! Stores all the data associated with a row in the table. + struct TableRow + { + template + struct TableRowCompareFunctor + { + TableRowCompareFunctor(T memberPointer, bool isAscending) : m_memberPointer(memberPointer), m_ascending(isAscending){}; + + 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; + }; + + // Update running statistics with new region data + void RecordRegion(const CachedTimeRegion& region, size_t threadId); + + void ResetPerFrameStatistics(); + + // 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; + + // --- Per frame statistics --- + + AZ::u64 m_invocationsLastFrame = 0; + + // 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; + + // Maximum execution time of a region in the last frame. + AZStd::sys_time_t m_maxTicks = 0; + + // --- Aggregate statistics --- + + AZ::u64 m_invocationsTotal = 0; + + // Running average of Mean Time Per Call + AZStd::sys_time_t m_runningAverageTicks = 0; + }; + + //! ImGui widget for examining CPU Profiling instrumentation. + //! Offers both a statistical view (with sorting and searching capability) and a visualizer + //! similar to other profiling tools. + class ImGuiCpuProfiler + : public AZ::SystemTickBus::Handler + { + // Region Name -> statistical view row data + using RegionRowMap = AZStd::map; + // Group Name -> RegionRowMap + using GroupRegionMap = AZStd::map; + + using TimeRegion = CachedTimeRegion; + using GroupRegionName = CachedTimeRegion::GroupRegionName; + + public: + struct CpuTimingEntry + { + const AZStd::string& m_name; + double m_executeDuration; + }; + + ImGuiCpuProfiler() = default; + ~ImGuiCpuProfiler() = default; + + //! Draws the overall CPU profiling window, defaults to the statistical view + void Draw(bool& keepDrawing); + + private: + static constexpr float RowHeight = 35.0f; + static constexpr int DefaultFramesToCollect = 50; + static constexpr float MediumFrameTimeLimit = 16.6f; // 60 fps + static constexpr float HighFrameTimeLimit = 33.3f; // 30 fps + + //! Draws the statistical view of the CPU profiling data. + void DrawStatisticsView(); + + //! Callback invoked when the "Load File" button is pressed in the file picker. + void LoadFile(); + + //! Draws the file picker window. + void DrawFilePicker(); + + //! Draws the CPU profiling visualizer. + void DrawVisualizer(); + + // 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(); + + // Sort the table by a given column, rearranges the pointers in m_tableData. + void SortTable(ImGuiTableSortSpecs* sortSpecs); + + // 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(); + + // 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, AZ::u64 targetRow); + + // Draw horizontal lines between threads in the timeline + void DrawThreadSeparator(AZ::u64 threadBoundary, AZ::u64 maxDepth); + + // Draw the "Thread XXXXX" label onto the viewport + void DrawThreadLabel(AZ::u64 baseRow, size_t threadId); + + // Draw the vertical lines separating frames in the timeline + void DrawFrameBoundaries(); + + // Draw the ruler with frame time labels + void DrawRuler(); + + // 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; + + 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); + + // System tick bus overrides + void OnSystemTick() override; + + // --- Visualizer Members --- + + int m_framesToCollect = DefaultFramesToCollect; + + // Tally of the number of saved profiling events so far + AZ::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; + + // 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; + + // Tracks the frame boundaries + AZStd::vector m_frameEndTicks = { INT64_MIN }; + + // Filter for highlighting regions on the visualizer + ImGuiTextFilter m_visualizerHighlightFilter; + + // --- Tabular view members --- + + // 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 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; + + // 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; + + // Last captured CPU timing statistics + AZStd::vector m_cpuTimingStatisticsWhenPause; + AZStd::sys_time_t m_frameToFrameTime{}; + + AZStd::string m_lastCapturedFilePath; + + bool m_showFilePicker = false; + + // Cached file paths to previous traces on disk, sorted with the most recent trace at the front. + AZStd::vector m_cachedCapturePaths; + + // 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 Profiler + +#endif // defined(IMGUI_ENABLED) diff --git a/Gems/Profiler/Code/Source/ProfilerImGuiModule.cpp b/Gems/Profiler/Code/Source/ProfilerImGuiModule.cpp new file mode 100644 index 0000000000..38cfee2c26 --- /dev/null +++ b/Gems/Profiler/Code/Source/ProfilerImGuiModule.cpp @@ -0,0 +1,49 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#include +#include + +#include +#include + +namespace Profiler +{ + class ProfilerImGuiModule + : public AZ::Module + { + public: + AZ_RTTI(ProfilerImGuiModule, "{5946991E-A96C-4E7A-A9B3-605E3C8EC3CB}", AZ::Module); + AZ_CLASS_ALLOCATOR(ProfilerImGuiModule, AZ::SystemAllocator, 0); + + ProfilerImGuiModule() + { + // Push results of [MyComponent]::CreateDescriptor() into m_descriptors here. + // Add ALL components descriptors associated with this gem to m_descriptors. + // This will associate the AzTypeInfo information for the components with the the SerializeContext, BehaviorContext and EditContext. + // This happens through the [MyComponent]::Reflect() function. + m_descriptors.insert(m_descriptors.end(), { + ProfilerSystemComponent::CreateDescriptor(), + ProfilerImGuiSystemComponent::CreateDescriptor(), + }); + } + + /** + * Add required SystemComponents to the SystemEntity. + */ + AZ::ComponentTypeList GetRequiredSystemComponents() const override + { + return AZ::ComponentTypeList{ + azrtti_typeid(), + azrtti_typeid(), + }; + } + }; +}// namespace Profiler + +AZ_DECLARE_MODULE_CLASS(Gem_Profiler, Profiler::ProfilerImGuiModule) diff --git a/Gems/Profiler/Code/Source/ProfilerImGuiSystemComponent.cpp b/Gems/Profiler/Code/Source/ProfilerImGuiSystemComponent.cpp new file mode 100644 index 0000000000..62d27a1806 --- /dev/null +++ b/Gems/Profiler/Code/Source/ProfilerImGuiSystemComponent.cpp @@ -0,0 +1,116 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#include + +#include +#include +#include + +#include + +namespace Profiler +{ + static constexpr AZ::Crc32 profilerImGuiServiceCrc = AZ_CRC_CE("ProfilerImGuiService"); + + void ProfilerImGuiSystemComponent::Reflect(AZ::ReflectContext* context) + { + if (AZ::SerializeContext* serialize = azrtti_cast(context)) + { + serialize->Class() + ->Version(0); + + if (AZ::EditContext* ec = serialize->GetEditContext()) + { + ec->Class("ProfilerImGui", "Provides in-game visualization of the performance data gathered by the ProfilerSystemComponent") + ->ClassElement(AZ::Edit::ClassElements::EditorData, "") + ->Attribute(AZ::Edit::Attributes::AppearsInAddComponentMenu, AZ_CRC_CE("System")) + ->Attribute(AZ::Edit::Attributes::AutoExpand, true); + } + } + } + + void ProfilerImGuiSystemComponent::GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided) + { + provided.push_back(profilerImGuiServiceCrc); + } + + void ProfilerImGuiSystemComponent::GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible) + { + incompatible.push_back(profilerImGuiServiceCrc); + } + + void ProfilerImGuiSystemComponent::GetRequiredServices([[maybe_unused]] AZ::ComponentDescriptor::DependencyArrayType& required) + { + } + + void ProfilerImGuiSystemComponent::GetDependentServices([[maybe_unused]] AZ::ComponentDescriptor::DependencyArrayType& dependent) + { + } + + ProfilerImGuiSystemComponent::ProfilerImGuiSystemComponent() + { +#if defined(IMGUI_ENABLED) + if (ProfilerImGuiInterface::Get() == nullptr) + { + ProfilerImGuiInterface::Register(this); + } +#endif // defined(IMGUI_ENABLED) + } + + ProfilerImGuiSystemComponent::~ProfilerImGuiSystemComponent() + { +#if defined(IMGUI_ENABLED) + if (ProfilerImGuiInterface::Get() == this) + { + ProfilerImGuiInterface::Unregister(this); + } +#endif // defined(IMGUI_ENABLED) + } + + void ProfilerImGuiSystemComponent::Activate() + { +#if defined(IMGUI_ENABLED) + ImGui::ImGuiUpdateListenerBus::Handler::BusConnect(); +#endif // defined(IMGUI_ENABLED) + } + + void ProfilerImGuiSystemComponent::Deactivate() + { +#if defined(IMGUI_ENABLED) + ImGui::ImGuiUpdateListenerBus::Handler::BusDisconnect(); +#endif // defined(IMGUI_ENABLED) + } + +#if defined(IMGUI_ENABLED) + void ProfilerImGuiSystemComponent::ShowCpuProfilerWindow(bool& keepDrawing) + { + m_imguiCpuProfiler.Draw(keepDrawing); + } + + void ProfilerImGuiSystemComponent::OnImGuiUpdate() + { + if (m_showCpuProfiler) + { + ShowCpuProfilerWindow(m_showCpuProfiler); + } + } + + void ProfilerImGuiSystemComponent::OnImGuiMainMenuUpdate() + { + if (ImGui::BeginMenu("Profiler")) + { + if (ImGui::MenuItem("CPU", "", &m_showCpuProfiler)) + { + CpuProfiler::Get()->SetProfilerEnabled(m_showCpuProfiler); + } + ImGui::EndMenu(); + } + } +#endif // defined(IMGUI_ENABLED) +} // namespace Profiler diff --git a/Gems/Profiler/Code/Source/ProfilerImGuiSystemComponent.h b/Gems/Profiler/Code/Source/ProfilerImGuiSystemComponent.h new file mode 100644 index 0000000000..351197f961 --- /dev/null +++ b/Gems/Profiler/Code/Source/ProfilerImGuiSystemComponent.h @@ -0,0 +1,65 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#pragma once + +#include + +#include + +#include + +#if defined(IMGUI_ENABLED) +#include +#include +#endif // defined(IMGUI_ENABLED) + +namespace Profiler +{ + class ProfilerImGuiSystemComponent + : public AZ::Component +#if defined(IMGUI_ENABLED) + , public ProfilerImGuiRequests + , public ImGui::ImGuiUpdateListenerBus::Handler +#endif // defined(IMGUI_ENABLED) + { + public: + AZ_COMPONENT(ProfilerImGuiSystemComponent, "{E59A8A53-6784-4CCB-A8B5-9F91DA9BF1C5}"); + + static void Reflect(AZ::ReflectContext* context); + + static void GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided); + static void GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible); + static void GetRequiredServices(AZ::ComponentDescriptor::DependencyArrayType& required); + static void GetDependentServices(AZ::ComponentDescriptor::DependencyArrayType& dependent); + + ProfilerImGuiSystemComponent(); + ~ProfilerImGuiSystemComponent(); + + protected: + // AZ::Component interface implementation + void Activate() override; + void Deactivate() override; + +#if defined(IMGUI_ENABLED) + // ProfilerImGuiRequests interface implementation + void ShowCpuProfilerWindow(bool& keepDrawing) override; + + // ImGuiUpdateListenerBus overrides + void OnImGuiUpdate() override; + void OnImGuiMainMenuUpdate() override; +#endif // defined(IMGUI_ENABLED) + + private: +#if defined(IMGUI_ENABLED) + ImGuiCpuProfiler m_imguiCpuProfiler; + bool m_showCpuProfiler{ false }; +#endif // defined(IMGUI_ENABLED) + }; + +} // namespace Profiler diff --git a/Gems/Profiler/Code/Source/ProfilerModule.cpp b/Gems/Profiler/Code/Source/ProfilerModule.cpp new file mode 100644 index 0000000000..055c4cca4d --- /dev/null +++ b/Gems/Profiler/Code/Source/ProfilerModule.cpp @@ -0,0 +1,46 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#include + +#include +#include + +namespace Profiler +{ + class ProfilerModule + : public AZ::Module + { + public: + AZ_RTTI(ProfilerModule, "{4A286414-B387-4D20-9A7E-2F792755B769}", AZ::Module); + AZ_CLASS_ALLOCATOR(ProfilerModule, AZ::SystemAllocator, 0); + + ProfilerModule() + { + // Push results of [MyComponent]::CreateDescriptor() into m_descriptors here. + // Add ALL components descriptors associated with this gem to m_descriptors. + // This will associate the AzTypeInfo information for the components with the the SerializeContext, BehaviorContext and EditContext. + // This happens through the [MyComponent]::Reflect() function. + m_descriptors.insert(m_descriptors.end(), { + ProfilerSystemComponent::CreateDescriptor(), + }); + } + + /** + * Add required SystemComponents to the SystemEntity. + */ + AZ::ComponentTypeList GetRequiredSystemComponents() const override + { + return AZ::ComponentTypeList{ + azrtti_typeid(), + }; + } + }; +}// namespace Profiler + +AZ_DECLARE_MODULE_CLASS(Gem_Profiler, Profiler::ProfilerModule) diff --git a/Gems/Profiler/Code/Source/ProfilerSystemComponent.cpp b/Gems/Profiler/Code/Source/ProfilerSystemComponent.cpp new file mode 100644 index 0000000000..bc51ffd0a7 --- /dev/null +++ b/Gems/Profiler/Code/Source/ProfilerSystemComponent.cpp @@ -0,0 +1,284 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#include + +#include +#include +#include +#include +#include +#include + +namespace Profiler +{ + static constexpr AZ::Crc32 profilerServiceCrc = AZ_CRC_CE("ProfilerService"); + + struct DeplayedFunction + { + using func_type = AZStd::function; + + DeplayedFunction(int framesToDelay, func_type&& function) + : m_function(AZStd::move(function)) + , m_framesLeft(framesToDelay) + { + } + + void Run() + { + if (--m_framesLeft <= 0) + { + m_function(); + } + else + { + AZ::SystemTickBus::QueueFunction( + [](DeplayedFunction&& delayedFunc) + { + delayedFunc.Run(); + }, + AZStd::move(*this) + ); + } + } + + func_type m_function; + int m_framesLeft{ 0 }; + }; + + class ProfilerNotificationBusHandler final + : public ProfilerNotificationBus::Handler + , public AZ::BehaviorEBusHandler + { + public: + AZ_EBUS_BEHAVIOR_BINDER(ProfilerNotificationBusHandler, "{44161459-B816-4876-95A4-BA16DEC767D6}", AZ::SystemAllocator, + OnCaptureCpuProfilingStatisticsFinished + ); + + void OnCaptureCpuProfilingStatisticsFinished(bool result, const AZStd::string& info) override + { + Call(FN_OnCaptureCpuProfilingStatisticsFinished, result, info); + } + + static void Reflect(AZ::ReflectContext* context) + { + if (AZ::BehaviorContext* behaviorContext = azrtti_cast(context)) + { + behaviorContext->EBus("ProfilerNotificationBus") + ->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Automation) + ->Attribute(AZ::Script::Attributes::Module, "profiler") + ->Handler(); + } + } + }; + + bool SerializeCpuProfilingData(const AZStd::ring_buffer& data, AZStd::string outputFilePath, bool wasEnabled) + { + AZ_TracePrintf("ProfilerSystemComponent", "Beginning serialization of %zu frames of profiling data\n", data.size()); + AZ::JsonSerializerSettings serializationSettings; + serializationSettings.m_keepDefaults = true; + + CpuProfilingStatisticsSerializer serializer(data); + + const auto saveResult = AZ::JsonSerializationUtils::SaveObjectToFile(&serializer, + outputFilePath, (CpuProfilingStatisticsSerializer*)nullptr, &serializationSettings); + + AZStd::string captureInfo = outputFilePath; + if (!saveResult.IsSuccess()) + { + captureInfo = AZStd::string::format("Failed to save Cpu Profiling Statistics data to file '%s'. Error: %s", + outputFilePath.c_str(), + saveResult.GetError().c_str()); + AZ_Warning("ProfilerSystemComponent", false, captureInfo.c_str()); + } + else + { + AZ_Printf("ProfilerSystemComponent", "Cpu profiling statistics was saved to file [%s]\n", outputFilePath.c_str()); + } + + // Disable the profiler again + if (!wasEnabled) + { + CpuProfiler::Get()->SetProfilerEnabled(false); + } + + // Notify listeners that the pass' PipelineStatistics queries capture has finished. + ProfilerNotificationBus::Broadcast(&ProfilerNotificationBus::Events::OnCaptureCpuProfilingStatisticsFinished, + saveResult.IsSuccess(), + captureInfo); + + return saveResult.IsSuccess(); + } + + void ProfilerSystemComponent::Reflect(AZ::ReflectContext* context) + { + if (AZ::SerializeContext* serialize = azrtti_cast(context)) + { + serialize->Class() + ->Version(0); + + if (AZ::EditContext* ec = serialize->GetEditContext()) + { + ec->Class("Profiler", "Provides a custom implementation of the AZ::Debug::Profiler interface for capturing performance data") + ->ClassElement(AZ::Edit::ClassElements::EditorData, "") + ->Attribute(AZ::Edit::Attributes::AppearsInAddComponentMenu, AZ_CRC_CE("System")) + ->Attribute(AZ::Edit::Attributes::AutoExpand, true); + + ProfilerNotificationBusHandler::Reflect(context); + } + } + + if (AZ::BehaviorContext* behaviorContext = azrtti_cast(context)) + { + behaviorContext->EBus("ProfilerRequestBus") + ->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Automation) + ->Attribute(AZ::Script::Attributes::Module, "profiler") + ->Event("CaptureCpuProfilingStatistics", &ProfilerRequestBus::Events::CaptureCpuProfilingStatistics); + + ProfilerNotificationBusHandler::Reflect(context); + } + + CpuProfilingStatisticsSerializer::Reflect(context); + } + + void ProfilerSystemComponent::GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided) + { + provided.push_back(profilerServiceCrc); + } + + void ProfilerSystemComponent::GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible) + { + incompatible.push_back(profilerServiceCrc); + } + + void ProfilerSystemComponent::GetRequiredServices([[maybe_unused]] AZ::ComponentDescriptor::DependencyArrayType& required) + { + } + + void ProfilerSystemComponent::GetDependentServices([[maybe_unused]] AZ::ComponentDescriptor::DependencyArrayType& dependent) + { + } + + ProfilerSystemComponent::ProfilerSystemComponent() + { + if (ProfilerInterface::Get() == nullptr) + { + ProfilerInterface::Register(this); + } + } + + ProfilerSystemComponent::~ProfilerSystemComponent() + { + if (ProfilerInterface::Get() == this) + { + ProfilerInterface::Unregister(this); + } + } + + void ProfilerSystemComponent::Activate() + { + ProfilerRequestBus::Handler::BusConnect(); + + m_cpuProfiler.Init(); + } + + void ProfilerSystemComponent::Deactivate() + { + m_cpuProfiler.Shutdown(); + + ProfilerRequestBus::Handler::BusDisconnect(); + + // Block deactivation until the IO thread has finished serializing the CPU data + if (m_cpuDataSerializationThread.joinable()) + { + m_cpuDataSerializationThread.join(); + } + } + + void ProfilerSystemComponent::SetProfilerEnabled(bool enabled) + { + m_cpuProfiler.SetProfilerEnabled(enabled); + } + + bool ProfilerSystemComponent::CaptureCpuProfilingStatistics(const AZStd::string& outputFilePath) + { + bool expected = false; + if (!m_cpuCaptureInProgress.compare_exchange_strong(expected, true)) + { + return false; + } + + // Start the cpu profiling + bool wasEnabled = m_cpuProfiler.IsProfilerEnabled(); + if (!wasEnabled) + { + m_cpuProfiler.SetProfilerEnabled(true); + } + + const int frameDelay = 5; // arbitrary number + DeplayedFunction delayedFunc(frameDelay, + [this, outputFilePath, wasEnabled]() + { + // Blocking call for a single frame of data, avoid thread overhead + AZStd::ring_buffer singleFrameData(1); + singleFrameData.push_back(m_cpuProfiler.GetTimeRegionMap()); + SerializeCpuProfilingData(singleFrameData, outputFilePath, wasEnabled); + m_cpuCaptureInProgress.store(false); + } + ); + delayedFunc.Run(); + + return true; + } + + bool ProfilerSystemComponent::BeginContinuousCpuProfilingCapture() + { + return m_cpuProfiler.BeginContinuousCapture(); + } + + bool ProfilerSystemComponent::EndContinuousCpuProfilingCapture(const AZStd::string& outputFilePath) + { + bool expected = false; + if (!m_cpuDataSerializationInProgress.compare_exchange_strong(expected, true)) + { + AZ_TracePrintf( + "ProfilerSystemComponent", + "Cannot end a continuous capture - another serialization is currently in progress\n"); + return false; + } + + AZStd::ring_buffer captureResult; + const bool captureEnded = m_cpuProfiler.EndContinuousCapture(captureResult); + if (!captureEnded) + { + AZ_TracePrintf("ProfilerSystemComponent", "Could not end the continuous capture, is one in progress?\n"); + m_cpuDataSerializationInProgress.store(false); + return false; + } + + // cpuProfilingData could be 1GB+ once saved, so use an IO thread to write it to disk. + auto threadIoFunction = + [data = AZStd::move(captureResult), filePath = AZStd::string(outputFilePath), &flag = m_cpuDataSerializationInProgress]() + { + SerializeCpuProfilingData(data, filePath, true); + flag.store(false); + }; + + // If the thread object already exists (ex. we have already serialized data), join. This will not block since + // m_cpuDataSerializationInProgress was false, meaning the IO thread has already completed execution. + if (m_cpuDataSerializationThread.joinable()) + { + m_cpuDataSerializationThread.join(); + } + + auto thread = AZStd::thread(threadIoFunction); + m_cpuDataSerializationThread = AZStd::move(thread); + + return true; + } +} // namespace Profiler diff --git a/Gems/Profiler/Code/Source/ProfilerSystemComponent.h b/Gems/Profiler/Code/Source/ProfilerSystemComponent.h new file mode 100644 index 0000000000..76121be04f --- /dev/null +++ b/Gems/Profiler/Code/Source/ProfilerSystemComponent.h @@ -0,0 +1,56 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#pragma once + +#include +#include + +#include +#include + +namespace Profiler +{ + class ProfilerSystemComponent + : public AZ::Component + , protected ProfilerRequestBus::Handler + { + public: + AZ_COMPONENT(ProfilerSystemComponent, "{3f52c1d7-d920-4781-8ed7-88077ec4f305}"); + + static void Reflect(AZ::ReflectContext* context); + + static void GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided); + static void GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible); + static void GetRequiredServices(AZ::ComponentDescriptor::DependencyArrayType& required); + static void GetDependentServices(AZ::ComponentDescriptor::DependencyArrayType& dependent); + + ProfilerSystemComponent(); + ~ProfilerSystemComponent(); + + protected: + // AZ::Component interface implementation + void Activate() override; + void Deactivate() override; + + // ProfilerRequestBus interface implementation + void SetProfilerEnabled(bool enabled) override; + bool CaptureCpuProfilingStatistics(const AZStd::string& outputFilePath) override; + bool BeginContinuousCpuProfilingCapture() override; + bool EndContinuousCpuProfilingCapture(const AZStd::string& outputFilePath) override; + + + AZStd::thread m_cpuDataSerializationThread; + AZStd::atomic_bool m_cpuDataSerializationInProgress{ false }; + + AZStd::atomic_bool m_cpuCaptureInProgress{ false }; + + CpuProfilerImpl m_cpuProfiler; + }; + +} // namespace Profiler diff --git a/Gems/Profiler/Code/profiler_files.cmake b/Gems/Profiler/Code/profiler_files.cmake new file mode 100644 index 0000000000..51ceb00139 --- /dev/null +++ b/Gems/Profiler/Code/profiler_files.cmake @@ -0,0 +1,17 @@ +# +# Copyright (c) Contributors to the Open 3D Engine Project. +# For complete copyright and license terms please see the LICENSE at the root of this distribution. +# +# SPDX-License-Identifier: Apache-2.0 OR MIT +# +# + +set(FILES + Include/Profiler/ProfilerBus.h + Include/Profiler/ProfilerImGuiBus.h + Source/CpuProfiler.h + Source/CpuProfilerImpl.cpp + Source/CpuProfilerImpl.h + Source/ProfilerSystemComponent.cpp + Source/ProfilerSystemComponent.h +) diff --git a/Gems/Profiler/Code/profiler_imgui_shared_files.cmake b/Gems/Profiler/Code/profiler_imgui_shared_files.cmake new file mode 100644 index 0000000000..216f6ceeed --- /dev/null +++ b/Gems/Profiler/Code/profiler_imgui_shared_files.cmake @@ -0,0 +1,15 @@ +# +# Copyright (c) Contributors to the Open 3D Engine Project. +# For complete copyright and license terms please see the LICENSE at the root of this distribution. +# +# SPDX-License-Identifier: Apache-2.0 OR MIT +# +# + +set(FILES + Source/ImGuiCpuProfiler.cpp + Source/ImGuiCpuProfiler.h + Source/ProfilerImGuiModule.cpp + Source/ProfilerImGuiSystemComponent.cpp + Source/ProfilerImGuiSystemComponent.h +) diff --git a/Gems/Profiler/Code/profiler_shared_files.cmake b/Gems/Profiler/Code/profiler_shared_files.cmake new file mode 100644 index 0000000000..b8ee476e23 --- /dev/null +++ b/Gems/Profiler/Code/profiler_shared_files.cmake @@ -0,0 +1,11 @@ +# +# Copyright (c) Contributors to the Open 3D Engine Project. +# For complete copyright and license terms please see the LICENSE at the root of this distribution. +# +# SPDX-License-Identifier: Apache-2.0 OR MIT +# +# + +set(FILES + Source/ProfilerModule.cpp +) diff --git a/Gems/Profiler/gem.json b/Gems/Profiler/gem.json new file mode 100644 index 0000000000..2f121d7618 --- /dev/null +++ b/Gems/Profiler/gem.json @@ -0,0 +1,19 @@ +{ + "gem_name": "Profiler", + "display_name": "Profiler", + "license": "Apache-2.0 Or MIT", + "origin": "Open 3D Engine - o3de.org", + "type": "Code", + "summary": "A collection of utilities for capturing performance data", + "canonical_tags": [ + "Gem" + ], + "user_tags": [ + "Profiler" + ], + "icon_path": "preview.png", + "requirements": "", + "dependencies": [ + "ImGui" + ] +} diff --git a/Gems/Profiler/preview.png b/Gems/Profiler/preview.png new file mode 100644 index 0000000000..0f393ac886 --- /dev/null +++ b/Gems/Profiler/preview.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7ac9dd09bde78f389e3725ac49d61eff109857e004840bc0bc3881739df9618d +size 2217 diff --git a/engine.json b/engine.json index 6646c3bec3..14deffecb8 100644 --- a/engine.json +++ b/engine.json @@ -58,6 +58,7 @@ "Gems/Prefab", "Gems/Presence", "Gems/PrimitiveAssets", + "Gems/Profiler", "Gems/PythonAssetBuilder", "Gems/QtForPython", "Gems/SaveData",