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 b86a1b72af..66adfe9985 100644 --- a/Gems/Atom/Feature/Common/Code/Source/ProfilingCaptureSystemComponent.cpp +++ b/Gems/Atom/Feature/Common/Code/Source/ProfilingCaptureSystemComponent.cpp @@ -38,7 +38,6 @@ namespace AZ OnCaptureQueryTimestampFinished, OnCaptureCpuFrameTimeFinished, OnCaptureQueryPipelineStatisticsFinished, - OnCaptureCpuProfilingStatisticsFinished, OnCaptureBenchmarkMetadataFinished ); @@ -57,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); @@ -357,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) ; @@ -365,6 +358,7 @@ namespace AZ } TimestampSerializer::Reflect(context); + CpuFrameTimeSerializer::Reflect(context); PipelineStatisticsSerializer::Reflect(context); BenchmarkMetadataSerializer::Reflect(context); } @@ -379,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) @@ -525,24 +513,6 @@ namespace AZ return captureStarted; } - bool ProfilingCaptureSystemComponent::CaptureCpuProfilingStatistics([[maybe_unused]] const AZStd::string& outputFilePath) - { - AZ_Warning("ProfilingCaptureSystemComponent", false, "CaptureCpuProfilingStatistics has been disabled"); - return false; - } - - bool ProfilingCaptureSystemComponent::BeginContinuousCpuProfilingCapture() - { - AZ_Warning("ProfilingCaptureSystemComponent", false, "BeginContinuousCpuProfilingCapture has been disabled"); - return false; - } - - bool ProfilingCaptureSystemComponent::EndContinuousCpuProfilingCapture([[maybe_unused]] const AZStd::string& outputFilePath) - { - AZ_Warning("ProfilingCaptureSystemComponent", false, "EndContinuousCpuProfilingCapture has been disabled"); - return false; - } - bool ProfilingCaptureSystemComponent::CaptureBenchmarkMetadata(const AZStd::string& benchmarkName, const AZStd::string& outputFilePath) { const bool captureStarted = m_benchmarkMetadataCapture.StartCapture([benchmarkName, outputFilePath]() @@ -621,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/Profiler/Code/Include/Profiler/ProfilerBus.h b/Gems/Profiler/Code/Include/Profiler/ProfilerBus.h index 28e1513324..22d02dcc87 100644 --- a/Gems/Profiler/Code/Include/Profiler/ProfilerBus.h +++ b/Gems/Profiler/Code/Include/Profiler/ProfilerBus.h @@ -9,6 +9,7 @@ #include #include +#include namespace Profiler { @@ -17,21 +18,39 @@ namespace Profiler public: AZ_RTTI(ProfilerRequests, "{3757c4e5-1941-457c-85ae-16305e17a4c6}"); virtual ~ProfilerRequests() = default; - // Put your public methods here + + //! 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; - ////////////////////////////////////////////////////////////////////////// }; - using ProfilerRequestBus = AZ::EBus; - using ProfilerInterface = AZ::Interface; + 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/Source/ProfilerSystemComponent.cpp b/Gems/Profiler/Code/Source/ProfilerSystemComponent.cpp index 6d4e757b67..c51ed91ce1 100644 --- a/Gems/Profiler/Code/Source/ProfilerSystemComponent.cpp +++ b/Gems/Profiler/Code/Source/ProfilerSystemComponent.cpp @@ -8,14 +8,113 @@ #include -#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)) @@ -29,9 +128,21 @@ namespace Profiler ->ClassElement(AZ::Edit::ClassElements::EditorData, "") ->Attribute(AZ::Edit::Attributes::AppearsInAddComponentMenu, AZ_CRC("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); } @@ -69,10 +180,6 @@ namespace Profiler } } - void ProfilerSystemComponent::Init() - { - } - void ProfilerSystemComponent::Activate() { ProfilerRequestBus::Handler::BusConnect(); @@ -85,5 +192,89 @@ namespace Profiler 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(); + } + } + + 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. + // 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; } } // namespace Profiler diff --git a/Gems/Profiler/Code/Source/ProfilerSystemComponent.h b/Gems/Profiler/Code/Source/ProfilerSystemComponent.h index 65a06145e9..67519c098b 100644 --- a/Gems/Profiler/Code/Source/ProfilerSystemComponent.h +++ b/Gems/Profiler/Code/Source/ProfilerSystemComponent.h @@ -12,7 +12,7 @@ #include #include - +#include namespace Profiler { @@ -34,18 +34,20 @@ namespace Profiler ~ProfilerSystemComponent(); protected: - //////////////////////////////////////////////////////////////////////// - // ProfilerRequestBus interface implementation - - //////////////////////////////////////////////////////////////////////// - - //////////////////////////////////////////////////////////////////////// // AZ::Component interface implementation - void Init() override; void Activate() override; void Deactivate() override; - //////////////////////////////////////////////////////////////////////// + // ProfilerRequestBus interface implementation + 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; };