diff --git a/Code/Framework/AzCore/AzCore/Statistics/RunningStatisticsManager.cpp b/Code/Framework/AzCore/AzCore/Statistics/RunningStatisticsManager.cpp new file mode 100644 index 0000000000..52085d98e7 --- /dev/null +++ b/Code/Framework/AzCore/AzCore/Statistics/RunningStatisticsManager.cpp @@ -0,0 +1,106 @@ +/* + * 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 "RunningStatisticsManager.h" + +namespace AzFramework +{ + namespace Statistics + { + bool RunningStatisticsManager::ContainsStatistic(const AZStd::string& name) + { + auto iterator = m_statisticsNamesToIndexMap.find(name); + return iterator != m_statisticsNamesToIndexMap.end(); + } + + bool RunningStatisticsManager::AddStatistic(const AZStd::string& name, const AZStd::string& units) + { + if (ContainsStatistic(name)) + { + return false; + } + AddStatisticValidated(name, units); + return true; + } + + void RunningStatisticsManager::RemoveStatistic(const AZStd::string& name) + { + auto iterator = m_statisticsNamesToIndexMap.find(name); + if (iterator == m_statisticsNamesToIndexMap.end()) + { + return; + } + AZ::u32 itemIndex = iterator->second; + m_statistics.erase(m_statistics.begin() + itemIndex); + m_statisticsNamesToIndexMap.erase(iterator); + //Update the indices in m_statisticsNamesToIndexMap. + while (itemIndex < m_statistics.size()) + { + const AZStd::string& statName = m_statistics[itemIndex].GetName(); + m_statisticsNamesToIndexMap[statName] = itemIndex; + ++itemIndex; + } + } + + void RunningStatisticsManager::ResetStatistic(const AZStd::string& name) + { + NamedRunningStatistic* stat = GetStatistic(name); + if (!stat) + { + return; + } + stat->Reset(); + } + + void RunningStatisticsManager::ResetAllStatistics() + { + for (NamedRunningStatistic& stat : m_statistics) + { + stat.Reset(); + } + } + + void RunningStatisticsManager::PushSampleForStatistic(const AZStd::string& name, double value) + { + NamedRunningStatistic* stat = GetStatistic(name); + if (!stat) + { + return; + } + stat->PushSample(value); + } + + NamedRunningStatistic* RunningStatisticsManager::GetStatistic(const AZStd::string& name, AZ::u32* indexOut) + { + auto iterator = m_statisticsNamesToIndexMap.find(name); + if (iterator == m_statisticsNamesToIndexMap.end()) + { + return nullptr; + } + const AZ::u32 index = iterator->second; + if (indexOut) + { + *indexOut = index; + } + return &m_statistics[index]; + } + + const AZStd::vector& RunningStatisticsManager::GetAllStatistics() const + { + return m_statistics; + } + + void RunningStatisticsManager::AddStatisticValidated(const AZStd::string& name, const AZStd::string& units) + { + m_statistics.emplace_back(NamedRunningStatistic(name, units)); + const AZ::u32 itemIndex = static_cast(m_statistics.size() - 1); + m_statisticsNamesToIndexMap[name] = itemIndex; + } + + }//namespace Statistics +}//namespace AzFramework diff --git a/Code/Framework/AzCore/AzCore/Statistics/StatisticalProfiler.h b/Code/Framework/AzCore/AzCore/Statistics/StatisticalProfiler.h new file mode 100644 index 0000000000..2d8823c6e8 --- /dev/null +++ b/Code/Framework/AzCore/AzCore/Statistics/StatisticalProfiler.h @@ -0,0 +1,255 @@ +/* + * 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 //Just to get AZ::NullMutex +#include +#include +#include + +namespace AZ +{ + namespace Statistics + { + //! A helper class that facilitates collecting time spent in blocks (scopes) of code + //! and aggregating the measured times as running statistics. + //! + //! See "StatisticalProfilerProxy.h" for more explanations on the meaning of Statistical Profiling. + //! + //! The StatisticalProfiler was made as a template to accommodate for several performance needs... + //! If all the code that is being profiled is single threaded and you want to identify + //! each statistic by its string name, then the default StatisticalProfiler<> works for you. + //! If using a map is too much of what you can afford, then index your + //! statistics with an integer or crc32 and your code profiler should be declared as + //! StatisticalProfiler. + //! For multi-threaded cases and indexing statistic with Crc32 you can have a profiler like this: + //! StatisticalProfiler. + //! The UnitTests mentioned in the first paragraph do benchmarks of different combinations + //! of indexing and synchronization primitives. + //! + //! Even though you can create, subclass and use your own StatisticalProfiler<*,*>, there + //! are some things to consider when working with the StatisticalProfilerProxy: + //! The StatisticalProfilerProxy OWNS an array of StatisticalProfiler. + //! You can "manage" one of those StatisticalProfiler by getting a reference to it and + //! add Running statistics etc. See The TerrainProfilers mentioned above to see concrete use + //! cases on how to work with the StatisticalProfilerProxy. + template + class StatisticalProfiler + { + public: + + //! A Convenience class used to measure time performance of scopes of code + //! with constructor/destructor. Suitable to be used as part of a macro + //! to facilitate its usage. + class TimedScope + { + public: + TimedScope() = delete; + + TimedScope(StatisticalProfiler& profiler, const StatIdType& statId) + : m_profiler(profiler), m_statId(statId) + { + m_startTime = AZStd::chrono::high_resolution_clock::now(); + } + + ~TimedScope() + { + AZStd::chrono::system_clock::time_point stopTime = AZStd::chrono::high_resolution_clock::now(); + AZStd::chrono::microseconds duration = stopTime - m_startTime; + m_profiler.PushSample(m_statId, static_cast(duration.count())); + } + + private: + StatisticalProfiler& m_profiler; + const StatIdType& m_statId; + AZStd::chrono::system_clock::time_point m_startTime; + }; //class TimedScope + + friend class TimedScope; + + StatisticalProfiler() + { + } + + StatisticalProfiler(const StatisticalProfiler& other) + { + m_statisticsManager = other.m_statisticsManager; + m_statsVector.clear(); + m_perFrameAggregates.clear(); + } + + StatisticalProfiler(StatisticalProfiler&& other) + { + m_statisticsManager = AZStd::move(other.m_statisticsManager); + m_perFrameAggregates = AZStd::move(other.m_perFrameAggregates); + } + + virtual ~StatisticalProfiler() + { + } + + AZ::Statistics::StatisticsManager& GetStatsManager() + { + return m_statisticsManager; + } + + //! Should be called once per frame, it runs over all existing timed stats in m_statsForPerFrameCalculation + //! and accumulates all the values as a single stat per frame. + double SummarizePerFrameStats() + { + AZStd::scoped_lock lock(m_mutex); + + if (m_perFrameAggregates.size() < 1) + { + return 0.0; + } + + double allStatsSumMicroSecs = 0.0; + + for (StatisticalAggregate& aggregate : m_perFrameAggregates) + { + double statsSumMicroSecs = 0.0; + for (const AZ::Statistics::NamedRunningStatistic* stat : aggregate.m_statsForPerFrameCalculation) + { + statsSumMicroSecs += stat->GetSum(); + } + + const double frameTime = statsSumMicroSecs - aggregate.m_prevAccumulatedSums; + if (frameTime > 0.0) + { + aggregate.m_statPerFrame->PushSample(frameTime); + aggregate.m_prevAccumulatedSums = statsSumMicroSecs; + } + allStatsSumMicroSecs += statsSumMicroSecs; + } + + return allStatsSumMicroSecs; + } + + void LogAndResetStats(const char* windowName) + { + AZStd::scoped_lock lock(m_mutex); + + if (m_statsVector.size() != m_statisticsManager.GetCount()) + { + m_statsVector.clear(); + m_statisticsManager.GetAllStatistics(m_statsVector); + } + + for (AZ::Statistics::NamedRunningStatistic* stat : m_statsVector) + { + if (stat->GetNumSamples() == 0) + { + continue; + } + const AZStd::string& statReport = stat->GetFormatted(); + AZ_Printf(windowName, "%s\n", statReport.c_str()); + stat->Reset(); + } + for (StatisticalAggregate& aggregate : m_perFrameAggregates) + { + aggregate.m_prevAccumulatedSums = 0.0; + } + } + + void PushSample(const StatIdType& statId, double value) + { + AZStd::scoped_lock lock(m_mutex); + AZ::Statistics::NamedRunningStatistic* stat = m_statisticsManager.GetStatistic(statId); + if (!stat) + { + return; + } + stat->PushSample(value); + } + + const AZ::Statistics::NamedRunningStatistic* GetStatistic(const StatIdType& statId) + { + return m_statisticsManager.GetStatistic(statId); + } + + int AddPerFrameStatisticalAggregate(const AZStd::vector& statIds, + const StatIdType& timePerFrameStatId, + const AZStd::string& timePerFrameStatName) + { + AZStd::scoped_lock lock(m_mutex); + + m_perFrameAggregates.push_back(StatisticalAggregate()); + StatisticalAggregate& aggregate = m_perFrameAggregates[m_perFrameAggregates.size() - 1]; + + int added_count = 0; + for (const StatIdType& statId : statIds) + { + AZ::Statistics::NamedRunningStatistic* stat = m_statisticsManager.GetStatistic(statId); + if (!stat) + { + continue; + } + auto const& itor = AZStd::find(aggregate.m_statsForPerFrameCalculation.begin(), aggregate.m_statsForPerFrameCalculation.end(), stat); + if (itor != aggregate.m_statsForPerFrameCalculation.end()) + { + continue; + } + aggregate.m_statsForPerFrameCalculation.push_back(stat); + added_count++; + } + + if (added_count < 1) + { + m_perFrameAggregates.pop_back(); + return 0; + } + + aggregate.m_statPerFrame = m_statisticsManager.AddStatistic(timePerFrameStatId, timePerFrameStatName, "us", true); + if (!aggregate.m_statPerFrame) + { + AZ_Warning("StatisticalProfiler", false, "Per frame stat with name %s already exists\n", timePerFrameStatName.c_str()); + m_perFrameAggregates.pop_back(); + return 0; + } + + return added_count; + } + + const AZ::Statistics::NamedRunningStatistic* GetFirstStatPerFrame() const + { + if (m_perFrameAggregates.size() < 1) + { + return nullptr; + } + return m_perFrameAggregates[0].m_statPerFrame; + } + + protected: + //! Lock this before reading/writing to m_timeStatisticsManager, or else... + MutexType m_mutex; + AZ::Statistics::StatisticsManager m_statisticsManager; + AZStd::vector m_statsVector; + + + struct StatisticalAggregate + { + StatisticalAggregate() : m_statPerFrame(nullptr), m_prevAccumulatedSums(0.0) + { + + } + AZ::Statistics::NamedRunningStatistic* m_statPerFrame; + AZStd::vector m_statsForPerFrameCalculation; + + //! This one is needed because running statistics are collected many times across + //! several frames. This value is used to calculate a per frame sample for @m_totalTimePerFrameStat, + //! by subtracting @m_prevAccumulatedSums from the accumulated sum in @m_statisticsManager. + double m_prevAccumulatedSums; + }; + + AZStd::vector m_perFrameAggregates; + + }; //class StatisticalProfiler + + }; //namespace Statistics +}; //namespace AZ diff --git a/Code/Framework/AzCore/AzCore/Statistics/StatisticalProfilerProxy.h b/Code/Framework/AzCore/AzCore/Statistics/StatisticalProfilerProxy.h new file mode 100644 index 0000000000..5ea69b205c --- /dev/null +++ b/Code/Framework/AzCore/AzCore/Statistics/StatisticalProfilerProxy.h @@ -0,0 +1,164 @@ +/* + * 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 + + +#if !defined(AZ_PROFILE_TELEMETRY) && defined(AZ_STATISTICAL_PROFILING_ENABLED) + +#if defined(AZ_PROFILE_SCOPE) +#undef AZ_PROFILE_SCOPE +#endif // #if defined(AZ_PROFILE_SCOPE) + +#define AZ_PROFILE_SCOPE(profiler, scopeNameId) \ + static const AZStd::string AZ_JOIN(blockName, __LINE__)(scopeNameId); \ + AZ::Statistics::StatisticalProfilerProxy::TimedScope AZ_JOIN(scope, __LINE__)(profiler, AZ_JOIN(blockName, __LINE__)); + +#endif //#if !defined(AZ_PROFILE_TELEMETRY) + +namespace AZ::Statistics +{ + using StatisticalProfilerId = uint32_t; + + //! This AZ::Interface<> (Yes, it is an application wide singleton) owns an array of StatisticalProfilers. + //! When is this useful? + //! When you need to statistically profile code that runs across DLL boundaries. + //! + //! What is the meaning of "statistically profile" code? + //! In regular profiling with tools like RAD Telemetry, every execution of a profiled + //! scope of code will be captured when using AZ_PROFILE_SCOPE(). You can collect + //! very large amounts of data and do your own post processing and analysis in tools like Excel,etc. + //! In contrast, "statistical profiling" means that everytime AZ_PROFILE_SCOPE() is called, + //! the time spent in the given scope of code will be mathematically accumulated as part of a unique + //! Running statistic. Common statistical parameters like min, max, average, variance and standard deviation + //! are calculated on the fly. This approach reduces considerably the amount of data that is collected. + //! The data is recorded in the Game/Editor Log file. + //! + //! This StatisticalProfilerProxy should be used via the AZ_PROFILE_SCOPE() macro, and by using + //! this macro the developer gains the flexibility of switching at compile time between profiling + //! the code via RAD Telemetry or through statistical profiling. + //! + //! When creating a new statistical profiler add your category (aka profiler id) in Profiler.h (enum class ProfileCategory). + //! Get a reference of the statistical profiler with "GetProfiler(const StatisticalProfilerId& id)" using the profiler Id. + //! Once you get a reference to the profiler you can customize it, add Running statistics to it, etc. + //! Some class in your code will manage the reference to the statistical profiler and will determine + //! the policy on how often to log data to the game logs, etc. For example, by subclassing the TickBus Handler, etc. + //! + //! The StatisticalProfilerProxySystemComponent guarantees that the StatisticalProfilerProxy singleton exists + //! as soon as the AZ::Environment is fully initialized. + //! See StatisticalProfiler.h for more details and info. + class StatisticalProfilerProxy + { + public: + AZ_TYPE_INFO(StatisticalProfilerProxy, "{1103D0EB-1C32-4854-B9D9-40A2D65BDBD2}"); + + using StatIdType = AZStd::string; + using StatisticalProfilerType = StatisticalProfiler; + + //! A Convenience class used to measure time performance of scopes of code + //! with constructor/destructor. Suitable to be used as part of a macro + //! to facilitate its usage. + class TimedScope + { + public: + TimedScope() = delete; + + TimedScope(const StatisticalProfilerId profilerId, const StatIdType& statId) + : m_profilerId(profilerId) + , m_statId(statId) + { + if (!m_profilerProxy) + { + m_profilerProxy = AZ::Interface::Get(); + if (!m_profilerProxy) + { + return; + } + } + if (!m_profilerProxy->IsProfilerActive(profilerId)) + { + return; + } + m_startTime = AZStd::chrono::high_resolution_clock::now(); + } + ~TimedScope() + { + if (!m_profilerProxy) + { + return; + } + AZStd::chrono::system_clock::time_point stopTime = AZStd::chrono::high_resolution_clock::now(); + AZStd::chrono::microseconds duration = stopTime - m_startTime; + m_profilerProxy->PushSample(m_profilerId, m_statId, static_cast(duration.count())); + } + + //! Required only for UnitTests + static void ClearCachedProxy() + { + m_profilerProxy = nullptr; + } + + private: + static StatisticalProfilerProxy* m_profilerProxy; + const StatisticalProfilerId m_profilerId; + const StatIdType& m_statId; + AZStd::chrono::system_clock::time_point m_startTime; + }; // class TimedScope + + friend class TimedScope; + + StatisticalProfilerProxy() + { + // TODO:BUDGETS Query available budgets at registration time and create an associated profiler per type + AZ::Interface::Register(this); + } + + virtual ~StatisticalProfilerProxy() + { + AZ::Interface::Unregister(this); + } + + // Note that you have to delete these for safety reasons, you will trip a static_assert if you do not + StatisticalProfilerProxy(StatisticalProfilerProxy&&) = delete; + StatisticalProfilerProxy& operator=(StatisticalProfilerProxy&&) = delete; + + bool IsProfilerActive(StatisticalProfilerId id) const + { + return m_activeProfilersFlag[static_cast(id)]; + } + + StatisticalProfilerType& GetProfiler(StatisticalProfilerId id) + { + return m_profilers[static_cast(id)]; + } + + void ActivateProfiler(StatisticalProfilerId id, bool activate) + { + m_activeProfilersFlag[static_cast(id)] = activate; + } + + void PushSample(StatisticalProfilerId id, const StatIdType& statId, double value) + { + m_profilers[static_cast(id)].PushSample(statId, value); + } + + private: + // TODO:BUDGETS the number of bits allocated here must be based on the number of budgets available at profiler registration time + AZStd::bitset<128> m_activeProfilersFlag; + AZStd::vector m_profilers; + }; // class StatisticalProfilerProxy + +}; // namespace AZ::Statistics diff --git a/Code/Framework/AzCore/AzCore/Statistics/StatisticalProfilerProxySystemComponent.cpp b/Code/Framework/AzCore/AzCore/Statistics/StatisticalProfilerProxySystemComponent.cpp new file mode 100644 index 0000000000..00bb97b745 --- /dev/null +++ b/Code/Framework/AzCore/AzCore/Statistics/StatisticalProfilerProxySystemComponent.cpp @@ -0,0 +1,66 @@ +/* + * 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 "StatisticalProfilerProxySystemComponent.h" + +//////////////////////////////////////////////////////////////////////////////////////////////////// +namespace AZ +{ + namespace Statistics + { + StatisticalProfilerProxy* StatisticalProfilerProxy::TimedScope::m_profilerProxy = nullptr; + + //////////////////////////////////////////////////////////////////////////////////////////////// + void StatisticalProfilerProxySystemComponent::Reflect(AZ::ReflectContext* context) + { + if (AZ::SerializeContext* serializeContext = azrtti_cast(context)) + { + serializeContext->Class() + ->Version(1); + } + } + + //////////////////////////////////////////////////////////////////////////////////////////////// + void StatisticalProfilerProxySystemComponent::GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided) + { + provided.push_back(AZ_CRC("StatisticalProfilerService", 0x20066f73)); + } + + //////////////////////////////////////////////////////////////////////////////////////////////// + void StatisticalProfilerProxySystemComponent::GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible) + { + incompatible.push_back(AZ_CRC("StatisticalProfilerService", 0x20066f73)); + } + + //////////////////////////////////////////////////////////////////////////////////////////////// + StatisticalProfilerProxySystemComponent::StatisticalProfilerProxySystemComponent() + : m_StatisticalProfilerProxy(nullptr) + { + } + + //////////////////////////////////////////////////////////////////////////////////////////////// + StatisticalProfilerProxySystemComponent::~StatisticalProfilerProxySystemComponent() + { + } + + //////////////////////////////////////////////////////////////////////////////////////////////// + void StatisticalProfilerProxySystemComponent::Activate() + { + m_StatisticalProfilerProxy = new StatisticalProfilerProxy; + } + + //////////////////////////////////////////////////////////////////////////////////////////////// + void StatisticalProfilerProxySystemComponent::Deactivate() + { + delete m_StatisticalProfilerProxy; + } + } //namespace Statistics +} // namespace AZ diff --git a/Code/Framework/AzCore/AzCore/Statistics/StatisticalProfilerProxySystemComponent.h b/Code/Framework/AzCore/AzCore/Statistics/StatisticalProfilerProxySystemComponent.h new file mode 100644 index 0000000000..333e29e3b9 --- /dev/null +++ b/Code/Framework/AzCore/AzCore/Statistics/StatisticalProfilerProxySystemComponent.h @@ -0,0 +1,67 @@ +/* + * 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 "StatisticalProfilerProxy.h" + +//////////////////////////////////////////////////////////////////////////////////////////////////// +namespace AZ +{ + namespace Statistics + { + //////////////////////////////////////////////////////////////////////////////////////////////// + //! This system component manages the globally unique StatisticalProfilerProxy instance. + //! And this is all this component does... it simply makes sure the StatisticalProfilerProxy exists. + class StatisticalProfilerProxySystemComponent : public AZ::Component + { + public: + //////////////////////////////////////////////////////////////////////////////////////////// + // AZ::Component Setup + AZ_COMPONENT(StatisticalProfilerProxySystemComponent, "{1E15565F-A5C1-4BF2-8AEE-D3880AC9E1EB}") + + //////////////////////////////////////////////////////////////////////////////////////////// + //! \ref AZ::ComponentDescriptor::Reflect + static void Reflect(AZ::ReflectContext* reflection); + + //////////////////////////////////////////////////////////////////////////////////////////// + //! \ref AZ::ComponentDescriptor::GetProvidedServices + static void GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided); + + //////////////////////////////////////////////////////////////////////////////////////////// + //! \ref AZ::ComponentDescriptor::GetIncompatibleServices + static void GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible); + + //////////////////////////////////////////////////////////////////////////////////////////// + //! Constructor + StatisticalProfilerProxySystemComponent(); + + //////////////////////////////////////////////////////////////////////////////////////////// + //! Destructor + ~StatisticalProfilerProxySystemComponent() override; + + protected: + //////////////////////////////////////////////////////////////////////////////////////////// + //! \ref AZ::Component::Activate + void Activate() override; + + //////////////////////////////////////////////////////////////////////////////////////////// + //! \ref AZ::Component::Deactivate + void Deactivate() override; + + private: + //////////////////////////////////////////////////////////////////////////////////////////// + // Disable copy constructor + StatisticalProfilerProxySystemComponent(const StatisticalProfilerProxySystemComponent&) = delete; + + //////////////////////////////////////////////////////////////////////////////////////////// + // The one and only StatisticalProfilerProxy (Which is itself an AZ::Interface<>) + StatisticalProfilerProxy* m_StatisticalProfilerProxy; + }; + } //namespace Statistics +} // namespace AZ diff --git a/Code/Framework/AzCore/AzCore/Statistics/StatisticsManager.h b/Code/Framework/AzCore/AzCore/Statistics/StatisticsManager.h new file mode 100644 index 0000000000..5984701f4e --- /dev/null +++ b/Code/Framework/AzCore/AzCore/Statistics/StatisticsManager.h @@ -0,0 +1,200 @@ +/* + * 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 "NamedRunningStatistic.h" + +namespace AZ +{ + namespace Statistics + { + /** + * @brief A Collection of Running Statistics, addressable by a hashable + * class/primitive. e.g. AZ::Crc32, int, AZStd::string, etc. + * + */ + template + class StatisticsManager + { + public: + StatisticsManager() = default; + + StatisticsManager(const StatisticsManager& other) + { + m_statistics.reserve(other.m_statistics.size()); + for (auto const& it : other.m_statistics) + { + const StatIdType& statId = it.first; + const NamedRunningStatistic* stat = it.second; + m_statistics[statId] = new NamedRunningStatistic(*stat); + } + } + + virtual ~StatisticsManager() + { + Clear(); + } + + bool ContainsStatistic(const StatIdType& statId) const + { + auto iterator = m_statistics.find(statId); + return iterator != m_statistics.end(); + } + + AZ::u32 GetCount() const + { + return static_cast(m_statistics.size()); + } + + void GetAllStatistics(AZStd::vector& vector) + { + for (auto const& it : m_statistics) + { + NamedRunningStatistic* stat = it.second; + vector.push_back(stat); + } + } + + //! Helper method to apply units to statistics with empty units string. + AZ::u32 ApplyUnits(const AZStd::string& units) + { + AZ::u32 updatedCount = 0; + for (auto& it : m_statistics) + { + NamedRunningStatistic* stat = it.second; + if (stat->GetUnits().empty()) + { + stat->UpdateUnits(units); + updatedCount++; + } + } + return updatedCount; + } + + void Clear() + { + for (auto& it : m_statistics) + { + NamedRunningStatistic* stat = it.second; + delete stat; + } + m_statistics.clear(); + } + + /** + * Returns nullptr if a statistic with such name doesn't exist, + * otherwise returns a pointer to the statistic. + */ + NamedRunningStatistic* GetStatistic(const StatIdType& statId) + { + auto iterator = m_statistics.find(statId); + if (iterator == m_statistics.end()) + { + return nullptr; + } + return iterator->second; + } + + //! Returns false if a NamedRunningStatistic with such id already exists. + NamedRunningStatistic* AddStatistic(const StatIdType& statId, const bool failIfExist = true) + { + if (failIfExist) + { + NamedRunningStatistic* prevStat = GetStatistic(statId); + if (prevStat) + { + return nullptr; + } + } + NamedRunningStatistic* stat = new NamedRunningStatistic(); + m_statistics[statId] = stat; + return stat; + } + + //! Returns false if a NamedRunningStatistic with such id already exists. + NamedRunningStatistic* AddStatistic(const StatIdType& statId, const AZStd::string& name, const AZStd::string& units, const bool failIfExist = true) + { + if (failIfExist) + { + NamedRunningStatistic* prevStat = GetStatistic(statId); + if (prevStat) + { + return nullptr; + } + } + NamedRunningStatistic* stat = new NamedRunningStatistic(name, units); + m_statistics[statId] = stat; + return stat; + } + + virtual void RemoveStatistic(const StatIdType& statId) + { + auto iterator = m_statistics.find(statId); + if (iterator == m_statistics.end()) + { + return; + } + NamedRunningStatistic* prevStat = iterator->second; + delete prevStat; + m_statistics.erase(iterator); + } + + void ResetStatistic(const StatIdType& statId) + { + NamedRunningStatistic* stat = GetStatistic(statId); + if (!stat) + { + return; + } + stat->Reset(); + } + + void ResetAllStatistics() + { + for (auto& it : m_statistics) + { + NamedRunningStatistic* stat = it.second; + stat->Reset(); + } + } + + void PushSampleForStatistic(const StatIdType& statId, double value) + { + NamedRunningStatistic* stat = GetStatistic(statId); + if (!stat) + { + return; + } + stat->PushSample(value); + } + + //! Expensive function because it does a reverse lookup + bool GetStatId(NamedRunningStatistic* searchStat, StatIdType& statIdOut) const + { + for (auto& it : m_statistics) + { + NamedRunningStatistic* stat = it.second; + if (stat == searchStat) + { + statIdOut = it.first; + return true; + } + } + return false; + } + + + private: + ///Key: StatIdType, Value: NamedRunningStatistic* + AZStd::unordered_map m_statistics; + };//class StatisticsManager + }//namespace Statistics +}//namespace AZ diff --git a/Code/Framework/AzCore/AzCore/Statistics/TimeDataStatisticsManager.cpp b/Code/Framework/AzCore/AzCore/Statistics/TimeDataStatisticsManager.cpp new file mode 100644 index 0000000000..0e9b36a8b6 --- /dev/null +++ b/Code/Framework/AzCore/AzCore/Statistics/TimeDataStatisticsManager.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 "TimeDataStatisticsManager.h" + +namespace AZ +{ + namespace Statistics + { + void TimeDataStatisticsManager::PushTimeDataSample(const char * registerName, const AZ::Debug::ProfilerRegister::TimeData& timeData) + { + const AZStd::string statName(registerName); + NamedRunningStatistic* statistic = GetStatistic(statName); + if (!statistic) + { + const AZStd::string units("us"); + AddStatistic(statName, statName, units, false); + AZ::Debug::ProfilerRegister::TimeData zeroTimeData; + memset(&zeroTimeData, 0, sizeof(AZ::Debug::ProfilerRegister::TimeData)); + m_previousTimeData[statName] = zeroTimeData; + statistic = GetStatistic(statName); + AZ_Assert(statistic != nullptr, "Fatal error adding a new statistic object"); + } + + const AZ::u64 accumulatedTime = timeData.m_time; + const AZ::s64 totalNumCalls = timeData.m_calls; + const AZ::u64 previousAccumulatedTime = m_previousTimeData[statName].m_time; + const AZ::s64 previousTotalNumCalls = m_previousTimeData[statName].m_calls; + const AZ::u64 deltaTime = accumulatedTime - previousAccumulatedTime; + const AZ::s64 deltaCalls = totalNumCalls - previousTotalNumCalls; + + if (deltaCalls == 0) + { + //This is the same old data. Let's skip it + return; + } + + double newSample = static_cast(deltaTime) / deltaCalls; + + statistic->PushSample(newSample); + m_previousTimeData[statName] = timeData; + } + } //namespace Statistics +} //namespace AZ diff --git a/Code/Framework/AzCore/AzCore/Statistics/TimeDataStatisticsManager.h b/Code/Framework/AzCore/AzCore/Statistics/TimeDataStatisticsManager.h new file mode 100644 index 0000000000..c9adc4de2f --- /dev/null +++ b/Code/Framework/AzCore/AzCore/Statistics/TimeDataStatisticsManager.h @@ -0,0 +1,51 @@ +/* + * 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 AZ +{ + namespace Statistics + { + /** + * @brief Specialization useful for data generated with AZ::Debug::FrameProfileComponent + * + * Timer based data collection using AZ_PROFILE_TIMER(...), available in + * AzCore/Debug/Profiler.h can be collected when using AZ::Debug::FrameProfilerComponent + * and AZ::Debug::FrameProfilerBus. The method PushTimeDataSample(...) is a convenience + * to convert those Timer registers into a RunningStatistic. + * + * + */ + class TimeDataStatisticsManager : public StatisticsManager<> + { + public: + TimeDataStatisticsManager() = default; + virtual ~TimeDataStatisticsManager() = default; + + /** + * @brief Adds one sample data to a specific running stat by name. + * + * This method is specialized to work with ProfilerRegister::TimeData that can be intercepted + * during AZ::Debug::FrameProfilerBus::OnFrameProfilerData(). + * For each @param registerName a new RunningStat object is created if it doesn't exist. + * + * Adds the TimeData as one sample for its RunningStatistic. + */ + void PushTimeDataSample(const char * registerName, const AZ::Debug::ProfilerRegister::TimeData& timeData); + + protected: + ///We store here the previous value from the previous timer frame data. + ///This is necessary because AZ_PROFILER_TIMER is cumulative + ///and we need the time spent for each call. + AZStd::unordered_map m_previousTimeData; + }; + } //namespace Statistics +} //namespace AZ diff --git a/Code/Framework/AzCore/AzCore/azcore_files.cmake b/Code/Framework/AzCore/AzCore/azcore_files.cmake index c49c93f220..59db0d94fc 100644 --- a/Code/Framework/AzCore/AzCore/azcore_files.cmake +++ b/Code/Framework/AzCore/AzCore/azcore_files.cmake @@ -566,6 +566,13 @@ set(FILES Statistics/NamedRunningStatistic.h Statistics/RunningStatistic.cpp Statistics/RunningStatistic.h + Statistics/StatisticalProfiler.h + Statistics/StatisticalProfilerProxy.h + Statistics/StatisticalProfilerProxySystemComponent.cpp + Statistics/StatisticalProfilerProxySystemComponent.h + Statistics/StatisticsManager.h + Statistics/TimeDataStatisticsManager.cpp + Statistics/TimeDataStatisticsManager.h StringFunc/StringFunc.cpp StringFunc/StringFunc.h UserSettings/UserSettings.cpp diff --git a/Code/Framework/AzCore/Tests/StatisticalProfiler.cpp b/Code/Framework/AzCore/Tests/StatisticalProfiler.cpp new file mode 100644 index 0000000000..04e70d92a5 --- /dev/null +++ b/Code/Framework/AzCore/Tests/StatisticalProfiler.cpp @@ -0,0 +1,847 @@ +/* + * 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 + +//REMARK: The macros CODE_PROFILER_PROXY_PUSH_TIME and CODE_PROFILER_PUSH_TIME will be redefined +//several times in this file to accommodate for the different specializations of the StatisticalProfiler<> +//template. +#ifdef CODE_PROFILER_PROXY_PUSH_TIME +#undef CODE_PROFILER_PROXY_PUSH_TIME +#endif + +#ifdef CODE_PROFILER_PUSH_TIME +#undef CODE_PROFILER_PUSH_TIME +#endif + +namespace UnitTest +{ + class StatisticalProfilerTest + : public AllocatorsFixture + { + public: + + StatisticalProfilerTest() + { + } + + void SetUp() override + { + AllocatorsFixture::SetUp(); + } + + ~StatisticalProfilerTest() + { + } + + void TearDown() override + { + AllocatorsFixture::TearDown(); + } + + }; //class StatisticalProfilerTest + + TEST_F(StatisticalProfilerTest, StatisticalProfilerStringNoMutex_ProfileCode_ValidateStatistics) + { +//Helper macro. +#define CODE_PROFILER_PUSH_TIME(profiler, scopeNameId) \ + AZ::Statistics::StatisticalProfiler<>::TimedScope AZ_JOIN(scope, __LINE__)(profiler, scopeNameId); + + AZ::Statistics::StatisticalProfiler<> profiler; + + const AZStd::string statNamePerformance("PerformanceResult"); + const AZStd::string statNameBlock("Block"); + + ASSERT_TRUE(profiler.GetStatsManager().AddStatistic(statNamePerformance, statNamePerformance, "us") != nullptr); + ASSERT_TRUE(profiler.GetStatsManager().AddStatistic(statNameBlock, statNameBlock, "us") != nullptr); + + const int iter_count = 10; + { + CODE_PROFILER_PUSH_TIME(profiler, statNamePerformance) + int counter = 0; + for (int i = 0; i < iter_count; i++) + { + CODE_PROFILER_PUSH_TIME(profiler, statNameBlock) + counter++; + } + } + + ASSERT_TRUE(profiler.GetStatistic(statNamePerformance) != nullptr); + EXPECT_EQ(profiler.GetStatistic(statNamePerformance)->GetNumSamples(), 1); + + ASSERT_TRUE(profiler.GetStatistic(statNameBlock) != nullptr); + EXPECT_EQ(profiler.GetStatistic(statNameBlock)->GetNumSamples(), iter_count); + +#undef CODE_PROFILER_PUSH_TIME + + } + + TEST_F(StatisticalProfilerTest, StatisticalProfilerCrc32NoMutex_ProfileCode_ValidateStatistics) + { + //Helper macro. +#define CODE_PROFILER_PUSH_TIME(profiler, scopeNameId) \ + AZ::Statistics::StatisticalProfiler::TimedScope AZ_JOIN(scope, __LINE__)(profiler, scopeNameId); + + AZ::Statistics::StatisticalProfiler profiler; + + const AZ::Crc32 statIdPerformance = AZ_CRC("PerformanceResult", 0xc1f29a10); + const AZStd::string statNamePerformance("PerformanceResult"); + + const AZ::Crc32 statIdBlock = AZ_CRC("Block", 0x831b9722); + const AZStd::string statNameBlock("Block"); + + ASSERT_TRUE(profiler.GetStatsManager().AddStatistic(statIdPerformance, statNamePerformance, "us") != nullptr); + ASSERT_TRUE(profiler.GetStatsManager().AddStatistic(statIdBlock, statNameBlock, "us") != nullptr); + + const int iter_count = 10; + { + CODE_PROFILER_PUSH_TIME(profiler, statIdPerformance) + int counter = 0; + for (int i = 0; i < iter_count; i++) + { + CODE_PROFILER_PUSH_TIME(profiler, statIdBlock) + counter++; + } + } + + ASSERT_TRUE(profiler.GetStatistic(statIdPerformance) != nullptr); + EXPECT_EQ(profiler.GetStatistic(statIdPerformance)->GetNumSamples(), 1); + + ASSERT_TRUE(profiler.GetStatistic(statIdBlock) != nullptr); + EXPECT_EQ(profiler.GetStatistic(statIdBlock)->GetNumSamples(), iter_count); + + ASSERT_TRUE(profiler.GetStatistic(statIdPerformance) != nullptr); + +#undef CODE_PROFILER_PUSH_TIME + + } + + TEST_F(StatisticalProfilerTest, StatisticalProfilerStringWithSharedSpinMutex__ProfileCode_ValidateStatistics) + { + //Helper macro. +#define CODE_PROFILER_PUSH_TIME(profiler, scopeNameId) \ + AZ::Statistics::StatisticalProfiler::TimedScope AZ_JOIN(scope, __LINE__)(profiler, scopeNameId); + + AZ::Statistics::StatisticalProfiler profiler; + + const AZStd::string statNamePerformance("PerformanceResult"); + const AZStd::string statNameBlock("Block"); + + ASSERT_TRUE(profiler.GetStatsManager().AddStatistic(statNamePerformance, statNamePerformance, "us") != nullptr); + ASSERT_TRUE(profiler.GetStatsManager().AddStatistic(statNameBlock, statNameBlock, "us") != nullptr); + + const int iter_count = 10; + { + CODE_PROFILER_PUSH_TIME(profiler, statNamePerformance) + int counter = 0; + for (int i = 0; i < iter_count; i++) + { + CODE_PROFILER_PUSH_TIME(profiler, statNameBlock) + counter++; + } + } + + ASSERT_TRUE(profiler.GetStatistic(statNamePerformance) != nullptr); + EXPECT_EQ(profiler.GetStatistic(statNamePerformance)->GetNumSamples(), 1); + + ASSERT_TRUE(profiler.GetStatistic(statNameBlock) != nullptr); + EXPECT_EQ(profiler.GetStatistic(statNameBlock)->GetNumSamples(), iter_count); + + ASSERT_TRUE(profiler.GetStatistic(statNamePerformance) != nullptr); + +#undef CODE_PROFILER_PUSH_TIME + + } + + TEST_F(StatisticalProfilerTest, StatisticalProfilerCrc32WithSharedSpinMutex__ProfileCode_ValidateStatistics) + { + //Helper macro. +#define CODE_PROFILER_PUSH_TIME(profiler, scopeNameId) \ + AZ::Statistics::StatisticalProfiler::TimedScope AZ_JOIN(scope, __LINE__)(profiler, scopeNameId); + + AZ::Statistics::StatisticalProfiler profiler; + + const AZ::Crc32 statIdPerformance = AZ_CRC("PerformanceResult", 0xc1f29a10); + const AZStd::string statNamePerformance("PerformanceResult"); + + const AZ::Crc32 statIdBlock = AZ_CRC("Block", 0x831b9722); + const AZStd::string statNameBlock("Block"); + + ASSERT_TRUE(profiler.GetStatsManager().AddStatistic(statIdPerformance, statNamePerformance, "us") != nullptr); + ASSERT_TRUE(profiler.GetStatsManager().AddStatistic(statIdBlock, statNameBlock, "us") != nullptr); + + const int iter_count = 10; + { + CODE_PROFILER_PUSH_TIME(profiler, statIdPerformance) + int counter = 0; + for (int i = 0; i < iter_count; i++) + { + CODE_PROFILER_PUSH_TIME(profiler, statIdBlock) + counter++; + } + } + + ASSERT_TRUE(profiler.GetStatistic(statIdPerformance) != nullptr); + EXPECT_EQ(profiler.GetStatistic(statIdPerformance)->GetNumSamples(), 1); + + ASSERT_TRUE(profiler.GetStatistic(statIdBlock) != nullptr); + EXPECT_EQ(profiler.GetStatistic(statIdBlock)->GetNumSamples(), iter_count); + + ASSERT_TRUE(profiler.GetStatistic(statIdPerformance) != nullptr); + +#undef CODE_PROFILER_PUSH_TIME + + } + +#define CODE_PROFILER_PUSH_TIME(profiler, scopeNameId) \ + AZ::Statistics::StatisticalProfiler::TimedScope AZ_JOIN(scope, __LINE__)(profiler, scopeNameId); + + static void simple_thread01(AZ::Statistics::StatisticalProfiler* profiler, int loop_cnt) + { + const AZStd::string simple_thread("simple_thread1"); + const AZStd::string simple_thread_loop("simple_thread1_loop"); + + CODE_PROFILER_PUSH_TIME(*profiler, simple_thread); + + static int counter = 0; + for (int i = 0; i < loop_cnt; i++) + { + CODE_PROFILER_PUSH_TIME(*profiler, simple_thread_loop); + counter++; + } + } + + static void simple_thread02(AZ::Statistics::StatisticalProfiler* profiler, int loop_cnt) + { + const AZStd::string simple_thread("simple_thread2"); + const AZStd::string simple_thread_loop("simple_thread2_loop"); + + CODE_PROFILER_PUSH_TIME(*profiler, simple_thread); + + static int counter = 0; + for (int i = 0; i < loop_cnt; i++) + { + CODE_PROFILER_PUSH_TIME(*profiler, simple_thread_loop); + counter++; + } + } + + static void simple_thread03(AZ::Statistics::StatisticalProfiler* profiler, int loop_cnt) + { + const AZStd::string simple_thread("simple_thread3"); + const AZStd::string simple_thread_loop("simple_thread3_loop"); + + CODE_PROFILER_PUSH_TIME(*profiler, simple_thread); + + static int counter = 0; + for (int i = 0; i < loop_cnt; i++) + { + CODE_PROFILER_PUSH_TIME(*profiler, simple_thread_loop); + counter++; + } + } + +#undef CODE_PROFILER_PUSH_TIME + + TEST_F(StatisticalProfilerTest, StatisticalProfilerStringWithSharedSpinMutex_RunProfiledThreads_ValidateStatistics) + { + AZ::Statistics::StatisticalProfiler profiler; + + const AZStd::string statIdThread1 = "simple_thread1"; + const AZStd::string statNameThread1("simple_thread1"); + const AZStd::string statIdThread1Loop = "simple_thread1_loop"; + const AZStd::string statNameThread1Loop("simple_thread1_loop"); + + const AZStd::string statIdThread2 = "simple_thread2"; + const AZStd::string statNameThread2("simple_thread2"); + const AZStd::string statIdThread2Loop = "simple_thread2_loop"; + const AZStd::string statNameThread2Loop("simple_thread2_loop"); + + const AZStd::string statIdThread3 = "simple_thread3"; + const AZStd::string statNameThread3("simple_thread3"); + const AZStd::string statIdThread3Loop = "simple_thread3_loop"; + const AZStd::string statNameThread3Loop("simple_thread3_loop"); + + ASSERT_TRUE(profiler.GetStatsManager().AddStatistic(statIdThread1, statNameThread1, "us")); + ASSERT_TRUE(profiler.GetStatsManager().AddStatistic(statIdThread1Loop, statNameThread1Loop, "us")); + ASSERT_TRUE(profiler.GetStatsManager().AddStatistic(statIdThread2, statNameThread2, "us")); + ASSERT_TRUE(profiler.GetStatsManager().AddStatistic(statIdThread2Loop, statNameThread2Loop, "us")); + ASSERT_TRUE(profiler.GetStatsManager().AddStatistic(statIdThread3, statNameThread3, "us")); + ASSERT_TRUE(profiler.GetStatsManager().AddStatistic(statIdThread3Loop, statNameThread3Loop, "us")); + + //Let's kickoff the threads to see how much contention affects the profiler's performance. + const int iter_count = 10; + AZStd::thread t1(AZStd::bind(&simple_thread01, &profiler, iter_count)); + AZStd::thread t2(AZStd::bind(&simple_thread02, &profiler, iter_count)); + AZStd::thread t3(AZStd::bind(&simple_thread03, &profiler, iter_count)); + t1.join(); + t2.join(); + t3.join(); + + ASSERT_TRUE(profiler.GetStatistic(statIdThread1) != nullptr); + EXPECT_EQ(profiler.GetStatistic(statIdThread1)->GetNumSamples(), 1); + ASSERT_TRUE(profiler.GetStatistic(statIdThread1Loop) != nullptr); + EXPECT_EQ(profiler.GetStatistic(statIdThread1Loop)->GetNumSamples(), iter_count); + + ASSERT_TRUE(profiler.GetStatistic(statIdThread2) != nullptr); + EXPECT_EQ(profiler.GetStatistic(statIdThread2)->GetNumSamples(), 1); + ASSERT_TRUE(profiler.GetStatistic(statIdThread2Loop) != nullptr); + EXPECT_EQ(profiler.GetStatistic(statIdThread2Loop)->GetNumSamples(), iter_count); + + ASSERT_TRUE(profiler.GetStatistic(statIdThread3) != nullptr); + EXPECT_EQ(profiler.GetStatistic(statIdThread3)->GetNumSamples(), 1); + ASSERT_TRUE(profiler.GetStatistic(statIdThread3Loop) != nullptr); + EXPECT_EQ(profiler.GetStatistic(statIdThread3Loop)->GetNumSamples(), iter_count); + + } + + TEST_F(StatisticalProfilerTest, StatisticalProfilerProxy_ProfileCode_ValidateStatistics) + { +#define CODE_PROFILER_PROXY_PUSH_TIME(profiler, scopeNameId) \ + AZ::Statistics::StatisticalProfilerProxy::TimedScope AZ_JOIN(scope, __LINE__)(profiler, scopeNameId); + + AZ::Statistics::StatisticalProfilerProxy::TimedScope::ClearCachedProxy(); + AZ::Statistics::StatisticalProfilerProxy profilerProxy; + AZ::Statistics::StatisticalProfilerProxy* proxy = AZ::Interface::Get(); + AZ::Statistics::StatisticalProfilerProxy::StatisticalProfilerType& profiler = proxy->GetProfiler(AZ::Debug::ProfileCategory::Terrain); + + const AZ::Statistics::StatisticalProfilerProxy::StatIdType statIdPerformance = "PerformanceResult"; + const AZStd::string statNamePerformance("PerformanceResult"); + + const AZ::Statistics::StatisticalProfilerProxy::StatIdType statIdBlock = "Block"; + const AZStd::string statNameBlock("Block"); + + ASSERT_TRUE(profiler.GetStatsManager().AddStatistic(statIdPerformance, statNamePerformance, "us") != nullptr); + ASSERT_TRUE(profiler.GetStatsManager().AddStatistic(statIdBlock, statNameBlock, "us") != nullptr); + + proxy->ActivateProfiler(AZ::Debug::ProfileCategory::Terrain, true); + + const int iter_count = 10; + { + CODE_PROFILER_PROXY_PUSH_TIME(AZ::Debug::ProfileCategory::Terrain, statIdPerformance) + int counter = 0; + for (int i = 0; i < iter_count; i++) + { + CODE_PROFILER_PROXY_PUSH_TIME(AZ::Debug::ProfileCategory::Terrain, statIdBlock) + counter++; + } + } + + ASSERT_TRUE(profiler.GetStatistic(statIdPerformance) != nullptr); + EXPECT_EQ(profiler.GetStatistic(statIdPerformance)->GetNumSamples(), 1); + + ASSERT_TRUE(profiler.GetStatistic(statIdBlock) != nullptr); + EXPECT_EQ(profiler.GetStatistic(statIdBlock)->GetNumSamples(), iter_count); + + //Clean Up + proxy->ActivateProfiler(AZ::Debug::ProfileCategory::Terrain, false); + +#undef CODE_PROFILER_PROXY_PUSH_TIME + + } + +#define CODE_PROFILER_PROXY_PUSH_TIME(profiler, scopeNameId) \ + AZ::Statistics::StatisticalProfilerProxy::TimedScope AZ_JOIN(scope, __LINE__)(profiler, scopeNameId); + + static void simple_thread1(int loop_cnt) + { + const AZ::Statistics::StatisticalProfilerProxy::StatIdType simple_thread1("simple_thread1"); + const AZ::Statistics::StatisticalProfilerProxy::StatIdType simple_thread1_loop("simple_thread1_loop"); + + CODE_PROFILER_PROXY_PUSH_TIME(AZ::Debug::ProfileCategory::Terrain, simple_thread1); + + static int counter = 0; + for (int i = 0; i < loop_cnt; i++) + { + CODE_PROFILER_PROXY_PUSH_TIME(AZ::Debug::ProfileCategory::Terrain, simple_thread1_loop); + counter++; + } + } + + static void simple_thread2(int loop_cnt) + { + const AZ::Statistics::StatisticalProfilerProxy::StatIdType simple_thread2("simple_thread2"); + const AZ::Statistics::StatisticalProfilerProxy::StatIdType simple_thread2_loop("simple_thread2_loop"); + + CODE_PROFILER_PROXY_PUSH_TIME(AZ::Debug::ProfileCategory::Terrain, simple_thread2); + + static int counter = 0; + for (int i = 0; i < loop_cnt; i++) + { + CODE_PROFILER_PROXY_PUSH_TIME(AZ::Debug::ProfileCategory::Terrain, simple_thread2_loop); + counter++; + } + } + + static void simple_thread3(int loop_cnt) + { + const AZ::Statistics::StatisticalProfilerProxy::StatIdType simple_thread3("simple_thread3"); + const AZ::Statistics::StatisticalProfilerProxy::StatIdType simple_thread3_loop("simple_thread3_loop"); + + CODE_PROFILER_PROXY_PUSH_TIME(AZ::Debug::ProfileCategory::Terrain, simple_thread3); + + static int counter = 0; + for (int i = 0; i < loop_cnt; i++) + { + CODE_PROFILER_PROXY_PUSH_TIME(AZ::Debug::ProfileCategory::Terrain, simple_thread3_loop); + } + } + +#undef CODE_PROFILER_PROXY_PUSH_TIME + + TEST_F(StatisticalProfilerTest, StatisticalProfilerProxy3_RunProfiledThreads_ValidateStatistics) + { + AZ::Statistics::StatisticalProfilerProxy::TimedScope::ClearCachedProxy(); + AZ::Statistics::StatisticalProfilerProxy profilerProxy; + AZ::Statistics::StatisticalProfilerProxy* proxy = AZ::Interface::Get(); + AZ::Statistics::StatisticalProfilerProxy::StatisticalProfilerType& profiler = proxy->GetProfiler(AZ::Debug::ProfileCategory::Terrain); + + const AZ::Statistics::StatisticalProfilerProxy::StatIdType statIdThread1 = "simple_thread1"; + const AZStd::string statNameThread1("simple_thread1"); + const AZ::Statistics::StatisticalProfilerProxy::StatIdType statIdThread1Loop = "simple_thread1_loop"; + const AZStd::string statNameThread1Loop("simple_thread1_loop"); + + const AZ::Statistics::StatisticalProfilerProxy::StatIdType statIdThread2 = "simple_thread2"; + const AZStd::string statNameThread2("simple_thread2"); + const AZ::Statistics::StatisticalProfilerProxy::StatIdType statIdThread2Loop = "simple_thread2_loop"; + const AZStd::string statNameThread2Loop("simple_thread2_loop"); + + const AZ::Statistics::StatisticalProfilerProxy::StatIdType statIdThread3 = "simple_thread3"; + const AZStd::string statNameThread3("simple_thread3"); + const AZ::Statistics::StatisticalProfilerProxy::StatIdType statIdThread3Loop = "simple_thread3_loop"; + const AZStd::string statNameThread3Loop("simple_thread3_loop"); + + ASSERT_TRUE(profiler.GetStatsManager().AddStatistic(statIdThread1, statNameThread1, "us")); + ASSERT_TRUE(profiler.GetStatsManager().AddStatistic(statIdThread1Loop, statNameThread1Loop, "us")); + ASSERT_TRUE(profiler.GetStatsManager().AddStatistic(statIdThread2, statNameThread2, "us")); + ASSERT_TRUE(profiler.GetStatsManager().AddStatistic(statIdThread2Loop, statNameThread2Loop, "us")); + ASSERT_TRUE(profiler.GetStatsManager().AddStatistic(statIdThread3, statNameThread3, "us")); + ASSERT_TRUE(profiler.GetStatsManager().AddStatistic(statIdThread3Loop, statNameThread3Loop, "us")); + + proxy->ActivateProfiler(AZ::Debug::ProfileCategory::Terrain, true); + + //Let's kickoff the threads to see how much contention affects the profiler's performance. + const int iter_count = 10; + AZStd::thread t1(AZStd::bind(&simple_thread1, iter_count)); + AZStd::thread t2(AZStd::bind(&simple_thread2, iter_count)); + AZStd::thread t3(AZStd::bind(&simple_thread3, iter_count)); + t1.join(); + t2.join(); + t3.join(); + + ASSERT_TRUE(profiler.GetStatistic(statIdThread1) != nullptr); + EXPECT_EQ(profiler.GetStatistic(statIdThread1)->GetNumSamples(), 1); + ASSERT_TRUE(profiler.GetStatistic(statIdThread1Loop) != nullptr); + EXPECT_EQ(profiler.GetStatistic(statIdThread1Loop)->GetNumSamples(), iter_count); + + ASSERT_TRUE(profiler.GetStatistic(statIdThread2) != nullptr); + EXPECT_EQ(profiler.GetStatistic(statIdThread2)->GetNumSamples(), 1); + ASSERT_TRUE(profiler.GetStatistic(statIdThread2Loop) != nullptr); + EXPECT_EQ(profiler.GetStatistic(statIdThread2Loop)->GetNumSamples(), iter_count); + + ASSERT_TRUE(profiler.GetStatistic(statIdThread3) != nullptr); + EXPECT_EQ(profiler.GetStatistic(statIdThread3)->GetNumSamples(), 1); + ASSERT_TRUE(profiler.GetStatistic(statIdThread3Loop) != nullptr); + EXPECT_EQ(profiler.GetStatistic(statIdThread3Loop)->GetNumSamples(), iter_count); + + //Clean Up + proxy->ActivateProfiler(AZ::Debug::ProfileCategory::Terrain, false); + } + + /** Trace message handler to track messages during tests +*/ + struct MyTraceMessageSink final + : public AZ::Debug::TraceMessageDrillerBus::Handler + { + MyTraceMessageSink() + { + AZ::Debug::TraceMessageDrillerBus::Handler::BusConnect(); + } + + ~MyTraceMessageSink() + { + AZ::Debug::TraceMessageDrillerBus::Handler::BusDisconnect(); + } + + ////////////////////////////////////////////////////////////////////////// + // TraceMessageDrillerBus + void OnPrintf(const char* window, const char* message) override + { + OnOutput(window, message); + } + + void OnOutput(const char* window, const char* message) override + { + printf("%s: %s\n", window, message); + } + }; //struct MyTraceMessageSink + + class Suite_StatisticalProfilerPerformance + : public AllocatorsFixture + { + public: + MyTraceMessageSink* m_testSink; + + Suite_StatisticalProfilerPerformance() :m_testSink(nullptr) + { + } + + void SetUp() override + { + AllocatorsFixture::SetUp(); + m_testSink = new MyTraceMessageSink(); + } + + ~Suite_StatisticalProfilerPerformance() + { + } + + void TearDown() override + { + // clearing up memory + delete m_testSink; + AllocatorsFixture::TearDown(); + } + + }; //class Suite_StatisticalProfilerPerformance + + TEST_F(Suite_StatisticalProfilerPerformance, StatisticalProfilerStringNoMutex_1ThreadPerformance) + { + //Helper macro. +#define CODE_PROFILER_PUSH_TIME(profiler, scopeNameId) \ + AZ::Statistics::StatisticalProfiler<>::TimedScope AZ_JOIN(scope, __LINE__)(profiler, scopeNameId); + + AZ::Statistics::StatisticalProfiler<> profiler; + + const AZStd::string statNamePerformance("PerformanceResult"); + const AZStd::string statNameBlock("Block"); + + ASSERT_TRUE(profiler.GetStatsManager().AddStatistic(statNamePerformance, statNamePerformance, "us") != nullptr); + ASSERT_TRUE(profiler.GetStatsManager().AddStatistic(statNameBlock, statNameBlock, "us") != nullptr); + + const int iter_count = 1000000; + { + CODE_PROFILER_PUSH_TIME(profiler, statNamePerformance) + int counter = 0; + for (int i = 0; i < iter_count; i++) + { + CODE_PROFILER_PUSH_TIME(profiler, statNameBlock) + counter++; + } + } + + ASSERT_TRUE(profiler.GetStatistic(statNamePerformance) != nullptr); + EXPECT_EQ(profiler.GetStatistic(statNamePerformance)->GetNumSamples(), 1); + + ASSERT_TRUE(profiler.GetStatistic(statNameBlock) != nullptr); + EXPECT_EQ(profiler.GetStatistic(statNameBlock)->GetNumSamples(), iter_count); + + profiler.LogAndResetStats("StatisticalProfilerStringNoMutex"); + + ASSERT_TRUE(profiler.GetStatistic(statNamePerformance) != nullptr); + +#undef CODE_PROFILER_PUSH_TIME + + } + + TEST_F(Suite_StatisticalProfilerPerformance, StatisticalProfilerCrc32NoMutex_1ThreadPerformance) + { + //Helper macro. +#define CODE_PROFILER_PUSH_TIME(profiler, scopeNameId) \ + AZ::Statistics::StatisticalProfiler::TimedScope AZ_JOIN(scope, __LINE__)(profiler, scopeNameId); + + AZ::Statistics::StatisticalProfiler profiler; + + const AZ::Crc32 statIdPerformance = AZ_CRC("PerformanceResult", 0xc1f29a10); + const AZStd::string statNamePerformance("PerformanceResult"); + + const AZ::Crc32 statIdBlock = AZ_CRC("Block", 0x831b9722); + const AZStd::string statNameBlock("Block"); + + ASSERT_TRUE(profiler.GetStatsManager().AddStatistic(statIdPerformance, statNamePerformance, "us") != nullptr); + ASSERT_TRUE(profiler.GetStatsManager().AddStatistic(statIdBlock, statNameBlock, "us") != nullptr); + + const int iter_count = 1000000; + { + CODE_PROFILER_PUSH_TIME(profiler, statIdPerformance) + int counter = 0; + for (int i = 0; i < iter_count; i++) + { + CODE_PROFILER_PUSH_TIME(profiler, statIdBlock) + counter++; + } + } + + ASSERT_TRUE(profiler.GetStatistic(statIdPerformance) != nullptr); + EXPECT_EQ(profiler.GetStatistic(statIdPerformance)->GetNumSamples(), 1); + + ASSERT_TRUE(profiler.GetStatistic(statIdBlock) != nullptr); + EXPECT_EQ(profiler.GetStatistic(statIdBlock)->GetNumSamples(), iter_count); + + profiler.LogAndResetStats("StatisticalProfilerCrc32NoMutex"); + + ASSERT_TRUE(profiler.GetStatistic(statIdPerformance) != nullptr); + +#undef CODE_PROFILER_PUSH_TIME + + } + + TEST_F(Suite_StatisticalProfilerPerformance, StatisticalProfilerStringWithSharedSpinMutex_1ThreadPerformance) + { + //Helper macro. +#define CODE_PROFILER_PUSH_TIME(profiler, scopeNameId) \ + AZ::Statistics::StatisticalProfiler::TimedScope AZ_JOIN(scope, __LINE__)(profiler, scopeNameId); + + AZ::Statistics::StatisticalProfiler profiler; + + const AZStd::string statNamePerformance("PerformanceResult"); + const AZStd::string statNameBlock("Block"); + + ASSERT_TRUE(profiler.GetStatsManager().AddStatistic(statNamePerformance, statNamePerformance, "us") != nullptr); + ASSERT_TRUE(profiler.GetStatsManager().AddStatistic(statNameBlock, statNameBlock, "us") != nullptr); + + const int iter_count = 1000000; + { + CODE_PROFILER_PUSH_TIME(profiler, statNamePerformance) + int counter = 0; + for (int i = 0; i < iter_count; i++) + { + CODE_PROFILER_PUSH_TIME(profiler, statNameBlock) + counter++; + } + } + + ASSERT_TRUE(profiler.GetStatistic(statNamePerformance) != nullptr); + EXPECT_EQ(profiler.GetStatistic(statNamePerformance)->GetNumSamples(), 1); + + ASSERT_TRUE(profiler.GetStatistic(statNameBlock) != nullptr); + EXPECT_EQ(profiler.GetStatistic(statNameBlock)->GetNumSamples(), iter_count); + + profiler.LogAndResetStats("StatisticalProfilerStringWithSharedSpinMutex"); + + ASSERT_TRUE(profiler.GetStatistic(statNamePerformance) != nullptr); + +#undef CODE_PROFILER_PUSH_TIME + + } + + TEST_F(Suite_StatisticalProfilerPerformance, StatisticalProfilerCrc32WithSharedSpinMutex_1ThreadPerformance) + { + //Helper macro. +#define CODE_PROFILER_PUSH_TIME(profiler, scopeNameId) \ + AZ::Statistics::StatisticalProfiler::TimedScope AZ_JOIN(scope, __LINE__)(profiler, scopeNameId); + + AZ::Statistics::StatisticalProfiler profiler; + + const AZ::Crc32 statIdPerformance = AZ_CRC("PerformanceResult", 0xc1f29a10); + const AZStd::string statNamePerformance("PerformanceResult"); + + const AZ::Crc32 statIdBlock = AZ_CRC("Block", 0x831b9722); + const AZStd::string statNameBlock("Block"); + + ASSERT_TRUE(profiler.GetStatsManager().AddStatistic(statIdPerformance, statNamePerformance, "us") != nullptr); + ASSERT_TRUE(profiler.GetStatsManager().AddStatistic(statIdBlock, statNameBlock, "us") != nullptr); + + const int iter_count = 1000000; + { + CODE_PROFILER_PUSH_TIME(profiler, statIdPerformance) + int counter = 0; + for (int i = 0; i < iter_count; i++) + { + CODE_PROFILER_PUSH_TIME(profiler, statIdBlock) + counter++; + } + } + + ASSERT_TRUE(profiler.GetStatistic(statIdPerformance) != nullptr); + EXPECT_EQ(profiler.GetStatistic(statIdPerformance)->GetNumSamples(), 1); + + ASSERT_TRUE(profiler.GetStatistic(statIdBlock) != nullptr); + EXPECT_EQ(profiler.GetStatistic(statIdBlock)->GetNumSamples(), iter_count); + + profiler.LogAndResetStats("StatisticalProfilerCrc32WithSharedSpinMutex"); + + ASSERT_TRUE(profiler.GetStatistic(statIdPerformance) != nullptr); + +#undef CODE_PROFILER_PUSH_TIME + + } + + TEST_F(Suite_StatisticalProfilerPerformance, StatisticalProfilerStringWithSharedSpinMutex3Threads_3ThreadsPerformance) + { + AZ::Statistics::StatisticalProfiler profiler; + + const AZStd::string statIdThread1 = "simple_thread1"; + const AZStd::string statNameThread1("simple_thread1"); + const AZStd::string statIdThread1Loop = "simple_thread1_loop"; + const AZStd::string statNameThread1Loop("simple_thread1_loop"); + + const AZStd::string statIdThread2 = "simple_thread2"; + const AZStd::string statNameThread2("simple_thread2"); + const AZStd::string statIdThread2Loop = "simple_thread2_loop"; + const AZStd::string statNameThread2Loop("simple_thread2_loop"); + + const AZStd::string statIdThread3 = "simple_thread3"; + const AZStd::string statNameThread3("simple_thread3"); + const AZStd::string statIdThread3Loop = "simple_thread3_loop"; + const AZStd::string statNameThread3Loop("simple_thread3_loop"); + + ASSERT_TRUE(profiler.GetStatsManager().AddStatistic(statIdThread1, statNameThread1, "us")); + ASSERT_TRUE(profiler.GetStatsManager().AddStatistic(statIdThread1Loop, statNameThread1Loop, "us")); + ASSERT_TRUE(profiler.GetStatsManager().AddStatistic(statIdThread2, statNameThread2, "us")); + ASSERT_TRUE(profiler.GetStatsManager().AddStatistic(statIdThread2Loop, statNameThread2Loop, "us")); + ASSERT_TRUE(profiler.GetStatsManager().AddStatistic(statIdThread3, statNameThread3, "us")); + ASSERT_TRUE(profiler.GetStatsManager().AddStatistic(statIdThread3Loop, statNameThread3Loop, "us")); + + //Let's kickoff the threads to see how much contention affects the profiler's performance. + const int iter_count = 1000000; + AZStd::thread t1(AZStd::bind(&simple_thread01, &profiler, iter_count)); + AZStd::thread t2(AZStd::bind(&simple_thread02, &profiler, iter_count)); + AZStd::thread t3(AZStd::bind(&simple_thread03, &profiler, iter_count)); + t1.join(); + t2.join(); + t3.join(); + + ASSERT_TRUE(profiler.GetStatistic(statIdThread1) != nullptr); + EXPECT_EQ(profiler.GetStatistic(statIdThread1)->GetNumSamples(), 1); + ASSERT_TRUE(profiler.GetStatistic(statIdThread1Loop) != nullptr); + EXPECT_EQ(profiler.GetStatistic(statIdThread1Loop)->GetNumSamples(), iter_count); + + ASSERT_TRUE(profiler.GetStatistic(statIdThread2) != nullptr); + EXPECT_EQ(profiler.GetStatistic(statIdThread2)->GetNumSamples(), 1); + ASSERT_TRUE(profiler.GetStatistic(statIdThread2Loop) != nullptr); + EXPECT_EQ(profiler.GetStatistic(statIdThread2Loop)->GetNumSamples(), iter_count); + + ASSERT_TRUE(profiler.GetStatistic(statIdThread3) != nullptr); + EXPECT_EQ(profiler.GetStatistic(statIdThread3)->GetNumSamples(), 1); + ASSERT_TRUE(profiler.GetStatistic(statIdThread3Loop) != nullptr); + EXPECT_EQ(profiler.GetStatistic(statIdThread3Loop)->GetNumSamples(), iter_count); + + profiler.LogAndResetStats("3_Threads_StatisticalProfiler"); + + ASSERT_TRUE(profiler.GetStatistic(statIdThread1) != nullptr); + + } + +#define CODE_PROFILER_PROXY_PUSH_TIME(profiler, scopeNameId) \ + AZ::Statistics::StatisticalProfilerProxy::TimedScope AZ_JOIN(scope, __LINE__)(profiler, scopeNameId); + + TEST_F(Suite_StatisticalProfilerPerformance, StatisticalProfilerProxy_1ThreadPerformance) + { + AZ::Statistics::StatisticalProfilerProxy::TimedScope::ClearCachedProxy(); + AZ::Statistics::StatisticalProfilerProxy profilerProxy; + AZ::Statistics::StatisticalProfilerProxy* proxy = AZ::Interface::Get(); + AZ::Statistics::StatisticalProfilerProxy::StatisticalProfilerType& profiler = proxy->GetProfiler(AZ::Debug::ProfileCategory::Terrain); + + const AZ::Statistics::StatisticalProfilerProxy::StatIdType statIdPerformance = "PerformanceResult"; + const AZStd::string statNamePerformance("PerformanceResult"); + + const AZ::Statistics::StatisticalProfilerProxy::StatIdType statIdBlock = "Block"; + const AZStd::string statNameBlock("Block"); + + ASSERT_TRUE(profiler.GetStatsManager().AddStatistic(statIdPerformance, statNamePerformance, "us") != nullptr); + ASSERT_TRUE(profiler.GetStatsManager().AddStatistic(statIdBlock, statNameBlock, "us") != nullptr); + + proxy->ActivateProfiler(AZ::Debug::ProfileCategory::Terrain, true); + + const int iter_count = 1000000; + { + CODE_PROFILER_PROXY_PUSH_TIME(AZ::Debug::ProfileCategory::Terrain, statIdPerformance) + int counter = 0; + for (int i = 0; i < iter_count; i++) + { + CODE_PROFILER_PROXY_PUSH_TIME(AZ::Debug::ProfileCategory::Terrain, statIdBlock) + counter++; + } + } + + ASSERT_TRUE(profiler.GetStatistic(statIdPerformance) != nullptr); + EXPECT_EQ(profiler.GetStatistic(statIdPerformance)->GetNumSamples(), 1); + + ASSERT_TRUE(profiler.GetStatistic(statIdBlock) != nullptr); + EXPECT_EQ(profiler.GetStatistic(statIdBlock)->GetNumSamples(), iter_count); + + profiler.LogAndResetStats("StatisticalProfilerProxy"); + + //Clean Up + proxy->ActivateProfiler(AZ::Debug::ProfileCategory::Terrain, false); + } + +#undef CODE_PROFILER_PROXY_PUSH_TIME + + TEST_F(Suite_StatisticalProfilerPerformance, StatisticalProfilerProxy_3ThreadsPerformance) + { + AZ::Statistics::StatisticalProfilerProxy::TimedScope::ClearCachedProxy(); + AZ::Statistics::StatisticalProfilerProxy profilerProxy; + AZ::Statistics::StatisticalProfilerProxy* proxy = AZ::Interface::Get(); + AZ::Statistics::StatisticalProfilerProxy::StatisticalProfilerType& profiler = proxy->GetProfiler(AZ::Debug::ProfileCategory::Terrain); + + const AZ::Statistics::StatisticalProfilerProxy::StatIdType statIdThread1 = "simple_thread1"; + const AZStd::string statNameThread1("simple_thread1"); + const AZ::Statistics::StatisticalProfilerProxy::StatIdType statIdThread1Loop = "simple_thread1_loop"; + const AZStd::string statNameThread1Loop("simple_thread1_loop"); + + const AZ::Statistics::StatisticalProfilerProxy::StatIdType statIdThread2 = "simple_thread2"; + const AZStd::string statNameThread2("simple_thread2"); + const AZ::Statistics::StatisticalProfilerProxy::StatIdType statIdThread2Loop = "simple_thread2_loop"; + const AZStd::string statNameThread2Loop("simple_thread2_loop"); + + const AZ::Statistics::StatisticalProfilerProxy::StatIdType statIdThread3 = "simple_thread3"; + const AZStd::string statNameThread3("simple_thread3"); + const AZ::Statistics::StatisticalProfilerProxy::StatIdType statIdThread3Loop = "simple_thread3_loop"; + const AZStd::string statNameThread3Loop("simple_thread3_loop"); + + ASSERT_TRUE(profiler.GetStatsManager().AddStatistic(statIdThread1, statNameThread1, "us")); + ASSERT_TRUE(profiler.GetStatsManager().AddStatistic(statIdThread1Loop, statNameThread1Loop, "us")); + ASSERT_TRUE(profiler.GetStatsManager().AddStatistic(statIdThread2, statNameThread2, "us")); + ASSERT_TRUE(profiler.GetStatsManager().AddStatistic(statIdThread2Loop, statNameThread2Loop, "us")); + ASSERT_TRUE(profiler.GetStatsManager().AddStatistic(statIdThread3, statNameThread3, "us")); + ASSERT_TRUE(profiler.GetStatsManager().AddStatistic(statIdThread3Loop, statNameThread3Loop, "us")); + + proxy->ActivateProfiler(AZ::Debug::ProfileCategory::Terrain, true); + + //Let's kickoff the threads to see how much contention affects the profiler's performance. + const int iter_count = 1000000; + AZStd::thread t1(AZStd::bind(&simple_thread1, iter_count)); + AZStd::thread t2(AZStd::bind(&simple_thread2, iter_count)); + AZStd::thread t3(AZStd::bind(&simple_thread3, iter_count)); + t1.join(); + t2.join(); + t3.join(); + + ASSERT_TRUE(profiler.GetStatistic(statIdThread1) != nullptr); + EXPECT_EQ(profiler.GetStatistic(statIdThread1)->GetNumSamples(), 1); + ASSERT_TRUE(profiler.GetStatistic(statIdThread1Loop) != nullptr); + EXPECT_EQ(profiler.GetStatistic(statIdThread1Loop)->GetNumSamples(), iter_count); + + ASSERT_TRUE(profiler.GetStatistic(statIdThread2) != nullptr); + EXPECT_EQ(profiler.GetStatistic(statIdThread2)->GetNumSamples(), 1); + ASSERT_TRUE(profiler.GetStatistic(statIdThread2Loop) != nullptr); + EXPECT_EQ(profiler.GetStatistic(statIdThread2Loop)->GetNumSamples(), iter_count); + + ASSERT_TRUE(profiler.GetStatistic(statIdThread3) != nullptr); + EXPECT_EQ(profiler.GetStatistic(statIdThread3)->GetNumSamples(), 1); + ASSERT_TRUE(profiler.GetStatistic(statIdThread3Loop) != nullptr); + EXPECT_EQ(profiler.GetStatistic(statIdThread3Loop)->GetNumSamples(), iter_count); + + profiler.LogAndResetStats("3_Threads_StatisticalProfilerProxy"); + + //Clean Up + proxy->ActivateProfiler(AZ::Debug::ProfileCategory::Terrain, false); + } + +}//namespace UnitTest diff --git a/Code/Framework/AzCore/Tests/Statistics.cpp b/Code/Framework/AzCore/Tests/Statistics.cpp new file mode 100644 index 0000000000..941c2eff0b --- /dev/null +++ b/Code/Framework/AzCore/Tests/Statistics.cpp @@ -0,0 +1,263 @@ +/* + * 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 + +using namespace AZ; +using namespace Debug; + +namespace UnitTest +{ + class StatisticsTest + : public AllocatorsFixture + { + public: + StatisticsTest() + { + } + + void SetUp() override + { + AllocatorsFixture::SetUp(); + + m_dataSamples = AZStd::make_unique>(); + const u32 numSamples = 100; + m_dataSamples->set_capacity(numSamples); + for (u32 i = 0; i < numSamples; ++i) + { + m_dataSamples->push_back(i); + } + } + + ~StatisticsTest() + { + } + + void TearDown() override + { + // clearing up memory + m_dataSamples = nullptr; + + AllocatorsFixture::TearDown(); + } + + protected: + AZStd::unique_ptr> m_dataSamples; + }; //class StatisticsTest + + TEST_F(StatisticsTest, RunningStatistic_ProcessAnArrayOfNumbers_GetExpectedStatisticalData) + { + Statistics::RunningStatistic runningStat; + + ASSERT_TRUE(m_dataSamples.get() != nullptr); + const AZStd::vector& dataSamples = *m_dataSamples; + for (u32 sample : dataSamples) + { + runningStat.PushSample(sample); + } + + EXPECT_EQ(runningStat.GetNumSamples(), dataSamples.size()); + EXPECT_EQ(runningStat.GetMostRecentSample(), dataSamples.back()); + EXPECT_EQ(runningStat.GetMinimum(), dataSamples[0]); + EXPECT_EQ(runningStat.GetMaximum(), dataSamples.back()); + EXPECT_NEAR(runningStat.GetAverage(), 49.5, 0.001); + EXPECT_NEAR(runningStat.GetVariance(), 841.666, 0.001); + EXPECT_NEAR(runningStat.GetStdev(), 29.011, 0.001); + EXPECT_NEAR(runningStat.GetVariance(Statistics::VarianceType::P), 833.25, 0.001); + EXPECT_NEAR(runningStat.GetStdev(Statistics::VarianceType::P), 28.866, 0.001); + + //Reset the stat object. + runningStat.Reset(); + EXPECT_EQ(runningStat.GetNumSamples(), 0); + EXPECT_EQ(runningStat.GetAverage(), 0.0); + EXPECT_EQ(runningStat.GetStdev(), 0.0); + } + + + TEST_F(StatisticsTest, StatisticsManager_AddAndRemoveStatisticistics_CollectionIntegrityIsCorrect) + { + Statistics::StatisticsManager<> statsManager; + AZStd::string statName0("stat0"); + AZStd::string statName1("stat1"); + AZStd::string statName2("stat2"); + AZStd::string statName3("stat3"); + EXPECT_TRUE(statsManager.AddStatistic(statName0, statName0, "")); + EXPECT_TRUE(statsManager.AddStatistic(statName1, statName1, "")); + EXPECT_TRUE(statsManager.AddStatistic(statName2, statName2, "")); + EXPECT_TRUE(statsManager.AddStatistic(statName3, statName3, "")); + + //Validate the number of running statistics object we have so far. + { + AZStd::vector allStats; + statsManager.GetAllStatistics(allStats); + EXPECT_TRUE(allStats.size() == 4); + } + + //Try to add an Stat that already exist. expect to fail. + EXPECT_EQ(statsManager.AddStatistic(statName1), nullptr); + + //Remove stat1. + statsManager.RemoveStatistic(statName1); + //Validate the number of running statistics object we have so far. + { + AZStd::vector allStats; + statsManager.GetAllStatistics(allStats); + EXPECT_TRUE(allStats.size() == 3); + } + + //Add stat1 again, expect to pass. + EXPECT_TRUE(statsManager.AddStatistic(statName1)); + + //Get a pointer to stat2. + Statistics::NamedRunningStatistic* stat2 = statsManager.GetStatistic(statName2); + ASSERT_TRUE(stat2 != nullptr); + EXPECT_EQ(stat2->GetName(), statName2); + } + + TEST_F(StatisticsTest, StatisticsManager_DistributeSamplesAcrossStatistics_StatisticsAreCorrect) + { + Statistics::StatisticsManager<> statsManager; + AZStd::string statName0("stat0"); + AZStd::string statName1("stat1"); + AZStd::string statName2("stat2"); + AZStd::string statName3("stat3"); + + EXPECT_TRUE(statsManager.AddStatistic(statName3)); + EXPECT_TRUE(statsManager.AddStatistic(statName0)); + EXPECT_TRUE(statsManager.AddStatistic(statName2)); + EXPECT_TRUE(statsManager.AddStatistic(statName1)); + + //Distribute the 100 samples of data evenly across the 4 running statistics. + ASSERT_TRUE(m_dataSamples.get() != nullptr); + const AZStd::vector& dataSamples = *m_dataSamples; + const size_t numSamples = dataSamples.size(); + const size_t numSamplesPerStat = numSamples / 4; + size_t sampleIndex = 0; + size_t nextStopIndex = numSamplesPerStat; + while (sampleIndex < nextStopIndex) + { + statsManager.PushSampleForStatistic(statName0, dataSamples[sampleIndex]); + sampleIndex++; + } + nextStopIndex += numSamplesPerStat; + while (sampleIndex < nextStopIndex) + { + statsManager.PushSampleForStatistic(statName1, dataSamples[sampleIndex]); + sampleIndex++; + } + nextStopIndex += numSamplesPerStat; + while (sampleIndex < nextStopIndex) + { + statsManager.PushSampleForStatistic(statName2, dataSamples[sampleIndex]); + sampleIndex++; + } + nextStopIndex += numSamplesPerStat; + while (sampleIndex < nextStopIndex) + { + statsManager.PushSampleForStatistic(statName3, dataSamples[sampleIndex]); + sampleIndex++; + } + + EXPECT_NEAR(statsManager.GetStatistic(statName0)->GetAverage(), 12.0, 0.001); + EXPECT_NEAR(statsManager.GetStatistic(statName1)->GetAverage(), 37.0, 0.001); + EXPECT_NEAR(statsManager.GetStatistic(statName2)->GetAverage(), 62.0, 0.001); + EXPECT_NEAR(statsManager.GetStatistic(statName3)->GetAverage(), 87.0, 0.001); + + EXPECT_NEAR(statsManager.GetStatistic(statName0)->GetStdev(), 7.359, 0.001); + EXPECT_NEAR(statsManager.GetStatistic(statName1)->GetStdev(), 7.359, 0.001); + EXPECT_NEAR(statsManager.GetStatistic(statName2)->GetStdev(), 7.359, 0.001); + EXPECT_NEAR(statsManager.GetStatistic(statName3)->GetStdev(), 7.359, 0.001); + + //Reset one of the stats. + statsManager.ResetStatistic(statName2); + EXPECT_EQ(statsManager.GetStatistic(statName2)->GetAverage(), 0.0); + //Reset all of the stats. + statsManager.ResetAllStatistics(); + EXPECT_EQ(statsManager.GetStatistic(statName0)->GetNumSamples(), 0); + EXPECT_EQ(statsManager.GetStatistic(statName1)->GetNumSamples(), 0); + EXPECT_EQ(statsManager.GetStatistic(statName2)->GetNumSamples(), 0); + EXPECT_EQ(statsManager.GetStatistic(statName3)->GetNumSamples(), 0); + } + + TEST_F(StatisticsTest, StatisticsManagerCrc32_DistributeSamplesAcrossStatistics_StatisticsAreCorrect) + { + Statistics::StatisticsManager statsManager; + AZ::Crc32 statName0 = AZ_CRC("stat0", 0xb8927780); + AZ::Crc32 statName1 = AZ_CRC("stat1", 0xcf954716); + AZ::Crc32 statName2 = AZ_CRC("stat2", 0x569c16ac); + AZ::Crc32 statName3 = AZ_CRC("stat3", 0x219b263a); + + EXPECT_TRUE(statsManager.AddStatistic(statName3) != nullptr); + EXPECT_TRUE(statsManager.AddStatistic(statName0) != nullptr); + EXPECT_TRUE(statsManager.AddStatistic(statName2) != nullptr); + EXPECT_TRUE(statsManager.AddStatistic(statName1) != nullptr); + + EXPECT_TRUE(statsManager.GetStatistic(statName3) != nullptr); + EXPECT_TRUE(statsManager.GetStatistic(statName0) != nullptr); + EXPECT_TRUE(statsManager.GetStatistic(statName1) != nullptr); + EXPECT_TRUE(statsManager.GetStatistic(statName2) != nullptr); + + //Distribute the 100 samples of data evenly across the 4 running statistics. + ASSERT_TRUE(m_dataSamples.get() != nullptr); + const AZStd::vector& dataSamples = *m_dataSamples; + const size_t numSamples = dataSamples.size(); + const size_t numSamplesPerStat = numSamples / 4; + size_t sampleIndex = 0; + size_t nextStopIndex = numSamplesPerStat; + while (sampleIndex < nextStopIndex) + { + statsManager.PushSampleForStatistic(statName0, dataSamples[sampleIndex]); + sampleIndex++; + } + nextStopIndex += numSamplesPerStat; + while (sampleIndex < nextStopIndex) + { + statsManager.PushSampleForStatistic(statName1, dataSamples[sampleIndex]); + sampleIndex++; + } + nextStopIndex += numSamplesPerStat; + while (sampleIndex < nextStopIndex) + { + statsManager.PushSampleForStatistic(statName2, dataSamples[sampleIndex]); + sampleIndex++; + } + nextStopIndex += numSamplesPerStat; + while (sampleIndex < nextStopIndex) + { + statsManager.PushSampleForStatistic(statName3, dataSamples[sampleIndex]); + sampleIndex++; + } + + EXPECT_NEAR(statsManager.GetStatistic(statName0)->GetAverage(), 12.0, 0.001); + EXPECT_NEAR(statsManager.GetStatistic(statName1)->GetAverage(), 37.0, 0.001); + EXPECT_NEAR(statsManager.GetStatistic(statName2)->GetAverage(), 62.0, 0.001); + EXPECT_NEAR(statsManager.GetStatistic(statName3)->GetAverage(), 87.0, 0.001); + + EXPECT_NEAR(statsManager.GetStatistic(statName0)->GetStdev(), 7.359, 0.001); + EXPECT_NEAR(statsManager.GetStatistic(statName1)->GetStdev(), 7.359, 0.001); + EXPECT_NEAR(statsManager.GetStatistic(statName2)->GetStdev(), 7.359, 0.001); + EXPECT_NEAR(statsManager.GetStatistic(statName3)->GetStdev(), 7.359, 0.001); + + //Reset one of the stats. + statsManager.ResetStatistic(statName2); + EXPECT_EQ(statsManager.GetStatistic(statName2)->GetAverage(), 0.0); + //Reset all of the stats. + statsManager.ResetAllStatistics(); + EXPECT_EQ(statsManager.GetStatistic(statName0)->GetNumSamples(), 0); + EXPECT_EQ(statsManager.GetStatistic(statName1)->GetNumSamples(), 0); + EXPECT_EQ(statsManager.GetStatistic(statName2)->GetNumSamples(), 0); + EXPECT_EQ(statsManager.GetStatistic(statName3)->GetNumSamples(), 0); + } + +}//namespace UnitTest diff --git a/Code/Framework/AzCore/Tests/TimeDataStatistics.cpp b/Code/Framework/AzCore/Tests/TimeDataStatistics.cpp new file mode 100644 index 0000000000..50856b1df8 --- /dev/null +++ b/Code/Framework/AzCore/Tests/TimeDataStatistics.cpp @@ -0,0 +1,207 @@ +/* + * 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 + +using namespace AZ; +using namespace Debug; + +namespace UnitTest +{ + /** + * Validate functionality of the convenience class TimeDataStatisticsManager. + * It is a specialized version of RunningStatisticsManager that works with Timer type + * of registers that can be captured with the FrameProfilerBus::OnFrameProfilerData() + */ + class TimeDataStatisticsManagerTest + : public AllocatorsFixture + , public FrameProfilerBus::Handler + { + static constexpr const char* PARENT_TIMER_STAT = "ParentStat"; + static constexpr const char* CHILD_TIMER_STAT0 = "ChildStat0"; + static constexpr const char* CHILD_TIMER_STAT1 = "ChildStat1"; + + public: + TimeDataStatisticsManagerTest() + : AllocatorsFixture() + { + } + + void SetUp() override + { + AllocatorsFixture::SetUp(); + m_statsManager = AZStd::make_unique(); + } + + void TearDown() override + { + m_statsManager = nullptr; + AllocatorsFixture::TearDown(); + } + + ////////////////////////////////////////////////////////////////////////// + // FrameProfilerBus + virtual void OnFrameProfilerData(const FrameProfiler::ThreadDataArray& data) + { + for (size_t iThread = 0; iThread < data.size(); ++iThread) + { + const FrameProfiler::ThreadData& td = data[iThread]; + FrameProfiler::ThreadData::RegistersMap::const_iterator regIt = td.m_registers.begin(); + for (; regIt != td.m_registers.end(); ++regIt) + { + const FrameProfiler::RegisterData& rd = regIt->second; + u32 unitTestCrc = AZ_CRC("UnitTest", 0x8089cea8); + if (unitTestCrc != rd.m_systemId) + { + continue; //Not for us. + } + ASSERT_EQ(ProfilerRegister::PRT_TIME, rd.m_type); + const FrameProfiler::FrameData& fd = rd.m_frames.back(); + m_statsManager->PushTimeDataSample(rd.m_name, fd.m_timeData); + } + } + } + ////////////////////////////////////////////////////////////////////////// + + int ChildFunction0(int numIterations, int sleepTimeMilliseconds) + { + AZ_PROFILE_TIMER("UnitTest", CHILD_TIMER_STAT0); + AZStd::this_thread::sleep_for(AZStd::chrono::milliseconds(sleepTimeMilliseconds)); + int result = 5; + for (int i = 0; i < numIterations; ++i) + { + result += i % 3; + } + return result; + } + + int ChildFunction1(int numIterations, int sleepTimeMilliseconds) + { + AZ_PROFILE_TIMER("UnitTest", CHILD_TIMER_STAT1); + AZStd::this_thread::sleep_for(AZStd::chrono::milliseconds(sleepTimeMilliseconds)); + int result = 5; + for (int i = 0; i < numIterations; ++i) + { + result += i % 3; + } + return result; + } + + int ParentFunction(int numIterations, int sleepTimeMilliseconds) + { + AZ_PROFILE_TIMER("UnitTest", PARENT_TIMER_STAT); + AZStd::this_thread::sleep_for(AZStd::chrono::milliseconds(sleepTimeMilliseconds)); + int result = 0; + result += ChildFunction0(numIterations, sleepTimeMilliseconds); + result += ChildFunction1(numIterations, sleepTimeMilliseconds); + return result; + } + + void run() + { + Debug::FrameProfilerBus::Handler::BusConnect(); + + ComponentApplication app; + ComponentApplication::Descriptor desc; + desc.m_useExistingAllocator = true; + desc.m_enableDrilling = false; // we already created a memory driller for the test (AllocatorsFixture) + ComponentApplication::StartupParameters startupParams; + startupParams.m_allocator = &AllocatorInstance::Get(); + Entity* systemEntity = app.Create(desc, startupParams); + systemEntity->CreateComponent(); + + systemEntity->Init(); + systemEntity->Activate(); // start frame component + + const int sleepTimeAllFuncsMillis = 1; + const int numIterations = 10; + for (int iterationCounter = 0; iterationCounter < numIterations; ++iterationCounter) + { + ParentFunction(numIterations, sleepTimeAllFuncsMillis); + //Collect all samples. + app.Tick(); + } + + //Verify we have three running stats. + { + AZStd::vector allStats; + m_statsManager->GetAllStatistics(allStats); + EXPECT_EQ(allStats.size(), 3); + } + + AZStd::string parentStatName(PARENT_TIMER_STAT); + AZStd::string child0StatName(CHILD_TIMER_STAT0); + AZStd::string child1StatName(CHILD_TIMER_STAT1); + ASSERT_TRUE(m_statsManager->GetStatistic(parentStatName) != nullptr); + ASSERT_TRUE(m_statsManager->GetStatistic(child0StatName) != nullptr); + ASSERT_TRUE(m_statsManager->GetStatistic(child1StatName) != nullptr); + + EXPECT_EQ(m_statsManager->GetStatistic(parentStatName)->GetNumSamples(), numIterations); + EXPECT_EQ(m_statsManager->GetStatistic(child0StatName)->GetNumSamples(), numIterations); + EXPECT_EQ(m_statsManager->GetStatistic(child1StatName)->GetNumSamples(), numIterations); + + const double minimumExpectDurationOfChildFunctionMicros = 1; + const double minimumExpectDurationOfParentFunctionMicros = 1; + + EXPECT_GE(m_statsManager->GetStatistic(parentStatName)->GetMinimum(), minimumExpectDurationOfParentFunctionMicros); + EXPECT_GE(m_statsManager->GetStatistic(parentStatName)->GetAverage(), minimumExpectDurationOfParentFunctionMicros); + EXPECT_GE(m_statsManager->GetStatistic(parentStatName)->GetMaximum(), minimumExpectDurationOfParentFunctionMicros); + + EXPECT_GE(m_statsManager->GetStatistic(child0StatName)->GetMinimum(), minimumExpectDurationOfChildFunctionMicros); + EXPECT_GE(m_statsManager->GetStatistic(child0StatName)->GetAverage(), minimumExpectDurationOfChildFunctionMicros); + EXPECT_GE(m_statsManager->GetStatistic(child0StatName)->GetMaximum(), minimumExpectDurationOfChildFunctionMicros); + + EXPECT_GE(m_statsManager->GetStatistic(child1StatName)->GetMinimum(), minimumExpectDurationOfChildFunctionMicros); + EXPECT_GE(m_statsManager->GetStatistic(child1StatName)->GetAverage(), minimumExpectDurationOfChildFunctionMicros); + EXPECT_GE(m_statsManager->GetStatistic(child1StatName)->GetMaximum(), minimumExpectDurationOfChildFunctionMicros); + + //Let's validate TimeDataStatisticsManager::RemoveStatistics() + m_statsManager->RemoveStatistic(child1StatName); + ASSERT_TRUE(m_statsManager->GetStatistic(parentStatName) != nullptr); + ASSERT_TRUE(m_statsManager->GetStatistic(child0StatName) != nullptr); + EXPECT_EQ(m_statsManager->GetStatistic(child1StatName), nullptr); + + //Let's store the sample count for both parentStatName and child0StatName. + const AZ::u64 numSamplesParent = m_statsManager->GetStatistic(parentStatName)->GetNumSamples(); + const AZ::u64 numSamplesChild0 = m_statsManager->GetStatistic(child0StatName)->GetNumSamples(); + + //Let's call child1 function again and call app.Tick(). child1StatName should be readded to m_statsManager. + ChildFunction1(numIterations, sleepTimeAllFuncsMillis); + app.Tick(); + ASSERT_TRUE(m_statsManager->GetStatistic(child1StatName) != nullptr); + EXPECT_EQ(m_statsManager->GetStatistic(parentStatName)->GetNumSamples(), numSamplesParent); + EXPECT_EQ(m_statsManager->GetStatistic(child0StatName)->GetNumSamples(), numSamplesChild0); + EXPECT_EQ(m_statsManager->GetStatistic(child1StatName)->GetNumSamples(), 1); + + Debug::FrameProfilerBus::Handler::BusDisconnect(); + app.Destroy(); + } + + AZStd::unique_ptr m_statsManager; + };//class TimeDataStatisticsManagerTest + + TEST_F(TimeDataStatisticsManagerTest, Test) + { + run(); + } + //End of all Tests of TimeDataStatisticsManagerTest + +}//namespace UnitTest diff --git a/Code/Framework/AzCore/Tests/azcoretests_files.cmake b/Code/Framework/AzCore/Tests/azcoretests_files.cmake index 7833cf97bb..911eaa7b10 100644 --- a/Code/Framework/AzCore/Tests/azcoretests_files.cmake +++ b/Code/Framework/AzCore/Tests/azcoretests_files.cmake @@ -60,11 +60,13 @@ set(FILES SerializeContextFixture.h Slice.cpp State.cpp + Statistics.cpp StreamerTests.cpp StringFunc.cpp SystemFile.cpp TaskTests.cpp TickBusTest.cpp + TimeDataStatistics.cpp UUIDTests.cpp XML.cpp Debug/AssetTracking.cpp diff --git a/Gems/Atom/Utils/Code/Include/Atom/Utils/StableDynamicArray.h b/Gems/Atom/Utils/Code/Include/Atom/Utils/StableDynamicArray.h index e73f641b96..454a6d4086 100644 --- a/Gems/Atom/Utils/Code/Include/Atom/Utils/StableDynamicArray.h +++ b/Gems/Atom/Utils/Code/Include/Atom/Utils/StableDynamicArray.h @@ -130,7 +130,6 @@ namespace AZ template struct StableDynamicArray::Page { - static constexpr size_t PageSize = ElementsPerPage * sizeof(T); static constexpr size_t InvalidPage = -1; static constexpr uint64_t FullBits = 0xFFFFFFFFFFFFFFFFull; static constexpr size_t NumUint64_t = ElementsPerPage / 64;