[development] promoted Atom CPU profiler into its own gem (#4742)
Extracted the CPU data and visualization systems out of Atom and turned them into a separate gem NOTES: - A large portion of this change is code restructuring. The only changes made to such code was the minimum necessary for it to work in the new location. - The *CpuProfiler* files shown below as deletes/adds were originally moves (seemonroegm-disable-blank-issue-26946145), my guess is the namespace/formatting change ind193dc4is the culprit for this. - There is one minor outstanding issue post move which is related to how the live CPU frame graph is displaying in the ImGui window. All the data is there and correct but the frame boundary is anchored to the wrong side (left instead of right). Signed-off-by: AMZN-ScottR 24445312+AMZN-ScottR@users.noreply.github.com
commit
10b175e453
@ -1,87 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) Contributors to the Open 3D Engine Project.
|
||||
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
*
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AzCore/Debug/EventTrace.h>
|
||||
#include <AzCore/Debug/Profiler.h>
|
||||
#include <AzCore/RTTI/RTTI.h>
|
||||
#include <AzCore/std/containers/ring_buffer.h>
|
||||
#include <AzCore/std/containers/unordered_map.h>
|
||||
#include <AzCore/std/string/string.h>
|
||||
|
||||
namespace AZ
|
||||
{
|
||||
namespace RHI
|
||||
{
|
||||
//! Structure that is used to cache a timed region into the thread's local storage.
|
||||
struct CachedTimeRegion
|
||||
{
|
||||
//! Structure used internally for caching assumed global string pointers (ideally literals) to the marker group/region
|
||||
//! NOTE: When used in a separate shared library, the library mustn't be unloaded before the CpuProfiler is shutdown.
|
||||
struct GroupRegionName
|
||||
{
|
||||
GroupRegionName() = delete;
|
||||
GroupRegionName(const char* const group, const char* const region);
|
||||
|
||||
const char* m_groupName = nullptr;
|
||||
const char* m_regionName = nullptr;
|
||||
|
||||
struct Hash
|
||||
{
|
||||
AZStd::size_t operator()(const GroupRegionName& name) const;
|
||||
};
|
||||
bool operator==(const GroupRegionName& other) const;
|
||||
};
|
||||
|
||||
CachedTimeRegion() = default;
|
||||
CachedTimeRegion(const GroupRegionName& groupRegionName);
|
||||
CachedTimeRegion(const GroupRegionName& groupRegionName, uint16_t stackDepth, uint64_t startTick, uint64_t endTick);
|
||||
|
||||
GroupRegionName m_groupRegionName{nullptr, nullptr};
|
||||
|
||||
uint16_t m_stackDepth = 0u;
|
||||
AZStd::sys_time_t m_startTick = 0;
|
||||
AZStd::sys_time_t m_endTick = 0;
|
||||
};
|
||||
|
||||
//! Interface class of the CpuProfiler
|
||||
class CpuProfiler
|
||||
{
|
||||
public:
|
||||
using ThreadTimeRegionMap = AZStd::unordered_map<AZStd::string, AZStd::vector<CachedTimeRegion>>;
|
||||
using TimeRegionMap = AZStd::unordered_map<AZStd::thread_id, ThreadTimeRegionMap>;
|
||||
|
||||
AZ_RTTI(CpuProfiler, "{127C1D0B-BE05-4E18-A8F6-24F3EED2ECA6}");
|
||||
|
||||
CpuProfiler() = default;
|
||||
virtual ~CpuProfiler() = default;
|
||||
|
||||
AZ_DISABLE_COPY_MOVE(CpuProfiler);
|
||||
|
||||
static CpuProfiler* Get();
|
||||
|
||||
//! Get the last frame's TimeRegionMap
|
||||
virtual const TimeRegionMap& GetTimeRegionMap() const = 0;
|
||||
|
||||
//! Begin a continuous capture. Blocks the profiler from being toggled off until EndContinuousCapture is called.
|
||||
[[nodiscard]] virtual bool BeginContinuousCapture() = 0;
|
||||
|
||||
//! Flush the CPU Profiler's saved data into the passed ring buffer .
|
||||
[[nodiscard]] virtual bool EndContinuousCapture(AZStd::ring_buffer<TimeRegionMap>& flushTarget) = 0;
|
||||
|
||||
virtual bool IsContinuousCaptureInProgress() const = 0;
|
||||
|
||||
//! Enable/Disable the CpuProfiler
|
||||
virtual void SetProfilerEnabled(bool enabled) = 0;
|
||||
|
||||
virtual bool IsProfilerEnabled() const = 0 ;
|
||||
};
|
||||
|
||||
} // namespace RPI
|
||||
} // namespace AZ
|
||||
@ -1,188 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) Contributors to the Open 3D Engine Project.
|
||||
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
*
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <Atom/RHI/CpuProfiler.h>
|
||||
#include <Atom/RHI.Reflect/Base.h>
|
||||
|
||||
#include <AzCore/Component/TickBus.h>
|
||||
#include <AzCore/Memory/OSAllocator.h>
|
||||
#include <AzCore/Name/Name.h>
|
||||
#include <AzCore/std/containers/map.h>
|
||||
#include <AzCore/std/containers/unordered_set.h>
|
||||
#include <AzCore/std/parallel/mutex.h>
|
||||
#include <AzCore/std/parallel/shared_mutex.h>
|
||||
#include <AzCore/std/smart_ptr/intrusive_refcount.h>
|
||||
|
||||
|
||||
namespace AZ
|
||||
{
|
||||
namespace RHI
|
||||
{
|
||||
//! Thread local class to keep track of the thread's cached time regions.
|
||||
//! Each thread keeps track of its own time regions, which is communicated from the CpuProfilerImpl.
|
||||
//! The CpuProfilerImpl is able to request the cached time regions from the CpuTimingLocalStorage.
|
||||
class CpuTimingLocalStorage :
|
||||
public AZStd::intrusive_refcount<AZStd::atomic_uint>
|
||||
{
|
||||
friend class CpuProfilerImpl;
|
||||
|
||||
public:
|
||||
AZ_CLASS_ALLOCATOR(CpuTimingLocalStorage, AZ::OSAllocator, 0);
|
||||
|
||||
CpuTimingLocalStorage();
|
||||
~CpuTimingLocalStorage();
|
||||
|
||||
private:
|
||||
// Maximum stack size
|
||||
static constexpr uint32_t TimeRegionStackSize = 2048u;
|
||||
|
||||
// Adds a region to the stack, gets called each time a region begins
|
||||
void RegionStackPushBack(CachedTimeRegion& timeRegion);
|
||||
|
||||
// Pops a region from the stack, gets called each time a region ends
|
||||
void RegionStackPopBack();
|
||||
|
||||
// Add a new cached time region. If the stack is empty, flush all entries to the cached map
|
||||
void AddCachedRegion(const CachedTimeRegion& timeRegionCached);
|
||||
|
||||
// Tries to flush the map to the passed parameter, only if the thread's mutex is unlocked
|
||||
void TryFlushCachedMap(CpuProfiler::ThreadTimeRegionMap& cachedRegionMap);
|
||||
|
||||
AZStd::thread_id m_executingThreadId;
|
||||
// Keeps track of the current thread's stack depth
|
||||
uint32_t m_stackLevel = 0u;
|
||||
|
||||
// Cached region map, will be flushed to the system's map when the system requests it
|
||||
CpuProfiler::ThreadTimeRegionMap m_cachedTimeRegionMap;
|
||||
|
||||
// Use fixed vectors to avoid re-allocating new elements
|
||||
// Keeps track of the regions that added and removed using the macro
|
||||
AZStd::fixed_vector<CachedTimeRegion, TimeRegionStackSize> m_timeRegionStack;
|
||||
|
||||
// Keeps track of regions that completed (i.e regions that was pushed and popped from the stack)
|
||||
// Intermediate storage point for the CachedTimeRegions, when the stack is empty, all entries will be
|
||||
// copied to the map.
|
||||
AZStd::fixed_vector<CachedTimeRegion, TimeRegionStackSize> m_cachedTimeRegions;
|
||||
AZStd::mutex m_cachedTimeRegionMutex;
|
||||
|
||||
// Dirty flag which is set when the CpuProfiler's enabled state is set from false to true
|
||||
AZStd::atomic_bool m_clearContainers = false;
|
||||
|
||||
// When the thread is terminated, it will flag itself for deletion
|
||||
AZStd::atomic_bool m_deleteFlag = false;
|
||||
|
||||
// Keep track of the regions that have hit the size limit so we don't have to lock to check
|
||||
AZStd::map<AZStd::string, bool> m_hitSizeLimitMap;
|
||||
};
|
||||
|
||||
//! CpuProfiler will keep track of the registered threads, and
|
||||
//! forwards the request to profile a region to the appropriate thread. The user is able to request all
|
||||
//! cached regions, which are stored on a per thread frequency.
|
||||
class CpuProfilerImpl final
|
||||
: public AZ::Debug::Profiler
|
||||
, public CpuProfiler
|
||||
, public SystemTickBus::Handler
|
||||
{
|
||||
friend class CpuTimingLocalStorage;
|
||||
|
||||
public:
|
||||
AZ_TYPE_INFO(CpuProfilerImpl, "{10E9D394-FC83-4B45-B2B8-807C6BF07BF0}");
|
||||
AZ_CLASS_ALLOCATOR(CpuProfilerImpl, AZ::OSAllocator, 0);
|
||||
|
||||
CpuProfilerImpl() = default;
|
||||
~CpuProfilerImpl() = default;
|
||||
|
||||
//! Registers the CpuProfilerImpl instance to the interface
|
||||
void Init();
|
||||
//! Unregisters the CpuProfilerImpl instance from the interface
|
||||
void Shutdown();
|
||||
|
||||
// SystemTickBus::Handler overrides
|
||||
// When fired, the profiler collects all profiling data from registered threads and updates
|
||||
// m_timeRegionMap so that the next frame has up-to-date profiling data.
|
||||
void OnSystemTick() final override;
|
||||
|
||||
//! AZ::Debug::Profiler overrides...
|
||||
void BeginRegion(const AZ::Debug::Budget* budget, const char* eventName) final override;
|
||||
void EndRegion(const AZ::Debug::Budget* budget) final override;
|
||||
|
||||
//! CpuProfiler overrides...
|
||||
const TimeRegionMap& GetTimeRegionMap() const final override;
|
||||
bool BeginContinuousCapture() final override;
|
||||
bool EndContinuousCapture(AZStd::ring_buffer<TimeRegionMap>& flushTarget) final override;
|
||||
bool IsContinuousCaptureInProgress() const final override;
|
||||
void SetProfilerEnabled(bool enabled) final override;
|
||||
bool IsProfilerEnabled() const final override;
|
||||
|
||||
private:
|
||||
static constexpr AZStd::size_t MaxFramesToSave = 2 * 60 * 120; // 2 minutes of 120fps
|
||||
static constexpr AZStd::size_t MaxRegionStringPoolSize = 16384; // Max amount of unique strings to save in the pool before throwing warnings.
|
||||
|
||||
// Lazily create and register the local thread data
|
||||
void RegisterThreadStorage();
|
||||
|
||||
// ThreadId -> ThreadTimeRegionMap
|
||||
// On the start of each frame, this map will be updated with the last frame's profiling data.
|
||||
TimeRegionMap m_timeRegionMap;
|
||||
|
||||
// Set of registered threads when created
|
||||
AZStd::vector<RHI::Ptr<CpuTimingLocalStorage>, AZ::OSStdAllocator> m_registeredThreads;
|
||||
AZStd::mutex m_threadRegisterMutex;
|
||||
|
||||
// Thread local storage, gets lazily allocated when a thread is created
|
||||
static thread_local CpuTimingLocalStorage* ms_threadLocalStorage;
|
||||
|
||||
// Enable/Disables the threads from profiling
|
||||
AZStd::atomic_bool m_enabled = false;
|
||||
|
||||
// This lock will only be contested when the CpuProfiler's Shutdown() method has been called
|
||||
AZStd::shared_mutex m_shutdownMutex;
|
||||
|
||||
bool m_initialized = false;
|
||||
|
||||
AZStd::mutex m_continuousCaptureEndingMutex;
|
||||
|
||||
AZStd::atomic_bool m_continuousCaptureInProgress;
|
||||
|
||||
// Stores multiple frames of profiling data, size is controlled by MaxFramesToSave. Flushed when EndContinuousCapture is called.
|
||||
// Ring buffer so that we can have fast append of new data + removal of old profiling data with good cache locality.
|
||||
AZStd::ring_buffer<TimeRegionMap> m_continuousCaptureData;
|
||||
};
|
||||
|
||||
// Intermediate class to serialize Cpu TimedRegion data.
|
||||
class CpuProfilingStatisticsSerializer
|
||||
{
|
||||
public:
|
||||
class CpuProfilingStatisticsSerializerEntry
|
||||
{
|
||||
public:
|
||||
AZ_TYPE_INFO(CpuProfilingStatisticsSerializer::CpuProfilingStatisticsSerializerEntry, "{26B78F65-EB96-46E2-BE7E-A1233880B225}");
|
||||
static void Reflect(AZ::ReflectContext* context);
|
||||
|
||||
CpuProfilingStatisticsSerializerEntry() = default;
|
||||
CpuProfilingStatisticsSerializerEntry(const RHI::CachedTimeRegion& cachedTimeRegion, AZStd::thread_id threadId);
|
||||
|
||||
Name m_groupName;
|
||||
Name m_regionName;
|
||||
uint16_t m_stackDepth;
|
||||
AZStd::sys_time_t m_startTick;
|
||||
AZStd::sys_time_t m_endTick;
|
||||
size_t m_threadId;
|
||||
};
|
||||
|
||||
AZ_TYPE_INFO(CpuProfilingStatisticsSerializer, "{D5B02946-0D27-474F-9A44-364C2706DD41}");
|
||||
static void Reflect(AZ::ReflectContext* context);
|
||||
|
||||
CpuProfilingStatisticsSerializer() = default;
|
||||
CpuProfilingStatisticsSerializer(const AZStd::ring_buffer<RHI::CpuProfiler::TimeRegionMap>& continuousData);
|
||||
|
||||
AZStd::vector<CpuProfilingStatisticsSerializerEntry> m_cpuProfilingStatisticsSerializerEntries;
|
||||
};
|
||||
}; // namespace RHI
|
||||
}; // namespace AZ
|
||||
@ -1,448 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) Contributors to the Open 3D Engine Project.
|
||||
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
*
|
||||
*/
|
||||
|
||||
#include <Atom/RHI/CpuProfilerImpl.h>
|
||||
|
||||
#include <AzCore/Interface/Interface.h>
|
||||
#include <AzCore/std/smart_ptr/shared_ptr.h>
|
||||
|
||||
#include <AzCore/Debug/Timer.h>
|
||||
#include <AzCore/Statistics/StatisticalProfilerProxy.h>
|
||||
#include <Atom/RHI/RHIUtils.h>
|
||||
|
||||
namespace AZ
|
||||
{
|
||||
namespace RHI
|
||||
{
|
||||
thread_local CpuTimingLocalStorage* CpuProfilerImpl::ms_threadLocalStorage = nullptr;
|
||||
|
||||
// --- CpuProfiler ---
|
||||
|
||||
CpuProfiler* CpuProfiler::Get()
|
||||
{
|
||||
return Interface<CpuProfiler>::Get();
|
||||
}
|
||||
|
||||
// --- CachedTimeRegion ---
|
||||
|
||||
CachedTimeRegion::CachedTimeRegion(const GroupRegionName& groupRegionName)
|
||||
{
|
||||
m_groupRegionName = groupRegionName;
|
||||
}
|
||||
|
||||
CachedTimeRegion::CachedTimeRegion(const GroupRegionName& groupRegionName, uint16_t stackDepth, uint64_t startTick, uint64_t endTick)
|
||||
{
|
||||
m_groupRegionName = groupRegionName;
|
||||
m_stackDepth = stackDepth;
|
||||
m_startTick = startTick;
|
||||
m_endTick = endTick;
|
||||
}
|
||||
|
||||
// --- GroupRegionName ---
|
||||
|
||||
CachedTimeRegion::GroupRegionName::GroupRegionName(const char* const group, const char* const region) :
|
||||
m_groupName(group),
|
||||
m_regionName(region)
|
||||
{
|
||||
}
|
||||
|
||||
AZStd::size_t CachedTimeRegion::GroupRegionName::Hash::operator()(const CachedTimeRegion::GroupRegionName& name) const
|
||||
{
|
||||
AZStd::size_t seed = 0;
|
||||
AZStd::hash_combine(seed, name.m_groupName);
|
||||
AZStd::hash_combine(seed, name.m_regionName);
|
||||
return seed;
|
||||
}
|
||||
|
||||
bool CachedTimeRegion::GroupRegionName::operator==(const GroupRegionName& other) const
|
||||
{
|
||||
return (m_groupName == other.m_groupName) && (m_regionName == other.m_regionName);
|
||||
}
|
||||
|
||||
|
||||
// --- CpuProfilerImpl ---
|
||||
|
||||
void CpuProfilerImpl::Init()
|
||||
{
|
||||
Interface<AZ::Debug::Profiler>::Register(this);
|
||||
Interface<CpuProfiler>::Register(this);
|
||||
m_initialized = true;
|
||||
SystemTickBus::Handler::BusConnect();
|
||||
m_continuousCaptureData.set_capacity(10);
|
||||
|
||||
if (auto statsProfiler = AZ::Interface<AZ::Statistics::StatisticalProfilerProxy>::Get(); statsProfiler)
|
||||
{
|
||||
statsProfiler->ActivateProfiler(AZ_CRC_CE("RHI"), true);
|
||||
}
|
||||
}
|
||||
|
||||
void CpuProfilerImpl::Shutdown()
|
||||
{
|
||||
if (!m_initialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
// When this call is made, no more thread profiling calls can be performed anymore
|
||||
Interface<CpuProfiler>::Unregister(this);
|
||||
Interface<AZ::Debug::Profiler>::Unregister(this);
|
||||
|
||||
// Wait for the remaining threads that might still be processing its profiling calls
|
||||
AZStd::unique_lock<AZStd::shared_mutex> shutdownLock(m_shutdownMutex);
|
||||
|
||||
m_enabled = false;
|
||||
|
||||
// Cleanup all TLS
|
||||
m_registeredThreads.clear();
|
||||
m_timeRegionMap.clear();
|
||||
m_initialized = false;
|
||||
m_continuousCaptureInProgress.store(false);
|
||||
m_continuousCaptureData.clear();
|
||||
SystemTickBus::Handler::BusDisconnect();
|
||||
}
|
||||
|
||||
void CpuProfilerImpl::BeginRegion(const AZ::Debug::Budget* budget, const char* eventName)
|
||||
{
|
||||
// Try to lock here, the shutdownMutex will only be contested when the CpuProfiler is shutting down.
|
||||
if (m_shutdownMutex.try_lock_shared())
|
||||
{
|
||||
if (m_enabled)
|
||||
{
|
||||
// Lazy initialization, creates an instance of the Thread local data if it's not created, and registers it
|
||||
RegisterThreadStorage();
|
||||
|
||||
// Push it to the stack
|
||||
CachedTimeRegion timeRegion({budget->Name(), eventName});
|
||||
ms_threadLocalStorage->RegionStackPushBack(timeRegion);
|
||||
}
|
||||
|
||||
m_shutdownMutex.unlock_shared();
|
||||
}
|
||||
}
|
||||
|
||||
void CpuProfilerImpl::EndRegion([[maybe_unused]] const AZ::Debug::Budget* budget)
|
||||
{
|
||||
// Try to lock here, the shutdownMutex will only be contested when the CpuProfiler is shutting down.
|
||||
if (m_shutdownMutex.try_lock_shared())
|
||||
{
|
||||
// guard against enabling mid-marker
|
||||
if (m_enabled && ms_threadLocalStorage != nullptr)
|
||||
{
|
||||
ms_threadLocalStorage->RegionStackPopBack();
|
||||
}
|
||||
|
||||
m_shutdownMutex.unlock_shared();
|
||||
}
|
||||
}
|
||||
|
||||
const CpuProfiler::TimeRegionMap& CpuProfilerImpl::GetTimeRegionMap() const
|
||||
{
|
||||
return m_timeRegionMap;
|
||||
}
|
||||
|
||||
bool CpuProfilerImpl::BeginContinuousCapture()
|
||||
{
|
||||
bool expected = false;
|
||||
if (m_continuousCaptureInProgress.compare_exchange_strong(expected, true))
|
||||
{
|
||||
m_enabled = true;
|
||||
AZ_TracePrintf("Profiler", "Continuous capture started\n");
|
||||
return true;
|
||||
}
|
||||
|
||||
AZ_TracePrintf("Profiler", "Attempting to start a continuous capture while one already in progress");
|
||||
return false;
|
||||
}
|
||||
|
||||
bool CpuProfilerImpl::EndContinuousCapture(AZStd::ring_buffer<TimeRegionMap>& flushTarget)
|
||||
{
|
||||
if (!m_continuousCaptureInProgress.load())
|
||||
{
|
||||
AZ_TracePrintf("Profiler", "Attempting to end a continuous capture while one not in progress");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (m_continuousCaptureEndingMutex.try_lock())
|
||||
{
|
||||
m_enabled = false;
|
||||
flushTarget = AZStd::move(m_continuousCaptureData);
|
||||
m_continuousCaptureData.clear();
|
||||
AZ_TracePrintf("Profiler", "Continuous capture ended\n");
|
||||
m_continuousCaptureInProgress.store(false);
|
||||
|
||||
m_continuousCaptureEndingMutex.unlock();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool CpuProfilerImpl::IsContinuousCaptureInProgress() const
|
||||
{
|
||||
return m_continuousCaptureInProgress.load();
|
||||
}
|
||||
|
||||
void CpuProfilerImpl::SetProfilerEnabled(bool enabled)
|
||||
{
|
||||
AZStd::unique_lock<AZStd::mutex> lock(m_threadRegisterMutex);
|
||||
|
||||
// Early out if the state is already the same or a continuous capture is in progress
|
||||
if (m_enabled == enabled || m_continuousCaptureInProgress.load())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Set the dirty flag in all the TLS to clear the caches
|
||||
if (enabled)
|
||||
{
|
||||
// Iterate through all the threads, and set the clearing flag
|
||||
for (auto& threadLocal : m_registeredThreads)
|
||||
{
|
||||
threadLocal->m_clearContainers = true;
|
||||
}
|
||||
|
||||
m_enabled = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
m_enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
bool CpuProfilerImpl::IsProfilerEnabled() const
|
||||
{
|
||||
return m_enabled;
|
||||
}
|
||||
|
||||
void CpuProfilerImpl::OnSystemTick()
|
||||
{
|
||||
if (!m_enabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_continuousCaptureInProgress.load() && m_continuousCaptureEndingMutex.try_lock())
|
||||
{
|
||||
if (m_continuousCaptureData.full() && m_continuousCaptureData.size() != MaxFramesToSave)
|
||||
{
|
||||
const AZStd::size_t size = m_continuousCaptureData.size();
|
||||
m_continuousCaptureData.set_capacity(AZStd::min(MaxFramesToSave, size + size / 2));
|
||||
}
|
||||
|
||||
m_continuousCaptureData.push_back(AZStd::move(m_timeRegionMap));
|
||||
m_timeRegionMap.clear();
|
||||
m_continuousCaptureEndingMutex.unlock();
|
||||
}
|
||||
|
||||
AZStd::unique_lock<AZStd::mutex> lock(m_threadRegisterMutex);
|
||||
|
||||
// Iterate through all the threads, and collect the thread's cached time regions
|
||||
TimeRegionMap newMap;
|
||||
for (auto& threadLocal : m_registeredThreads)
|
||||
{
|
||||
ThreadTimeRegionMap& threadMapEntry = newMap[threadLocal->m_executingThreadId];
|
||||
threadLocal->TryFlushCachedMap(threadMapEntry);
|
||||
}
|
||||
|
||||
// Clear all TLS that flagged themselves to be deleted, meaning that the thread is already terminated
|
||||
AZStd::remove_if(m_registeredThreads.begin(), m_registeredThreads.end(), [](const RHI::Ptr<CpuTimingLocalStorage>& thread)
|
||||
{
|
||||
return thread->m_deleteFlag.load();
|
||||
});
|
||||
|
||||
// Update our saved time regions to the last frame's collected data
|
||||
m_timeRegionMap = AZStd::move(newMap);
|
||||
}
|
||||
|
||||
void CpuProfilerImpl::RegisterThreadStorage()
|
||||
{
|
||||
AZStd::unique_lock<AZStd::mutex> lock(m_threadRegisterMutex);
|
||||
if (!ms_threadLocalStorage)
|
||||
{
|
||||
ms_threadLocalStorage = aznew CpuTimingLocalStorage();
|
||||
m_registeredThreads.emplace_back(ms_threadLocalStorage);
|
||||
}
|
||||
}
|
||||
|
||||
// --- CpuTimingLocalStorage ---
|
||||
|
||||
CpuTimingLocalStorage::CpuTimingLocalStorage()
|
||||
{
|
||||
m_executingThreadId = AZStd::this_thread::get_id();
|
||||
}
|
||||
|
||||
CpuTimingLocalStorage::~CpuTimingLocalStorage()
|
||||
{
|
||||
m_deleteFlag = true;
|
||||
}
|
||||
|
||||
void CpuTimingLocalStorage::RegionStackPushBack(CachedTimeRegion& timeRegion)
|
||||
{
|
||||
// If it was (re)enabled, clear the lists first
|
||||
if (m_clearContainers)
|
||||
{
|
||||
m_clearContainers = false;
|
||||
|
||||
m_stackLevel = 0;
|
||||
m_cachedTimeRegionMap.clear();
|
||||
m_timeRegionStack.clear();
|
||||
m_cachedTimeRegions.clear();
|
||||
}
|
||||
|
||||
timeRegion.m_stackDepth = static_cast<uint16_t>(m_stackLevel);
|
||||
|
||||
AZ_Assert(m_timeRegionStack.size() < TimeRegionStackSize, "Adding too many time regions to the stack. Increase the size of TimeRegionStackSize.");
|
||||
m_timeRegionStack.push_back(timeRegion);
|
||||
|
||||
// Increment the stack
|
||||
m_stackLevel++;
|
||||
|
||||
// Set the starting time at the end, to avoid recording the minor overhead
|
||||
m_timeRegionStack.back().m_startTick = AZStd::GetTimeNowTicks();
|
||||
}
|
||||
|
||||
void CpuTimingLocalStorage::RegionStackPopBack()
|
||||
{
|
||||
// Early out when the stack is empty, this might happen when the profiler was enabled while the thread encountered profiling markers
|
||||
if (m_timeRegionStack.empty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the end timestamp here, to avoid the minor overhead
|
||||
const AZStd::sys_time_t endRegionTime = AZStd::GetTimeNowTicks();
|
||||
|
||||
AZ_Assert(!m_timeRegionStack.empty(), "Trying to pop an element in the stack, but it's empty.");
|
||||
CachedTimeRegion back = m_timeRegionStack.back();
|
||||
m_timeRegionStack.pop_back();
|
||||
|
||||
// Set the ending time
|
||||
back.m_endTick = endRegionTime;
|
||||
|
||||
// Decrement the stack
|
||||
m_stackLevel--;
|
||||
|
||||
// Add an entry to the cached region
|
||||
AddCachedRegion(back);
|
||||
}
|
||||
|
||||
// Gets called when region ends and all data is set
|
||||
void CpuTimingLocalStorage::AddCachedRegion(const CachedTimeRegion& timeRegionCached)
|
||||
{
|
||||
if (m_hitSizeLimitMap[timeRegionCached.m_groupRegionName.m_regionName])
|
||||
{
|
||||
return;
|
||||
}
|
||||
// Add an entry to the cached region
|
||||
m_cachedTimeRegions.push_back(timeRegionCached);
|
||||
|
||||
// If the stack is empty, add it to the local cache map. Only gets called when the stack is empty
|
||||
// NOTE: this is where the largest overhead will be, but due to it only being called when the stack is empty
|
||||
// (i.e when the root region ended), this overhead won't affect any time regions.
|
||||
// The exception being for functions that are being profiled and create/spawn threads that are also profiled. Unfortunately, in this
|
||||
// case, the overhead of the profiled threads will be added to the main thread.
|
||||
if (m_timeRegionStack.empty())
|
||||
{
|
||||
AZStd::unique_lock<AZStd::mutex> lock(m_cachedTimeRegionMutex);
|
||||
|
||||
// Add the cached regions to the map
|
||||
for (auto& cachedTimeRegion : m_cachedTimeRegions)
|
||||
{
|
||||
const AZStd::string regionName = cachedTimeRegion.m_groupRegionName.m_regionName;
|
||||
AZStd::vector<CachedTimeRegion>& regionVec = m_cachedTimeRegionMap[regionName];
|
||||
regionVec.push_back(cachedTimeRegion);
|
||||
if (regionVec.size() >= TimeRegionStackSize)
|
||||
{
|
||||
m_hitSizeLimitMap.insert_or_assign(AZStd::move(regionName), true);
|
||||
}
|
||||
}
|
||||
|
||||
// Clear the cached regions
|
||||
m_cachedTimeRegions.clear();
|
||||
}
|
||||
}
|
||||
|
||||
void CpuTimingLocalStorage::TryFlushCachedMap(CpuProfiler::ThreadTimeRegionMap& cachedTimeRegionMap)
|
||||
{
|
||||
// Try to lock, if it's already in use (the cached regions in the array are being copied to the map)
|
||||
// it'll show up in the next iteration when the user requests it.
|
||||
if (m_cachedTimeRegionMutex.try_lock())
|
||||
{
|
||||
// Only flush cached time regions if there are entries available
|
||||
if (!m_cachedTimeRegionMap.empty())
|
||||
{
|
||||
cachedTimeRegionMap = AZStd::move(m_cachedTimeRegionMap);
|
||||
m_cachedTimeRegionMap.clear();
|
||||
m_hitSizeLimitMap.clear();
|
||||
}
|
||||
m_cachedTimeRegionMutex.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
// --- CpuProfilingStatisticsSerializer ---
|
||||
|
||||
CpuProfilingStatisticsSerializer::CpuProfilingStatisticsSerializer(const AZStd::ring_buffer<RHI::CpuProfiler::TimeRegionMap>& continuousData)
|
||||
{
|
||||
// Create serializable entries
|
||||
for (const auto& timeRegionMap : continuousData)
|
||||
{
|
||||
for (const auto& [threadId, regionMap] : timeRegionMap)
|
||||
{
|
||||
for (const auto& [regionName, regionVec] : regionMap)
|
||||
{
|
||||
for (const auto& region : regionVec)
|
||||
{
|
||||
m_cpuProfilingStatisticsSerializerEntries.emplace_back(region, threadId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void CpuProfilingStatisticsSerializer::Reflect(AZ::ReflectContext* context)
|
||||
{
|
||||
if (auto* serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
|
||||
{
|
||||
serializeContext->Class<CpuProfilingStatisticsSerializer>()
|
||||
->Version(1)
|
||||
->Field("cpuProfilingStatisticsSerializerEntries", &CpuProfilingStatisticsSerializer::m_cpuProfilingStatisticsSerializerEntries)
|
||||
;
|
||||
}
|
||||
|
||||
CpuProfilingStatisticsSerializerEntry::Reflect(context);
|
||||
}
|
||||
|
||||
// --- CpuProfilingStatisticsSerializerEntry ---
|
||||
|
||||
CpuProfilingStatisticsSerializer::CpuProfilingStatisticsSerializerEntry::CpuProfilingStatisticsSerializerEntry(
|
||||
const RHI::CachedTimeRegion& cachedTimeRegion, AZStd::thread_id threadId)
|
||||
{
|
||||
m_groupName = cachedTimeRegion.m_groupRegionName.m_groupName;
|
||||
m_regionName = cachedTimeRegion.m_groupRegionName.m_regionName;
|
||||
m_stackDepth = cachedTimeRegion.m_stackDepth;
|
||||
m_startTick = cachedTimeRegion.m_startTick;
|
||||
m_endTick = cachedTimeRegion.m_endTick;
|
||||
m_threadId = AZStd::hash<AZStd::thread_id>{}(threadId);
|
||||
}
|
||||
|
||||
void CpuProfilingStatisticsSerializer::CpuProfilingStatisticsSerializerEntry::Reflect(AZ::ReflectContext* context)
|
||||
{
|
||||
if (auto* serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
|
||||
{
|
||||
serializeContext->Class<CpuProfilingStatisticsSerializerEntry>()
|
||||
->Version(1)
|
||||
->Field("groupName", &CpuProfilingStatisticsSerializerEntry::m_groupName)
|
||||
->Field("regionName", &CpuProfilingStatisticsSerializerEntry::m_regionName)
|
||||
->Field("stackDepth", &CpuProfilingStatisticsSerializerEntry::m_stackDepth)
|
||||
->Field("startTick", &CpuProfilingStatisticsSerializerEntry::m_startTick)
|
||||
->Field("endTick", &CpuProfilingStatisticsSerializerEntry::m_endTick)
|
||||
->Field("threadId", &CpuProfilingStatisticsSerializerEntry::m_threadId)
|
||||
;
|
||||
}
|
||||
}
|
||||
} // namespace RHI
|
||||
} // namespace AZ
|
||||
@ -1,231 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) Contributors to the Open 3D Engine Project.
|
||||
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
*
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AzCore/Component/TickBus.h>
|
||||
#include <AzCore/IO/Path/Path.h>
|
||||
#include <AzCore/Math/Random.h>
|
||||
|
||||
#include <Atom/RHI/CpuProfiler.h>
|
||||
|
||||
|
||||
namespace AZ
|
||||
{
|
||||
namespace Render
|
||||
{
|
||||
//! Stores all the data associated with a row in the table.
|
||||
struct TableRow
|
||||
{
|
||||
template <typename T>
|
||||
struct TableRowCompareFunctor
|
||||
{
|
||||
TableRowCompareFunctor(T memberPointer, bool isAscending) : m_memberPointer(memberPointer), m_ascending(isAscending){};
|
||||
|
||||
bool operator()(const TableRow* lhs, const TableRow* rhs)
|
||||
{
|
||||
return m_ascending ? lhs->*m_memberPointer < rhs->*m_memberPointer : lhs->*m_memberPointer > rhs->*m_memberPointer;
|
||||
}
|
||||
|
||||
T m_memberPointer;
|
||||
bool m_ascending;
|
||||
};
|
||||
|
||||
// Update running statistics with new region data
|
||||
void RecordRegion(const AZ::RHI::CachedTimeRegion& region, size_t threadId);
|
||||
|
||||
void ResetPerFrameStatistics();
|
||||
|
||||
// Get a string of all threads that this region executed in during the last frame
|
||||
AZStd::string GetExecutingThreadsLabel() const;
|
||||
|
||||
AZStd::string m_groupName;
|
||||
AZStd::string m_regionName;
|
||||
|
||||
// --- Per frame statistics ---
|
||||
|
||||
u64 m_invocationsLastFrame = 0;
|
||||
|
||||
// NOTE: set over unordered_set so the threads can be shown in increasing order in tooltip.
|
||||
AZStd::set<size_t> m_executingThreads;
|
||||
|
||||
AZStd::sys_time_t m_lastFrameTotalTicks = 0;
|
||||
|
||||
// Maximum execution time of a region in the last frame.
|
||||
AZStd::sys_time_t m_maxTicks = 0;
|
||||
|
||||
// --- Aggregate statistics ---
|
||||
|
||||
u64 m_invocationsTotal = 0;
|
||||
|
||||
// Running average of Mean Time Per Call
|
||||
AZStd::sys_time_t m_runningAverageTicks = 0;
|
||||
};
|
||||
|
||||
//! ImGui widget for examining Atom CPU Profiling instrumentation.
|
||||
//! Offers both a statistical view (with sorting and searching capability) and a visualizer
|
||||
//! similar to RAD and other profiling tools.
|
||||
class ImGuiCpuProfiler
|
||||
: SystemTickBus::Handler
|
||||
{
|
||||
// Region Name -> statistical view row data
|
||||
using RegionRowMap = AZStd::map<AZStd::string, TableRow>;
|
||||
// Group Name -> RegionRowMap
|
||||
using GroupRegionMap = AZStd::map<AZStd::string, RegionRowMap>;
|
||||
|
||||
using TimeRegion = AZ::RHI::CachedTimeRegion;
|
||||
using GroupRegionName = AZ::RHI::CachedTimeRegion::GroupRegionName;
|
||||
|
||||
public:
|
||||
struct CpuTimingEntry
|
||||
{
|
||||
const AZStd::string& m_name;
|
||||
double m_executeDuration;
|
||||
};
|
||||
|
||||
ImGuiCpuProfiler() = default;
|
||||
~ImGuiCpuProfiler() = default;
|
||||
|
||||
//! Draws the overall CPU profiling window, defaults to the statistical view
|
||||
void Draw(bool& keepDrawing);
|
||||
|
||||
private:
|
||||
static constexpr float RowHeight = 35.0;
|
||||
static constexpr int DefaultFramesToCollect = 50;
|
||||
static constexpr float MediumFrameTimeLimit = 16.6; // 60 fps
|
||||
static constexpr float HighFrameTimeLimit = 33.3; // 30 fps
|
||||
|
||||
//! Draws the statistical view of the CPU profiling data.
|
||||
void DrawStatisticsView();
|
||||
|
||||
//! Callback invoked when the "Load File" button is pressed in the file picker.
|
||||
void LoadFile();
|
||||
|
||||
//! Draws the file picker window.
|
||||
void DrawFilePicker();
|
||||
|
||||
//! Draws the CPU profiling visualizer.
|
||||
void DrawVisualizer();
|
||||
|
||||
// Draw the shared header between the two windows.
|
||||
void DrawCommonHeader();
|
||||
|
||||
// Draw the region statistics table in the order specified by the pointers in m_tableData.
|
||||
void DrawTable();
|
||||
|
||||
// Sort the table by a given column, rearranges the pointers in m_tableData.
|
||||
void SortTable(ImGuiTableSortSpecs* sortSpecs);
|
||||
|
||||
// gather the latest timing statistics
|
||||
void CacheCpuTimingStatistics();
|
||||
|
||||
// Get the profiling data from the last frame, only called when the profiler is not paused.
|
||||
void CollectFrameData();
|
||||
|
||||
// Cull old data from internal storage, only called when profiler is not paused.
|
||||
void CullFrameData();
|
||||
|
||||
// Draws a single block onto the timeline into the specified row
|
||||
void DrawBlock(const TimeRegion& block, u64 targetRow);
|
||||
|
||||
// Draw horizontal lines between threads in the timeline
|
||||
void DrawThreadSeparator(u64 threadBoundary, u64 maxDepth);
|
||||
|
||||
// Draw the "Thread XXXXX" label onto the viewport
|
||||
void DrawThreadLabel(u64 baseRow, size_t threadId);
|
||||
|
||||
// Draw the vertical lines separating frames in the timeline
|
||||
void DrawFrameBoundaries();
|
||||
|
||||
// Draw the ruler with frame time labels
|
||||
void DrawRuler();
|
||||
|
||||
// Draw the frame time histogram
|
||||
void DrawFrameTimeHistogram();
|
||||
|
||||
// Converts raw ticks to a pixel value suitable to give to ImDrawList, handles window scrolling
|
||||
float ConvertTickToPixelSpace(AZStd::sys_time_t tick, AZStd::sys_time_t leftBound, AZStd::sys_time_t rightBound) const;
|
||||
|
||||
AZStd::sys_time_t GetViewportTickWidth() const;
|
||||
|
||||
// Gets the color for a block using the GroupRegionName as a key into the cache.
|
||||
// Generates a random ImU32 if the block does not yet have a color.
|
||||
ImU32 GetBlockColor(const TimeRegion& block);
|
||||
|
||||
// System tick bus overrides
|
||||
virtual void OnSystemTick() override;
|
||||
|
||||
// --- Visualizer Members ---
|
||||
|
||||
int m_framesToCollect = DefaultFramesToCollect;
|
||||
|
||||
// Tally of the number of saved profiling events so far
|
||||
u64 m_savedRegionCount = 0;
|
||||
|
||||
// Viewport tick bounds, these are used to convert tick space -> screen space and cull so we only draw onscreen objects
|
||||
AZStd::sys_time_t m_viewportStartTick;
|
||||
AZStd::sys_time_t m_viewportEndTick;
|
||||
|
||||
// Map to store each thread's TimeRegions, individual vectors are sorted by start tick
|
||||
// note: we use size_t as a proxy for thread_id because native_thread_id_type differs differs from
|
||||
// platform to platform, which causes problems when deserializing saved captures.
|
||||
AZStd::unordered_map<size_t, AZStd::vector<TimeRegion>> m_savedData;
|
||||
|
||||
// Region color cache
|
||||
AZStd::unordered_map<GroupRegionName, ImVec4, RHI::CachedTimeRegion::GroupRegionName::Hash> m_regionColorMap;
|
||||
|
||||
// Tracks the frame boundaries
|
||||
AZStd::vector<AZStd::sys_time_t> m_frameEndTicks = { INT64_MIN };
|
||||
|
||||
// Filter for highlighting regions on the visualizer
|
||||
ImGuiTextFilter m_visualizerHighlightFilter;
|
||||
|
||||
// --- Tabular view members ---
|
||||
|
||||
// ImGui filter used to filter TimedRegions.
|
||||
ImGuiTextFilter m_timedRegionFilter;
|
||||
|
||||
// Saves statistical view data organized by group name -> region name -> row data
|
||||
GroupRegionMap m_groupRegionMap;
|
||||
|
||||
// Saves pointers to objects in m_groupRegionMap, order reflects table ordering.
|
||||
// Non-owning, will be cleared when m_groupRegionMap is cleared.
|
||||
AZStd::vector<TableRow*> m_tableData;
|
||||
|
||||
// Pause cpu profiling. The profiler will show the statistics of the last frame before pause.
|
||||
bool m_paused = false;
|
||||
|
||||
// Export the profiling data from a single frame to a local file.
|
||||
bool m_captureToFile = false;
|
||||
|
||||
// Toggle between the normal statistical view and the visual profiling view.
|
||||
bool m_enableVisualizer = false;
|
||||
|
||||
// Last captured CPU timing statistics
|
||||
AZStd::vector<CpuTimingEntry> m_cpuTimingStatisticsWhenPause;
|
||||
AZStd::sys_time_t m_frameToFrameTime{};
|
||||
|
||||
AZStd::string m_lastCapturedFilePath;
|
||||
|
||||
bool m_showFilePicker = false;
|
||||
|
||||
// Cached file paths to previous traces on disk, sorted with the most recent trace at the front.
|
||||
AZStd::vector<IO::Path> m_cachedCapturePaths;
|
||||
|
||||
// Index into the file picker, used to determine which file to load when "Load File" is pressed.
|
||||
int m_currentFileIndex = 0;
|
||||
|
||||
|
||||
// --- Loading capture state ---
|
||||
AZStd::unordered_set<AZStd::string> m_deserializedStringPool;
|
||||
AZStd::unordered_set<RHI::CachedTimeRegion::GroupRegionName, RHI::CachedTimeRegion::GroupRegionName::Hash> m_deserializedGroupRegionNamePool;
|
||||
};
|
||||
} // namespace Render
|
||||
} // namespace AZ
|
||||
|
||||
#include "ImGuiCpuProfiler.inl"
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,9 @@
|
||||
#
|
||||
# Copyright (c) Contributors to the Open 3D Engine Project.
|
||||
# For complete copyright and license terms please see the LICENSE at the root of this distribution.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
#
|
||||
#
|
||||
|
||||
add_subdirectory(Code)
|
||||
@ -0,0 +1,64 @@
|
||||
#
|
||||
# Copyright (c) Contributors to the Open 3D Engine Project.
|
||||
# For complete copyright and license terms please see the LICENSE at the root of this distribution.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
#
|
||||
#
|
||||
|
||||
# data portion
|
||||
ly_add_target(
|
||||
NAME Profiler.Static STATIC
|
||||
NAMESPACE Gem
|
||||
FILES_CMAKE
|
||||
profiler_files.cmake
|
||||
INCLUDE_DIRECTORIES
|
||||
PUBLIC
|
||||
Include
|
||||
PRIVATE
|
||||
Source
|
||||
BUILD_DEPENDENCIES
|
||||
PUBLIC
|
||||
AZ::AzCore
|
||||
AZ::AzFramework
|
||||
)
|
||||
|
||||
ly_add_target(
|
||||
NAME Profiler ${PAL_TRAIT_MONOLITHIC_DRIVEN_MODULE_TYPE}
|
||||
NAMESPACE Gem
|
||||
FILES_CMAKE
|
||||
profiler_shared_files.cmake
|
||||
INCLUDE_DIRECTORIES
|
||||
PUBLIC
|
||||
Include
|
||||
PRIVATE
|
||||
Source
|
||||
BUILD_DEPENDENCIES
|
||||
PRIVATE
|
||||
Gem::Profiler.Static
|
||||
)
|
||||
|
||||
ly_create_alias(NAME Profiler.Servers NAMESPACE Gem TARGETS Gem::Profiler)
|
||||
ly_create_alias(NAME Profiler.Builders NAMESPACE Gem TARGETS Gem::Profiler)
|
||||
|
||||
# visualization portion
|
||||
ly_add_target(
|
||||
NAME ProfilerImGui ${PAL_TRAIT_MONOLITHIC_DRIVEN_MODULE_TYPE}
|
||||
NAMESPACE Gem
|
||||
FILES_CMAKE
|
||||
profiler_imgui_shared_files.cmake
|
||||
INCLUDE_DIRECTORIES
|
||||
PUBLIC
|
||||
Include
|
||||
PRIVATE
|
||||
Source
|
||||
BUILD_DEPENDENCIES
|
||||
PRIVATE
|
||||
Gem::Profiler.Static
|
||||
Gem::ImGui.imguilib
|
||||
RUNTIME_DEPENDENCIES
|
||||
Gem::ImGui.imguilib
|
||||
)
|
||||
|
||||
ly_create_alias(NAME Profiler.Clients NAMESPACE Gem TARGETS Gem::ProfilerImGui)
|
||||
ly_create_alias(NAME Profiler.Tools NAMESPACE Gem TARGETS Gem::ProfilerImGui)
|
||||
@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright (c) Contributors to the Open 3D Engine Project.
|
||||
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
*
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <AzCore/EBus/EBus.h>
|
||||
#include <AzCore/Interface/Interface.h>
|
||||
#include <AzCore/std/string/string.h>
|
||||
|
||||
namespace Profiler
|
||||
{
|
||||
class ProfilerRequests
|
||||
{
|
||||
public:
|
||||
AZ_RTTI(ProfilerRequests, "{3757c4e5-1941-457c-85ae-16305e17a4c6}");
|
||||
virtual ~ProfilerRequests() = default;
|
||||
|
||||
//! Enable/Disable the CpuProfiler
|
||||
virtual void SetProfilerEnabled(bool enabled) = 0;
|
||||
|
||||
//! Dump a single frame of Cpu profiling data
|
||||
virtual bool CaptureCpuProfilingStatistics(const AZStd::string& outputFilePath) = 0;
|
||||
|
||||
//! Start a multiframe capture of CPU profiling data.
|
||||
virtual bool BeginContinuousCpuProfilingCapture() = 0;
|
||||
|
||||
//! End and dump an in-progress continuous capture.
|
||||
virtual bool EndContinuousCpuProfilingCapture(const AZStd::string& outputFilePath) = 0;
|
||||
};
|
||||
|
||||
class ProfilerBusTraits
|
||||
: public AZ::EBusTraits
|
||||
{
|
||||
public:
|
||||
// EBusTraits overrides
|
||||
static constexpr AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Single;
|
||||
static constexpr AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::Single;
|
||||
};
|
||||
|
||||
class ProfilerNotifications
|
||||
: public AZ::EBusTraits
|
||||
{
|
||||
public:
|
||||
virtual ~ProfilerNotifications() = default;
|
||||
|
||||
//! Notify when the current CpuProfilingStatistics capture is finished
|
||||
//! @param result Set to true if it's finished successfully
|
||||
//! @param info The output file path or error information which depends on the return.
|
||||
virtual void OnCaptureCpuProfilingStatisticsFinished(bool result, const AZStd::string& info) = 0;
|
||||
};
|
||||
|
||||
using ProfilerInterface = AZ::Interface<ProfilerRequests>;
|
||||
using ProfilerRequestBus = AZ::EBus<ProfilerRequests, ProfilerBusTraits>;
|
||||
using ProfilerNotificationBus = AZ::EBus<ProfilerNotifications>;
|
||||
} // namespace Profiler
|
||||
@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright (c) Contributors to the Open 3D Engine Project.
|
||||
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
*
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <AzCore/EBus/EBus.h>
|
||||
#include <AzCore/Interface/Interface.h>
|
||||
|
||||
namespace Profiler
|
||||
{
|
||||
class ProfilerImGuiRequests
|
||||
{
|
||||
public:
|
||||
AZ_RTTI(ProfilerImGuiRequests, "{E0443400-D108-4D3F-8FF5-4F076FCF6D13}");
|
||||
virtual ~ProfilerImGuiRequests() = default;
|
||||
|
||||
// special request to render the CPU profiler window in a non-standard way
|
||||
// e.g not through ImGuiUpdateListenerBus::OnImGuiUpdate
|
||||
virtual void ShowCpuProfilerWindow(bool& keepDrawing) = 0;
|
||||
};
|
||||
|
||||
using ProfilerImGuiInterface = AZ::Interface<ProfilerImGuiRequests>;
|
||||
} // namespace Profiler
|
||||
@ -0,0 +1,83 @@
|
||||
/*
|
||||
* Copyright (c) Contributors to the Open 3D Engine Project.
|
||||
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
*
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AzCore/Debug/EventTrace.h>
|
||||
#include <AzCore/Debug/Profiler.h>
|
||||
#include <AzCore/RTTI/RTTI.h>
|
||||
#include <AzCore/std/containers/ring_buffer.h>
|
||||
#include <AzCore/std/containers/unordered_map.h>
|
||||
#include <AzCore/std/string/string.h>
|
||||
|
||||
namespace Profiler
|
||||
{
|
||||
//! Structure that is used to cache a timed region into the thread's local storage.
|
||||
struct CachedTimeRegion
|
||||
{
|
||||
//! Structure used internally for caching assumed global string pointers (ideally literals) to the marker group/region
|
||||
//! NOTE: When used in a separate shared library, the library mustn't be unloaded before the CpuProfiler is shutdown.
|
||||
struct GroupRegionName
|
||||
{
|
||||
GroupRegionName() = delete;
|
||||
GroupRegionName(const char* const group, const char* const region);
|
||||
|
||||
const char* m_groupName = nullptr;
|
||||
const char* m_regionName = nullptr;
|
||||
|
||||
struct Hash
|
||||
{
|
||||
AZStd::size_t operator()(const GroupRegionName& name) const;
|
||||
};
|
||||
bool operator==(const GroupRegionName& other) const;
|
||||
};
|
||||
|
||||
CachedTimeRegion() = default;
|
||||
explicit CachedTimeRegion(const GroupRegionName& groupRegionName);
|
||||
CachedTimeRegion(const GroupRegionName& groupRegionName, uint16_t stackDepth, uint64_t startTick, uint64_t endTick);
|
||||
|
||||
GroupRegionName m_groupRegionName{nullptr, nullptr};
|
||||
|
||||
uint16_t m_stackDepth = 0u;
|
||||
AZStd::sys_time_t m_startTick = 0;
|
||||
AZStd::sys_time_t m_endTick = 0;
|
||||
};
|
||||
|
||||
//! Interface class of the CpuProfiler
|
||||
class CpuProfiler
|
||||
{
|
||||
public:
|
||||
using ThreadTimeRegionMap = AZStd::unordered_map<AZStd::string, AZStd::vector<CachedTimeRegion>>;
|
||||
using TimeRegionMap = AZStd::unordered_map<AZStd::thread_id, ThreadTimeRegionMap>;
|
||||
|
||||
AZ_RTTI(CpuProfiler, "{127C1D0B-BE05-4E18-A8F6-24F3EED2ECA6}");
|
||||
|
||||
CpuProfiler() = default;
|
||||
virtual ~CpuProfiler() = default;
|
||||
|
||||
AZ_DISABLE_COPY_MOVE(CpuProfiler);
|
||||
|
||||
static CpuProfiler* Get();
|
||||
|
||||
//! Get the last frame's TimeRegionMap
|
||||
virtual const TimeRegionMap& GetTimeRegionMap() const = 0;
|
||||
|
||||
//! Begin a continuous capture. Blocks the profiler from being toggled off until EndContinuousCapture is called.
|
||||
[[nodiscard]] virtual bool BeginContinuousCapture() = 0;
|
||||
|
||||
//! Flush the CPU Profiler's saved data into the passed ring buffer .
|
||||
[[nodiscard]] virtual bool EndContinuousCapture(AZStd::ring_buffer<TimeRegionMap>& flushTarget) = 0;
|
||||
|
||||
virtual bool IsContinuousCaptureInProgress() const = 0;
|
||||
|
||||
//! Enable/Disable the CpuProfiler
|
||||
virtual void SetProfilerEnabled(bool enabled) = 0;
|
||||
|
||||
virtual bool IsProfilerEnabled() const = 0 ;
|
||||
};
|
||||
} // namespace Profiler
|
||||
@ -0,0 +1,437 @@
|
||||
/*
|
||||
* Copyright (c) Contributors to the Open 3D Engine Project.
|
||||
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
*
|
||||
*/
|
||||
|
||||
#include <CpuProfilerImpl.h>
|
||||
|
||||
#include <AzCore/Debug/Timer.h>
|
||||
#include <AzCore/Interface/Interface.h>
|
||||
#include <AzCore/Serialization/SerializeContext.h>
|
||||
#include <AzCore/Statistics/StatisticalProfilerProxy.h>
|
||||
#include <AzCore/std/smart_ptr/shared_ptr.h>
|
||||
|
||||
namespace Profiler
|
||||
{
|
||||
thread_local CpuTimingLocalStorage* CpuProfilerImpl::ms_threadLocalStorage = nullptr;
|
||||
|
||||
// --- CpuProfiler ---
|
||||
|
||||
CpuProfiler* CpuProfiler::Get()
|
||||
{
|
||||
return AZ::Interface<CpuProfiler>::Get();
|
||||
}
|
||||
|
||||
// --- CachedTimeRegion ---
|
||||
|
||||
CachedTimeRegion::CachedTimeRegion(const GroupRegionName& groupRegionName)
|
||||
{
|
||||
m_groupRegionName = groupRegionName;
|
||||
}
|
||||
|
||||
CachedTimeRegion::CachedTimeRegion(const GroupRegionName& groupRegionName, uint16_t stackDepth, uint64_t startTick, uint64_t endTick)
|
||||
{
|
||||
m_groupRegionName = groupRegionName;
|
||||
m_stackDepth = stackDepth;
|
||||
m_startTick = startTick;
|
||||
m_endTick = endTick;
|
||||
}
|
||||
|
||||
// --- GroupRegionName ---
|
||||
|
||||
CachedTimeRegion::GroupRegionName::GroupRegionName(const char* const group, const char* const region) :
|
||||
m_groupName(group),
|
||||
m_regionName(region)
|
||||
{
|
||||
}
|
||||
|
||||
AZStd::size_t CachedTimeRegion::GroupRegionName::Hash::operator()(const CachedTimeRegion::GroupRegionName& name) const
|
||||
{
|
||||
AZStd::size_t seed = 0;
|
||||
AZStd::hash_combine(seed, name.m_groupName);
|
||||
AZStd::hash_combine(seed, name.m_regionName);
|
||||
return seed;
|
||||
}
|
||||
|
||||
bool CachedTimeRegion::GroupRegionName::operator==(const GroupRegionName& other) const
|
||||
{
|
||||
return (m_groupName == other.m_groupName) && (m_regionName == other.m_regionName);
|
||||
}
|
||||
|
||||
|
||||
// --- CpuProfilerImpl ---
|
||||
|
||||
void CpuProfilerImpl::Init()
|
||||
{
|
||||
AZ::Interface<AZ::Debug::Profiler>::Register(this);
|
||||
AZ::Interface<CpuProfiler>::Register(this);
|
||||
m_initialized = true;
|
||||
AZ::SystemTickBus::Handler::BusConnect();
|
||||
m_continuousCaptureData.set_capacity(10);
|
||||
}
|
||||
|
||||
void CpuProfilerImpl::Shutdown()
|
||||
{
|
||||
if (!m_initialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
// When this call is made, no more thread profiling calls can be performed anymore
|
||||
AZ::Interface<CpuProfiler>::Unregister(this);
|
||||
AZ::Interface<AZ::Debug::Profiler>::Unregister(this);
|
||||
|
||||
// Wait for the remaining threads that might still be processing its profiling calls
|
||||
AZStd::unique_lock<AZStd::shared_mutex> shutdownLock(m_shutdownMutex);
|
||||
|
||||
m_enabled = false;
|
||||
|
||||
// Cleanup all TLS
|
||||
m_registeredThreads.clear();
|
||||
m_timeRegionMap.clear();
|
||||
m_initialized = false;
|
||||
m_continuousCaptureInProgress.store(false);
|
||||
m_continuousCaptureData.clear();
|
||||
AZ::SystemTickBus::Handler::BusDisconnect();
|
||||
}
|
||||
|
||||
void CpuProfilerImpl::BeginRegion(const AZ::Debug::Budget* budget, const char* eventName)
|
||||
{
|
||||
// Try to lock here, the shutdownMutex will only be contested when the CpuProfiler is shutting down.
|
||||
if (m_shutdownMutex.try_lock_shared())
|
||||
{
|
||||
if (m_enabled)
|
||||
{
|
||||
// Lazy initialization, creates an instance of the Thread local data if it's not created, and registers it
|
||||
RegisterThreadStorage();
|
||||
|
||||
// Push it to the stack
|
||||
CachedTimeRegion timeRegion({budget->Name(), eventName});
|
||||
ms_threadLocalStorage->RegionStackPushBack(timeRegion);
|
||||
}
|
||||
|
||||
m_shutdownMutex.unlock_shared();
|
||||
}
|
||||
}
|
||||
|
||||
void CpuProfilerImpl::EndRegion([[maybe_unused]] const AZ::Debug::Budget* budget)
|
||||
{
|
||||
// Try to lock here, the shutdownMutex will only be contested when the CpuProfiler is shutting down.
|
||||
if (m_shutdownMutex.try_lock_shared())
|
||||
{
|
||||
// guard against enabling mid-marker
|
||||
if (m_enabled && ms_threadLocalStorage != nullptr)
|
||||
{
|
||||
ms_threadLocalStorage->RegionStackPopBack();
|
||||
}
|
||||
|
||||
m_shutdownMutex.unlock_shared();
|
||||
}
|
||||
}
|
||||
|
||||
const CpuProfiler::TimeRegionMap& CpuProfilerImpl::GetTimeRegionMap() const
|
||||
{
|
||||
return m_timeRegionMap;
|
||||
}
|
||||
|
||||
bool CpuProfilerImpl::BeginContinuousCapture()
|
||||
{
|
||||
bool expected = false;
|
||||
if (m_continuousCaptureInProgress.compare_exchange_strong(expected, true))
|
||||
{
|
||||
m_enabled = true;
|
||||
AZ_TracePrintf("Profiler", "Continuous capture started\n");
|
||||
return true;
|
||||
}
|
||||
|
||||
AZ_TracePrintf("Profiler", "Attempting to start a continuous capture while one already in progress");
|
||||
return false;
|
||||
}
|
||||
|
||||
bool CpuProfilerImpl::EndContinuousCapture(AZStd::ring_buffer<TimeRegionMap>& flushTarget)
|
||||
{
|
||||
if (!m_continuousCaptureInProgress.load())
|
||||
{
|
||||
AZ_TracePrintf("Profiler", "Attempting to end a continuous capture while one not in progress");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (m_continuousCaptureEndingMutex.try_lock())
|
||||
{
|
||||
m_enabled = false;
|
||||
flushTarget = AZStd::move(m_continuousCaptureData);
|
||||
m_continuousCaptureData.clear();
|
||||
AZ_TracePrintf("Profiler", "Continuous capture ended\n");
|
||||
m_continuousCaptureInProgress.store(false);
|
||||
|
||||
m_continuousCaptureEndingMutex.unlock();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool CpuProfilerImpl::IsContinuousCaptureInProgress() const
|
||||
{
|
||||
return m_continuousCaptureInProgress.load();
|
||||
}
|
||||
|
||||
void CpuProfilerImpl::SetProfilerEnabled(bool enabled)
|
||||
{
|
||||
AZStd::unique_lock<AZStd::mutex> lock(m_threadRegisterMutex);
|
||||
|
||||
// Early out if the state is already the same or a continuous capture is in progress
|
||||
if (m_enabled == enabled || m_continuousCaptureInProgress.load())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Set the dirty flag in all the TLS to clear the caches
|
||||
if (enabled)
|
||||
{
|
||||
// Iterate through all the threads, and set the clearing flag
|
||||
for (auto& threadLocal : m_registeredThreads)
|
||||
{
|
||||
threadLocal->m_clearContainers = true;
|
||||
}
|
||||
|
||||
m_enabled = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
m_enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
bool CpuProfilerImpl::IsProfilerEnabled() const
|
||||
{
|
||||
return m_enabled;
|
||||
}
|
||||
|
||||
void CpuProfilerImpl::OnSystemTick()
|
||||
{
|
||||
if (!m_enabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_continuousCaptureInProgress.load() && m_continuousCaptureEndingMutex.try_lock())
|
||||
{
|
||||
if (m_continuousCaptureData.full() && m_continuousCaptureData.size() != MaxFramesToSave)
|
||||
{
|
||||
const AZStd::size_t size = m_continuousCaptureData.size();
|
||||
m_continuousCaptureData.set_capacity(AZStd::min(MaxFramesToSave, size + size / 2));
|
||||
}
|
||||
|
||||
m_continuousCaptureData.push_back(AZStd::move(m_timeRegionMap));
|
||||
m_timeRegionMap.clear();
|
||||
m_continuousCaptureEndingMutex.unlock();
|
||||
}
|
||||
|
||||
AZStd::unique_lock<AZStd::mutex> lock(m_threadRegisterMutex);
|
||||
|
||||
// Iterate through all the threads, and collect the thread's cached time regions
|
||||
TimeRegionMap newMap;
|
||||
for (auto& threadLocal : m_registeredThreads)
|
||||
{
|
||||
ThreadTimeRegionMap& threadMapEntry = newMap[threadLocal->m_executingThreadId];
|
||||
threadLocal->TryFlushCachedMap(threadMapEntry);
|
||||
}
|
||||
|
||||
// Clear all TLS that flagged themselves to be deleted, meaning that the thread is already terminated
|
||||
AZStd::remove_if(m_registeredThreads.begin(), m_registeredThreads.end(), [](const AZStd::intrusive_ptr<CpuTimingLocalStorage>& thread)
|
||||
{
|
||||
return thread->m_deleteFlag.load();
|
||||
});
|
||||
|
||||
// Update our saved time regions to the last frame's collected data
|
||||
m_timeRegionMap = AZStd::move(newMap);
|
||||
}
|
||||
|
||||
void CpuProfilerImpl::RegisterThreadStorage()
|
||||
{
|
||||
AZStd::unique_lock<AZStd::mutex> lock(m_threadRegisterMutex);
|
||||
if (!ms_threadLocalStorage)
|
||||
{
|
||||
ms_threadLocalStorage = aznew CpuTimingLocalStorage();
|
||||
m_registeredThreads.emplace_back(ms_threadLocalStorage);
|
||||
}
|
||||
}
|
||||
|
||||
// --- CpuTimingLocalStorage ---
|
||||
|
||||
CpuTimingLocalStorage::CpuTimingLocalStorage()
|
||||
{
|
||||
m_executingThreadId = AZStd::this_thread::get_id();
|
||||
}
|
||||
|
||||
CpuTimingLocalStorage::~CpuTimingLocalStorage()
|
||||
{
|
||||
m_deleteFlag = true;
|
||||
}
|
||||
|
||||
void CpuTimingLocalStorage::RegionStackPushBack(CachedTimeRegion& timeRegion)
|
||||
{
|
||||
// If it was (re)enabled, clear the lists first
|
||||
if (m_clearContainers)
|
||||
{
|
||||
m_clearContainers = false;
|
||||
|
||||
m_stackLevel = 0;
|
||||
m_cachedTimeRegionMap.clear();
|
||||
m_timeRegionStack.clear();
|
||||
m_cachedTimeRegions.clear();
|
||||
}
|
||||
|
||||
timeRegion.m_stackDepth = aznumeric_cast<uint16_t>(m_stackLevel);
|
||||
|
||||
AZ_Assert(m_timeRegionStack.size() < TimeRegionStackSize, "Adding too many time regions to the stack. Increase the size of TimeRegionStackSize.");
|
||||
m_timeRegionStack.push_back(timeRegion);
|
||||
|
||||
// Increment the stack
|
||||
m_stackLevel++;
|
||||
|
||||
// Set the starting time at the end, to avoid recording the minor overhead
|
||||
m_timeRegionStack.back().m_startTick = AZStd::GetTimeNowTicks();
|
||||
}
|
||||
|
||||
void CpuTimingLocalStorage::RegionStackPopBack()
|
||||
{
|
||||
// Early out when the stack is empty, this might happen when the profiler was enabled while the thread encountered profiling markers
|
||||
if (m_timeRegionStack.empty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the end timestamp here, to avoid the minor overhead
|
||||
const AZStd::sys_time_t endRegionTime = AZStd::GetTimeNowTicks();
|
||||
|
||||
AZ_Assert(!m_timeRegionStack.empty(), "Trying to pop an element in the stack, but it's empty.");
|
||||
CachedTimeRegion back = m_timeRegionStack.back();
|
||||
m_timeRegionStack.pop_back();
|
||||
|
||||
// Set the ending time
|
||||
back.m_endTick = endRegionTime;
|
||||
|
||||
// Decrement the stack
|
||||
m_stackLevel--;
|
||||
|
||||
// Add an entry to the cached region
|
||||
AddCachedRegion(back);
|
||||
}
|
||||
|
||||
// Gets called when region ends and all data is set
|
||||
void CpuTimingLocalStorage::AddCachedRegion(const CachedTimeRegion& timeRegionCached)
|
||||
{
|
||||
if (m_hitSizeLimitMap[timeRegionCached.m_groupRegionName.m_regionName])
|
||||
{
|
||||
return;
|
||||
}
|
||||
// Add an entry to the cached region
|
||||
m_cachedTimeRegions.push_back(timeRegionCached);
|
||||
|
||||
// If the stack is empty, add it to the local cache map. Only gets called when the stack is empty
|
||||
// NOTE: this is where the largest overhead will be, but due to it only being called when the stack is empty
|
||||
// (i.e when the root region ended), this overhead won't affect any time regions.
|
||||
// The exception being for functions that are being profiled and create/spawn threads that are also profiled. Unfortunately, in this
|
||||
// case, the overhead of the profiled threads will be added to the main thread.
|
||||
if (m_timeRegionStack.empty())
|
||||
{
|
||||
AZStd::unique_lock<AZStd::mutex> lock(m_cachedTimeRegionMutex);
|
||||
|
||||
// Add the cached regions to the map
|
||||
for (auto& cachedTimeRegion : m_cachedTimeRegions)
|
||||
{
|
||||
const AZStd::string regionName = cachedTimeRegion.m_groupRegionName.m_regionName;
|
||||
AZStd::vector<CachedTimeRegion>& regionVec = m_cachedTimeRegionMap[regionName];
|
||||
regionVec.push_back(cachedTimeRegion);
|
||||
if (regionVec.size() >= TimeRegionStackSize)
|
||||
{
|
||||
m_hitSizeLimitMap.insert_or_assign(AZStd::move(regionName), true);
|
||||
}
|
||||
}
|
||||
|
||||
// Clear the cached regions
|
||||
m_cachedTimeRegions.clear();
|
||||
}
|
||||
}
|
||||
|
||||
void CpuTimingLocalStorage::TryFlushCachedMap(CpuProfiler::ThreadTimeRegionMap& cachedTimeRegionMap)
|
||||
{
|
||||
// Try to lock, if it's already in use (the cached regions in the array are being copied to the map)
|
||||
// it'll show up in the next iteration when the user requests it.
|
||||
if (m_cachedTimeRegionMutex.try_lock())
|
||||
{
|
||||
// Only flush cached time regions if there are entries available
|
||||
if (!m_cachedTimeRegionMap.empty())
|
||||
{
|
||||
cachedTimeRegionMap = AZStd::move(m_cachedTimeRegionMap);
|
||||
m_cachedTimeRegionMap.clear();
|
||||
m_hitSizeLimitMap.clear();
|
||||
}
|
||||
m_cachedTimeRegionMutex.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
// --- CpuProfilingStatisticsSerializer ---
|
||||
|
||||
CpuProfilingStatisticsSerializer::CpuProfilingStatisticsSerializer(const AZStd::ring_buffer<CpuProfiler::TimeRegionMap>& continuousData)
|
||||
{
|
||||
// Create serializable entries
|
||||
for (const auto& timeRegionMap : continuousData)
|
||||
{
|
||||
for (const auto& [threadId, regionMap] : timeRegionMap)
|
||||
{
|
||||
for (const auto& [regionName, regionVec] : regionMap)
|
||||
{
|
||||
for (const auto& region : regionVec)
|
||||
{
|
||||
m_cpuProfilingStatisticsSerializerEntries.emplace_back(region, threadId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void CpuProfilingStatisticsSerializer::Reflect(AZ::ReflectContext* context)
|
||||
{
|
||||
if (auto* serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
|
||||
{
|
||||
serializeContext->Class<CpuProfilingStatisticsSerializer>()
|
||||
->Version(1)
|
||||
->Field("cpuProfilingStatisticsSerializerEntries", &CpuProfilingStatisticsSerializer::m_cpuProfilingStatisticsSerializerEntries);
|
||||
}
|
||||
|
||||
CpuProfilingStatisticsSerializerEntry::Reflect(context);
|
||||
}
|
||||
|
||||
// --- CpuProfilingStatisticsSerializerEntry ---
|
||||
|
||||
CpuProfilingStatisticsSerializer::CpuProfilingStatisticsSerializerEntry::CpuProfilingStatisticsSerializerEntry(
|
||||
const CachedTimeRegion& cachedTimeRegion, AZStd::thread_id threadId)
|
||||
{
|
||||
m_groupName = cachedTimeRegion.m_groupRegionName.m_groupName;
|
||||
m_regionName = cachedTimeRegion.m_groupRegionName.m_regionName;
|
||||
m_stackDepth = cachedTimeRegion.m_stackDepth;
|
||||
m_startTick = cachedTimeRegion.m_startTick;
|
||||
m_endTick = cachedTimeRegion.m_endTick;
|
||||
m_threadId = AZStd::hash<AZStd::thread_id>{}(threadId);
|
||||
}
|
||||
|
||||
void CpuProfilingStatisticsSerializer::CpuProfilingStatisticsSerializerEntry::Reflect(AZ::ReflectContext* context)
|
||||
{
|
||||
if (auto* serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
|
||||
{
|
||||
serializeContext->Class<CpuProfilingStatisticsSerializerEntry>()
|
||||
->Version(1)
|
||||
->Field("groupName", &CpuProfilingStatisticsSerializerEntry::m_groupName)
|
||||
->Field("regionName", &CpuProfilingStatisticsSerializerEntry::m_regionName)
|
||||
->Field("stackDepth", &CpuProfilingStatisticsSerializerEntry::m_stackDepth)
|
||||
->Field("startTick", &CpuProfilingStatisticsSerializerEntry::m_startTick)
|
||||
->Field("endTick", &CpuProfilingStatisticsSerializerEntry::m_endTick)
|
||||
->Field("threadId", &CpuProfilingStatisticsSerializerEntry::m_threadId);
|
||||
}
|
||||
}
|
||||
} // namespace Profiler
|
||||
@ -0,0 +1,183 @@
|
||||
/*
|
||||
* Copyright (c) Contributors to the Open 3D Engine Project.
|
||||
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
*
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <CpuProfiler.h>
|
||||
|
||||
#include <AzCore/Component/TickBus.h>
|
||||
#include <AzCore/Memory/OSAllocator.h>
|
||||
#include <AzCore/Name/Name.h>
|
||||
#include <AzCore/std/containers/map.h>
|
||||
#include <AzCore/std/containers/unordered_set.h>
|
||||
#include <AzCore/std/parallel/mutex.h>
|
||||
#include <AzCore/std/parallel/shared_mutex.h>
|
||||
#include <AzCore/std/smart_ptr/intrusive_refcount.h>
|
||||
|
||||
namespace Profiler
|
||||
{
|
||||
//! Thread local class to keep track of the thread's cached time regions.
|
||||
//! Each thread keeps track of its own time regions, which is communicated from the CpuProfilerImpl.
|
||||
//! The CpuProfilerImpl is able to request the cached time regions from the CpuTimingLocalStorage.
|
||||
class CpuTimingLocalStorage
|
||||
: public AZStd::intrusive_refcount<AZStd::atomic_uint>
|
||||
{
|
||||
friend class CpuProfilerImpl;
|
||||
|
||||
public:
|
||||
AZ_CLASS_ALLOCATOR(CpuTimingLocalStorage, AZ::OSAllocator, 0);
|
||||
|
||||
CpuTimingLocalStorage();
|
||||
~CpuTimingLocalStorage();
|
||||
|
||||
private:
|
||||
// Maximum stack size
|
||||
static constexpr uint32_t TimeRegionStackSize = 2048u;
|
||||
|
||||
// Adds a region to the stack, gets called each time a region begins
|
||||
void RegionStackPushBack(CachedTimeRegion& timeRegion);
|
||||
|
||||
// Pops a region from the stack, gets called each time a region ends
|
||||
void RegionStackPopBack();
|
||||
|
||||
// Add a new cached time region. If the stack is empty, flush all entries to the cached map
|
||||
void AddCachedRegion(const CachedTimeRegion& timeRegionCached);
|
||||
|
||||
// Tries to flush the map to the passed parameter, only if the thread's mutex is unlocked
|
||||
void TryFlushCachedMap(CpuProfiler::ThreadTimeRegionMap& cachedRegionMap);
|
||||
|
||||
AZStd::thread_id m_executingThreadId;
|
||||
// Keeps track of the current thread's stack depth
|
||||
uint32_t m_stackLevel = 0u;
|
||||
|
||||
// Cached region map, will be flushed to the system's map when the system requests it
|
||||
CpuProfiler::ThreadTimeRegionMap m_cachedTimeRegionMap;
|
||||
|
||||
// Use fixed vectors to avoid re-allocating new elements
|
||||
// Keeps track of the regions that added and removed using the macro
|
||||
AZStd::fixed_vector<CachedTimeRegion, TimeRegionStackSize> m_timeRegionStack;
|
||||
|
||||
// Keeps track of regions that completed (i.e regions that was pushed and popped from the stack)
|
||||
// Intermediate storage point for the CachedTimeRegions, when the stack is empty, all entries will be
|
||||
// copied to the map.
|
||||
AZStd::fixed_vector<CachedTimeRegion, TimeRegionStackSize> m_cachedTimeRegions;
|
||||
AZStd::mutex m_cachedTimeRegionMutex;
|
||||
|
||||
// Dirty flag which is set when the CpuProfiler's enabled state is set from false to true
|
||||
AZStd::atomic_bool m_clearContainers = false;
|
||||
|
||||
// When the thread is terminated, it will flag itself for deletion
|
||||
AZStd::atomic_bool m_deleteFlag = false;
|
||||
|
||||
// Keep track of the regions that have hit the size limit so we don't have to lock to check
|
||||
AZStd::map<AZStd::string, bool> m_hitSizeLimitMap;
|
||||
};
|
||||
|
||||
//! CpuProfiler will keep track of the registered threads, and
|
||||
//! forwards the request to profile a region to the appropriate thread. The user is able to request all
|
||||
//! cached regions, which are stored on a per thread frequency.
|
||||
class CpuProfilerImpl final
|
||||
: public AZ::Debug::Profiler
|
||||
, public CpuProfiler
|
||||
, public AZ::SystemTickBus::Handler
|
||||
{
|
||||
friend class CpuTimingLocalStorage;
|
||||
|
||||
public:
|
||||
AZ_TYPE_INFO(CpuProfilerImpl, "{10E9D394-FC83-4B45-B2B8-807C6BF07BF0}");
|
||||
AZ_CLASS_ALLOCATOR(CpuProfilerImpl, AZ::OSAllocator, 0);
|
||||
|
||||
CpuProfilerImpl() = default;
|
||||
~CpuProfilerImpl() = default;
|
||||
|
||||
//! Registers the CpuProfilerImpl instance to the interface
|
||||
void Init();
|
||||
//! Unregisters the CpuProfilerImpl instance from the interface
|
||||
void Shutdown();
|
||||
|
||||
// AZ::SystemTickBus::Handler overrides
|
||||
// When fired, the profiler collects all profiling data from registered threads and updates
|
||||
// m_timeRegionMap so that the next frame has up-to-date profiling data.
|
||||
void OnSystemTick() final override;
|
||||
|
||||
//! AZ::Debug::Profiler overrides...
|
||||
void BeginRegion(const AZ::Debug::Budget* budget, const char* eventName) final override;
|
||||
void EndRegion(const AZ::Debug::Budget* budget) final override;
|
||||
|
||||
//! CpuProfiler overrides...
|
||||
const TimeRegionMap& GetTimeRegionMap() const final override;
|
||||
bool BeginContinuousCapture() final override;
|
||||
bool EndContinuousCapture(AZStd::ring_buffer<TimeRegionMap>& flushTarget) final override;
|
||||
bool IsContinuousCaptureInProgress() const final override;
|
||||
void SetProfilerEnabled(bool enabled) final override;
|
||||
bool IsProfilerEnabled() const final override;
|
||||
|
||||
private:
|
||||
static constexpr AZStd::size_t MaxFramesToSave = 2 * 60 * 120; // 2 minutes of 120fps
|
||||
static constexpr AZStd::size_t MaxRegionStringPoolSize = 16384; // Max amount of unique strings to save in the pool before throwing warnings.
|
||||
|
||||
// Lazily create and register the local thread data
|
||||
void RegisterThreadStorage();
|
||||
|
||||
// ThreadId -> ThreadTimeRegionMap
|
||||
// On the start of each frame, this map will be updated with the last frame's profiling data.
|
||||
TimeRegionMap m_timeRegionMap;
|
||||
|
||||
// Set of registered threads when created
|
||||
AZStd::vector<AZStd::intrusive_ptr<CpuTimingLocalStorage>, AZ::OSStdAllocator> m_registeredThreads;
|
||||
AZStd::mutex m_threadRegisterMutex;
|
||||
|
||||
// Thread local storage, gets lazily allocated when a thread is created
|
||||
static thread_local CpuTimingLocalStorage* ms_threadLocalStorage;
|
||||
|
||||
// Enable/Disables the threads from profiling
|
||||
AZStd::atomic_bool m_enabled = false;
|
||||
|
||||
// This lock will only be contested when the CpuProfiler's Shutdown() method has been called
|
||||
AZStd::shared_mutex m_shutdownMutex;
|
||||
|
||||
bool m_initialized = false;
|
||||
|
||||
AZStd::mutex m_continuousCaptureEndingMutex;
|
||||
|
||||
AZStd::atomic_bool m_continuousCaptureInProgress;
|
||||
|
||||
// Stores multiple frames of profiling data, size is controlled by MaxFramesToSave. Flushed when EndContinuousCapture is called.
|
||||
// Ring buffer so that we can have fast append of new data + removal of old profiling data with good cache locality.
|
||||
AZStd::ring_buffer<TimeRegionMap> m_continuousCaptureData;
|
||||
};
|
||||
|
||||
// Intermediate class to serialize Cpu TimedRegion data.
|
||||
class CpuProfilingStatisticsSerializer
|
||||
{
|
||||
public:
|
||||
class CpuProfilingStatisticsSerializerEntry
|
||||
{
|
||||
public:
|
||||
AZ_TYPE_INFO(CpuProfilingStatisticsSerializer::CpuProfilingStatisticsSerializerEntry, "{26B78F65-EB96-46E2-BE7E-A1233880B225}");
|
||||
static void Reflect(AZ::ReflectContext* context);
|
||||
|
||||
CpuProfilingStatisticsSerializerEntry() = default;
|
||||
CpuProfilingStatisticsSerializerEntry(const CachedTimeRegion& cachedTimeRegion, AZStd::thread_id threadId);
|
||||
|
||||
AZ::Name m_groupName;
|
||||
AZ::Name m_regionName;
|
||||
uint16_t m_stackDepth;
|
||||
AZStd::sys_time_t m_startTick;
|
||||
AZStd::sys_time_t m_endTick;
|
||||
size_t m_threadId;
|
||||
};
|
||||
|
||||
AZ_TYPE_INFO(CpuProfilingStatisticsSerializer, "{D5B02946-0D27-474F-9A44-364C2706DD41}");
|
||||
static void Reflect(AZ::ReflectContext* context);
|
||||
|
||||
CpuProfilingStatisticsSerializer() = default;
|
||||
CpuProfilingStatisticsSerializer(const AZStd::ring_buffer<CpuProfiler::TimeRegionMap>& continuousData);
|
||||
|
||||
AZStd::vector<CpuProfilingStatisticsSerializerEntry> m_cpuProfilingStatisticsSerializerEntries;
|
||||
};
|
||||
}; // namespace Profiler
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,234 @@
|
||||
/*
|
||||
* Copyright (c) Contributors to the Open 3D Engine Project.
|
||||
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
*
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#if defined(IMGUI_ENABLED)
|
||||
|
||||
#include <CpuProfiler.h>
|
||||
|
||||
#include <AzCore/Component/TickBus.h>
|
||||
#include <AzCore/IO/Path/Path.h>
|
||||
#include <AzCore/Math/Random.h>
|
||||
#include <AzCore/std/containers/map.h>
|
||||
#include <AzCore/std/containers/set.h>
|
||||
#include <AzCore/std/containers/unordered_set.h>
|
||||
|
||||
#include <imgui/imgui.h>
|
||||
|
||||
namespace Profiler
|
||||
{
|
||||
//! Stores all the data associated with a row in the table.
|
||||
struct TableRow
|
||||
{
|
||||
template <typename T>
|
||||
struct TableRowCompareFunctor
|
||||
{
|
||||
TableRowCompareFunctor(T memberPointer, bool isAscending) : m_memberPointer(memberPointer), m_ascending(isAscending){};
|
||||
|
||||
bool operator()(const TableRow* lhs, const TableRow* rhs)
|
||||
{
|
||||
return m_ascending ? lhs->*m_memberPointer < rhs->*m_memberPointer : lhs->*m_memberPointer > rhs->*m_memberPointer;
|
||||
}
|
||||
|
||||
T m_memberPointer;
|
||||
bool m_ascending;
|
||||
};
|
||||
|
||||
// Update running statistics with new region data
|
||||
void RecordRegion(const CachedTimeRegion& region, size_t threadId);
|
||||
|
||||
void ResetPerFrameStatistics();
|
||||
|
||||
// Get a string of all threads that this region executed in during the last frame
|
||||
AZStd::string GetExecutingThreadsLabel() const;
|
||||
|
||||
AZStd::string m_groupName;
|
||||
AZStd::string m_regionName;
|
||||
|
||||
// --- Per frame statistics ---
|
||||
|
||||
AZ::u64 m_invocationsLastFrame = 0;
|
||||
|
||||
// NOTE: set over unordered_set so the threads can be shown in increasing order in tooltip.
|
||||
AZStd::set<size_t> m_executingThreads;
|
||||
|
||||
AZStd::sys_time_t m_lastFrameTotalTicks = 0;
|
||||
|
||||
// Maximum execution time of a region in the last frame.
|
||||
AZStd::sys_time_t m_maxTicks = 0;
|
||||
|
||||
// --- Aggregate statistics ---
|
||||
|
||||
AZ::u64 m_invocationsTotal = 0;
|
||||
|
||||
// Running average of Mean Time Per Call
|
||||
AZStd::sys_time_t m_runningAverageTicks = 0;
|
||||
};
|
||||
|
||||
//! ImGui widget for examining CPU Profiling instrumentation.
|
||||
//! Offers both a statistical view (with sorting and searching capability) and a visualizer
|
||||
//! similar to other profiling tools.
|
||||
class ImGuiCpuProfiler
|
||||
: public AZ::SystemTickBus::Handler
|
||||
{
|
||||
// Region Name -> statistical view row data
|
||||
using RegionRowMap = AZStd::map<AZStd::string, TableRow>;
|
||||
// Group Name -> RegionRowMap
|
||||
using GroupRegionMap = AZStd::map<AZStd::string, RegionRowMap>;
|
||||
|
||||
using TimeRegion = CachedTimeRegion;
|
||||
using GroupRegionName = CachedTimeRegion::GroupRegionName;
|
||||
|
||||
public:
|
||||
struct CpuTimingEntry
|
||||
{
|
||||
const AZStd::string& m_name;
|
||||
double m_executeDuration;
|
||||
};
|
||||
|
||||
ImGuiCpuProfiler() = default;
|
||||
~ImGuiCpuProfiler() = default;
|
||||
|
||||
//! Draws the overall CPU profiling window, defaults to the statistical view
|
||||
void Draw(bool& keepDrawing);
|
||||
|
||||
private:
|
||||
static constexpr float RowHeight = 35.0f;
|
||||
static constexpr int DefaultFramesToCollect = 50;
|
||||
static constexpr float MediumFrameTimeLimit = 16.6f; // 60 fps
|
||||
static constexpr float HighFrameTimeLimit = 33.3f; // 30 fps
|
||||
|
||||
//! Draws the statistical view of the CPU profiling data.
|
||||
void DrawStatisticsView();
|
||||
|
||||
//! Callback invoked when the "Load File" button is pressed in the file picker.
|
||||
void LoadFile();
|
||||
|
||||
//! Draws the file picker window.
|
||||
void DrawFilePicker();
|
||||
|
||||
//! Draws the CPU profiling visualizer.
|
||||
void DrawVisualizer();
|
||||
|
||||
// Draw the shared header between the two windows.
|
||||
void DrawCommonHeader();
|
||||
|
||||
// Draw the region statistics table in the order specified by the pointers in m_tableData.
|
||||
void DrawTable();
|
||||
|
||||
// Sort the table by a given column, rearranges the pointers in m_tableData.
|
||||
void SortTable(ImGuiTableSortSpecs* sortSpecs);
|
||||
|
||||
// gather the latest timing statistics
|
||||
void CacheCpuTimingStatistics();
|
||||
|
||||
// Get the profiling data from the last frame, only called when the profiler is not paused.
|
||||
void CollectFrameData();
|
||||
|
||||
// Cull old data from internal storage, only called when profiler is not paused.
|
||||
void CullFrameData();
|
||||
|
||||
// Draws a single block onto the timeline into the specified row
|
||||
void DrawBlock(const TimeRegion& block, AZ::u64 targetRow);
|
||||
|
||||
// Draw horizontal lines between threads in the timeline
|
||||
void DrawThreadSeparator(AZ::u64 threadBoundary, AZ::u64 maxDepth);
|
||||
|
||||
// Draw the "Thread XXXXX" label onto the viewport
|
||||
void DrawThreadLabel(AZ::u64 baseRow, size_t threadId);
|
||||
|
||||
// Draw the vertical lines separating frames in the timeline
|
||||
void DrawFrameBoundaries();
|
||||
|
||||
// Draw the ruler with frame time labels
|
||||
void DrawRuler();
|
||||
|
||||
// Draw the frame time histogram
|
||||
void DrawFrameTimeHistogram();
|
||||
|
||||
// Converts raw ticks to a pixel value suitable to give to ImDrawList, handles window scrolling
|
||||
float ConvertTickToPixelSpace(AZStd::sys_time_t tick, AZStd::sys_time_t leftBound, AZStd::sys_time_t rightBound) const;
|
||||
|
||||
AZStd::sys_time_t GetViewportTickWidth() const;
|
||||
|
||||
// Gets the color for a block using the GroupRegionName as a key into the cache.
|
||||
// Generates a random ImU32 if the block does not yet have a color.
|
||||
ImU32 GetBlockColor(const TimeRegion& block);
|
||||
|
||||
// System tick bus overrides
|
||||
void OnSystemTick() override;
|
||||
|
||||
// --- Visualizer Members ---
|
||||
|
||||
int m_framesToCollect = DefaultFramesToCollect;
|
||||
|
||||
// Tally of the number of saved profiling events so far
|
||||
AZ::u64 m_savedRegionCount = 0;
|
||||
|
||||
// Viewport tick bounds, these are used to convert tick space -> screen space and cull so we only draw onscreen objects
|
||||
AZStd::sys_time_t m_viewportStartTick;
|
||||
AZStd::sys_time_t m_viewportEndTick;
|
||||
|
||||
// Map to store each thread's TimeRegions, individual vectors are sorted by start tick
|
||||
// note: we use size_t as a proxy for thread_id because native_thread_id_type differs differs from
|
||||
// platform to platform, which causes problems when deserializing saved captures.
|
||||
AZStd::unordered_map<size_t, AZStd::vector<TimeRegion>> m_savedData;
|
||||
|
||||
// Region color cache
|
||||
AZStd::unordered_map<GroupRegionName, ImVec4, CachedTimeRegion::GroupRegionName::Hash> m_regionColorMap;
|
||||
|
||||
// Tracks the frame boundaries
|
||||
AZStd::vector<AZStd::sys_time_t> m_frameEndTicks = { INT64_MIN };
|
||||
|
||||
// Filter for highlighting regions on the visualizer
|
||||
ImGuiTextFilter m_visualizerHighlightFilter;
|
||||
|
||||
// --- Tabular view members ---
|
||||
|
||||
// ImGui filter used to filter TimedRegions.
|
||||
ImGuiTextFilter m_timedRegionFilter;
|
||||
|
||||
// Saves statistical view data organized by group name -> region name -> row data
|
||||
GroupRegionMap m_groupRegionMap;
|
||||
|
||||
// Saves pointers to objects in m_groupRegionMap, order reflects table ordering.
|
||||
// Non-owning, will be cleared when m_groupRegionMap is cleared.
|
||||
AZStd::vector<TableRow*> m_tableData;
|
||||
|
||||
// Pause cpu profiling. The profiler will show the statistics of the last frame before pause.
|
||||
bool m_paused = false;
|
||||
|
||||
// Export the profiling data from a single frame to a local file.
|
||||
bool m_captureToFile = false;
|
||||
|
||||
// Toggle between the normal statistical view and the visual profiling view.
|
||||
bool m_enableVisualizer = false;
|
||||
|
||||
// Last captured CPU timing statistics
|
||||
AZStd::vector<CpuTimingEntry> m_cpuTimingStatisticsWhenPause;
|
||||
AZStd::sys_time_t m_frameToFrameTime{};
|
||||
|
||||
AZStd::string m_lastCapturedFilePath;
|
||||
|
||||
bool m_showFilePicker = false;
|
||||
|
||||
// Cached file paths to previous traces on disk, sorted with the most recent trace at the front.
|
||||
AZStd::vector<AZ::IO::Path> m_cachedCapturePaths;
|
||||
|
||||
// Index into the file picker, used to determine which file to load when "Load File" is pressed.
|
||||
int m_currentFileIndex = 0;
|
||||
|
||||
|
||||
// --- Loading capture state ---
|
||||
AZStd::unordered_set<AZStd::string> m_deserializedStringPool;
|
||||
AZStd::unordered_set<CachedTimeRegion::GroupRegionName, CachedTimeRegion::GroupRegionName::Hash> m_deserializedGroupRegionNamePool;
|
||||
};
|
||||
} // namespace Profiler
|
||||
|
||||
#endif // defined(IMGUI_ENABLED)
|
||||
@ -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 <ProfilerImGuiSystemComponent.h>
|
||||
#include <ProfilerSystemComponent.h>
|
||||
|
||||
#include <AzCore/Memory/SystemAllocator.h>
|
||||
#include <AzCore/Module/Module.h>
|
||||
|
||||
namespace Profiler
|
||||
{
|
||||
class ProfilerImGuiModule
|
||||
: public AZ::Module
|
||||
{
|
||||
public:
|
||||
AZ_RTTI(ProfilerImGuiModule, "{5946991E-A96C-4E7A-A9B3-605E3C8EC3CB}", AZ::Module);
|
||||
AZ_CLASS_ALLOCATOR(ProfilerImGuiModule, AZ::SystemAllocator, 0);
|
||||
|
||||
ProfilerImGuiModule()
|
||||
{
|
||||
// Push results of [MyComponent]::CreateDescriptor() into m_descriptors here.
|
||||
// Add ALL components descriptors associated with this gem to m_descriptors.
|
||||
// This will associate the AzTypeInfo information for the components with the the SerializeContext, BehaviorContext and EditContext.
|
||||
// This happens through the [MyComponent]::Reflect() function.
|
||||
m_descriptors.insert(m_descriptors.end(), {
|
||||
ProfilerSystemComponent::CreateDescriptor(),
|
||||
ProfilerImGuiSystemComponent::CreateDescriptor(),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add required SystemComponents to the SystemEntity.
|
||||
*/
|
||||
AZ::ComponentTypeList GetRequiredSystemComponents() const override
|
||||
{
|
||||
return AZ::ComponentTypeList{
|
||||
azrtti_typeid<ProfilerSystemComponent>(),
|
||||
azrtti_typeid<ProfilerImGuiSystemComponent>(),
|
||||
};
|
||||
}
|
||||
};
|
||||
}// namespace Profiler
|
||||
|
||||
AZ_DECLARE_MODULE_CLASS(Gem_Profiler, Profiler::ProfilerImGuiModule)
|
||||
@ -0,0 +1,116 @@
|
||||
/*
|
||||
* Copyright (c) Contributors to the Open 3D Engine Project.
|
||||
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
*
|
||||
*/
|
||||
|
||||
#include <ProfilerImGuiSystemComponent.h>
|
||||
|
||||
#include <AzCore/Serialization/SerializeContext.h>
|
||||
#include <AzCore/Serialization/EditContext.h>
|
||||
#include <AzCore/Serialization/EditContextConstants.inl>
|
||||
|
||||
#include <AzFramework/Components/ConsoleBus.h>
|
||||
|
||||
namespace Profiler
|
||||
{
|
||||
static constexpr AZ::Crc32 profilerImGuiServiceCrc = AZ_CRC_CE("ProfilerImGuiService");
|
||||
|
||||
void ProfilerImGuiSystemComponent::Reflect(AZ::ReflectContext* context)
|
||||
{
|
||||
if (AZ::SerializeContext* serialize = azrtti_cast<AZ::SerializeContext*>(context))
|
||||
{
|
||||
serialize->Class<ProfilerImGuiSystemComponent, AZ::Component>()
|
||||
->Version(0);
|
||||
|
||||
if (AZ::EditContext* ec = serialize->GetEditContext())
|
||||
{
|
||||
ec->Class<ProfilerImGuiSystemComponent>("ProfilerImGui", "Provides in-game visualization of the performance data gathered by the ProfilerSystemComponent")
|
||||
->ClassElement(AZ::Edit::ClassElements::EditorData, "")
|
||||
->Attribute(AZ::Edit::Attributes::AppearsInAddComponentMenu, AZ_CRC_CE("System"))
|
||||
->Attribute(AZ::Edit::Attributes::AutoExpand, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ProfilerImGuiSystemComponent::GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided)
|
||||
{
|
||||
provided.push_back(profilerImGuiServiceCrc);
|
||||
}
|
||||
|
||||
void ProfilerImGuiSystemComponent::GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible)
|
||||
{
|
||||
incompatible.push_back(profilerImGuiServiceCrc);
|
||||
}
|
||||
|
||||
void ProfilerImGuiSystemComponent::GetRequiredServices([[maybe_unused]] AZ::ComponentDescriptor::DependencyArrayType& required)
|
||||
{
|
||||
}
|
||||
|
||||
void ProfilerImGuiSystemComponent::GetDependentServices([[maybe_unused]] AZ::ComponentDescriptor::DependencyArrayType& dependent)
|
||||
{
|
||||
}
|
||||
|
||||
ProfilerImGuiSystemComponent::ProfilerImGuiSystemComponent()
|
||||
{
|
||||
#if defined(IMGUI_ENABLED)
|
||||
if (ProfilerImGuiInterface::Get() == nullptr)
|
||||
{
|
||||
ProfilerImGuiInterface::Register(this);
|
||||
}
|
||||
#endif // defined(IMGUI_ENABLED)
|
||||
}
|
||||
|
||||
ProfilerImGuiSystemComponent::~ProfilerImGuiSystemComponent()
|
||||
{
|
||||
#if defined(IMGUI_ENABLED)
|
||||
if (ProfilerImGuiInterface::Get() == this)
|
||||
{
|
||||
ProfilerImGuiInterface::Unregister(this);
|
||||
}
|
||||
#endif // defined(IMGUI_ENABLED)
|
||||
}
|
||||
|
||||
void ProfilerImGuiSystemComponent::Activate()
|
||||
{
|
||||
#if defined(IMGUI_ENABLED)
|
||||
ImGui::ImGuiUpdateListenerBus::Handler::BusConnect();
|
||||
#endif // defined(IMGUI_ENABLED)
|
||||
}
|
||||
|
||||
void ProfilerImGuiSystemComponent::Deactivate()
|
||||
{
|
||||
#if defined(IMGUI_ENABLED)
|
||||
ImGui::ImGuiUpdateListenerBus::Handler::BusDisconnect();
|
||||
#endif // defined(IMGUI_ENABLED)
|
||||
}
|
||||
|
||||
#if defined(IMGUI_ENABLED)
|
||||
void ProfilerImGuiSystemComponent::ShowCpuProfilerWindow(bool& keepDrawing)
|
||||
{
|
||||
m_imguiCpuProfiler.Draw(keepDrawing);
|
||||
}
|
||||
|
||||
void ProfilerImGuiSystemComponent::OnImGuiUpdate()
|
||||
{
|
||||
if (m_showCpuProfiler)
|
||||
{
|
||||
ShowCpuProfilerWindow(m_showCpuProfiler);
|
||||
}
|
||||
}
|
||||
|
||||
void ProfilerImGuiSystemComponent::OnImGuiMainMenuUpdate()
|
||||
{
|
||||
if (ImGui::BeginMenu("Profiler"))
|
||||
{
|
||||
if (ImGui::MenuItem("CPU", "", &m_showCpuProfiler))
|
||||
{
|
||||
CpuProfiler::Get()->SetProfilerEnabled(m_showCpuProfiler);
|
||||
}
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
}
|
||||
#endif // defined(IMGUI_ENABLED)
|
||||
} // namespace Profiler
|
||||
@ -0,0 +1,65 @@
|
||||
/*
|
||||
* Copyright (c) Contributors to the Open 3D Engine Project.
|
||||
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
*
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <Profiler/ProfilerImGuiBus.h>
|
||||
|
||||
#include <ImGuiCpuProfiler.h>
|
||||
|
||||
#include <AzCore/Component/Component.h>
|
||||
|
||||
#if defined(IMGUI_ENABLED)
|
||||
#include <ImGuiBus.h>
|
||||
#include <imgui/imgui.h>
|
||||
#endif // defined(IMGUI_ENABLED)
|
||||
|
||||
namespace Profiler
|
||||
{
|
||||
class ProfilerImGuiSystemComponent
|
||||
: public AZ::Component
|
||||
#if defined(IMGUI_ENABLED)
|
||||
, public ProfilerImGuiRequests
|
||||
, public ImGui::ImGuiUpdateListenerBus::Handler
|
||||
#endif // defined(IMGUI_ENABLED)
|
||||
{
|
||||
public:
|
||||
AZ_COMPONENT(ProfilerImGuiSystemComponent, "{E59A8A53-6784-4CCB-A8B5-9F91DA9BF1C5}");
|
||||
|
||||
static void Reflect(AZ::ReflectContext* context);
|
||||
|
||||
static void GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided);
|
||||
static void GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible);
|
||||
static void GetRequiredServices(AZ::ComponentDescriptor::DependencyArrayType& required);
|
||||
static void GetDependentServices(AZ::ComponentDescriptor::DependencyArrayType& dependent);
|
||||
|
||||
ProfilerImGuiSystemComponent();
|
||||
~ProfilerImGuiSystemComponent();
|
||||
|
||||
protected:
|
||||
// AZ::Component interface implementation
|
||||
void Activate() override;
|
||||
void Deactivate() override;
|
||||
|
||||
#if defined(IMGUI_ENABLED)
|
||||
// ProfilerImGuiRequests interface implementation
|
||||
void ShowCpuProfilerWindow(bool& keepDrawing) override;
|
||||
|
||||
// ImGuiUpdateListenerBus overrides
|
||||
void OnImGuiUpdate() override;
|
||||
void OnImGuiMainMenuUpdate() override;
|
||||
#endif // defined(IMGUI_ENABLED)
|
||||
|
||||
private:
|
||||
#if defined(IMGUI_ENABLED)
|
||||
ImGuiCpuProfiler m_imguiCpuProfiler;
|
||||
bool m_showCpuProfiler{ false };
|
||||
#endif // defined(IMGUI_ENABLED)
|
||||
};
|
||||
|
||||
} // namespace Profiler
|
||||
@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright (c) Contributors to the Open 3D Engine Project.
|
||||
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
*
|
||||
*/
|
||||
|
||||
#include <ProfilerSystemComponent.h>
|
||||
|
||||
#include <AzCore/Memory/SystemAllocator.h>
|
||||
#include <AzCore/Module/Module.h>
|
||||
|
||||
namespace Profiler
|
||||
{
|
||||
class ProfilerModule
|
||||
: public AZ::Module
|
||||
{
|
||||
public:
|
||||
AZ_RTTI(ProfilerModule, "{4A286414-B387-4D20-9A7E-2F792755B769}", AZ::Module);
|
||||
AZ_CLASS_ALLOCATOR(ProfilerModule, AZ::SystemAllocator, 0);
|
||||
|
||||
ProfilerModule()
|
||||
{
|
||||
// Push results of [MyComponent]::CreateDescriptor() into m_descriptors here.
|
||||
// Add ALL components descriptors associated with this gem to m_descriptors.
|
||||
// This will associate the AzTypeInfo information for the components with the the SerializeContext, BehaviorContext and EditContext.
|
||||
// This happens through the [MyComponent]::Reflect() function.
|
||||
m_descriptors.insert(m_descriptors.end(), {
|
||||
ProfilerSystemComponent::CreateDescriptor(),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add required SystemComponents to the SystemEntity.
|
||||
*/
|
||||
AZ::ComponentTypeList GetRequiredSystemComponents() const override
|
||||
{
|
||||
return AZ::ComponentTypeList{
|
||||
azrtti_typeid<ProfilerSystemComponent>(),
|
||||
};
|
||||
}
|
||||
};
|
||||
}// namespace Profiler
|
||||
|
||||
AZ_DECLARE_MODULE_CLASS(Gem_Profiler, Profiler::ProfilerModule)
|
||||
@ -0,0 +1,284 @@
|
||||
/*
|
||||
* Copyright (c) Contributors to the Open 3D Engine Project.
|
||||
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
*
|
||||
*/
|
||||
|
||||
#include <ProfilerSystemComponent.h>
|
||||
|
||||
#include <AzCore/RTTI/BehaviorContext.h>
|
||||
#include <AzCore/Serialization/EditContext.h>
|
||||
#include <AzCore/Serialization/EditContextConstants.inl>
|
||||
#include <AzCore/Serialization/Json/JsonSerializationSettings.h>
|
||||
#include <AzCore/Serialization/Json/JsonUtils.h>
|
||||
#include <AzCore/Serialization/SerializeContext.h>
|
||||
|
||||
namespace Profiler
|
||||
{
|
||||
static constexpr AZ::Crc32 profilerServiceCrc = AZ_CRC_CE("ProfilerService");
|
||||
|
||||
struct DeplayedFunction
|
||||
{
|
||||
using func_type = AZStd::function<void()>;
|
||||
|
||||
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<AZ::BehaviorContext*>(context))
|
||||
{
|
||||
behaviorContext->EBus<ProfilerNotificationBus>("ProfilerNotificationBus")
|
||||
->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Automation)
|
||||
->Attribute(AZ::Script::Attributes::Module, "profiler")
|
||||
->Handler<ProfilerNotificationBusHandler>();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
bool SerializeCpuProfilingData(const AZStd::ring_buffer<CpuProfiler::TimeRegionMap>& 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<AZ::SerializeContext*>(context))
|
||||
{
|
||||
serialize->Class<ProfilerSystemComponent, AZ::Component>()
|
||||
->Version(0);
|
||||
|
||||
if (AZ::EditContext* ec = serialize->GetEditContext())
|
||||
{
|
||||
ec->Class<ProfilerSystemComponent>("Profiler", "Provides a custom implementation of the AZ::Debug::Profiler interface for capturing performance data")
|
||||
->ClassElement(AZ::Edit::ClassElements::EditorData, "")
|
||||
->Attribute(AZ::Edit::Attributes::AppearsInAddComponentMenu, AZ_CRC_CE("System"))
|
||||
->Attribute(AZ::Edit::Attributes::AutoExpand, true);
|
||||
|
||||
ProfilerNotificationBusHandler::Reflect(context);
|
||||
}
|
||||
}
|
||||
|
||||
if (AZ::BehaviorContext* behaviorContext = azrtti_cast<AZ::BehaviorContext*>(context))
|
||||
{
|
||||
behaviorContext->EBus<ProfilerRequestBus>("ProfilerRequestBus")
|
||||
->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Automation)
|
||||
->Attribute(AZ::Script::Attributes::Module, "profiler")
|
||||
->Event("CaptureCpuProfilingStatistics", &ProfilerRequestBus::Events::CaptureCpuProfilingStatistics);
|
||||
|
||||
ProfilerNotificationBusHandler::Reflect(context);
|
||||
}
|
||||
|
||||
CpuProfilingStatisticsSerializer::Reflect(context);
|
||||
}
|
||||
|
||||
void ProfilerSystemComponent::GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided)
|
||||
{
|
||||
provided.push_back(profilerServiceCrc);
|
||||
}
|
||||
|
||||
void ProfilerSystemComponent::GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible)
|
||||
{
|
||||
incompatible.push_back(profilerServiceCrc);
|
||||
}
|
||||
|
||||
void ProfilerSystemComponent::GetRequiredServices([[maybe_unused]] AZ::ComponentDescriptor::DependencyArrayType& required)
|
||||
{
|
||||
}
|
||||
|
||||
void ProfilerSystemComponent::GetDependentServices([[maybe_unused]] AZ::ComponentDescriptor::DependencyArrayType& dependent)
|
||||
{
|
||||
}
|
||||
|
||||
ProfilerSystemComponent::ProfilerSystemComponent()
|
||||
{
|
||||
if (ProfilerInterface::Get() == nullptr)
|
||||
{
|
||||
ProfilerInterface::Register(this);
|
||||
}
|
||||
}
|
||||
|
||||
ProfilerSystemComponent::~ProfilerSystemComponent()
|
||||
{
|
||||
if (ProfilerInterface::Get() == this)
|
||||
{
|
||||
ProfilerInterface::Unregister(this);
|
||||
}
|
||||
}
|
||||
|
||||
void ProfilerSystemComponent::Activate()
|
||||
{
|
||||
ProfilerRequestBus::Handler::BusConnect();
|
||||
|
||||
m_cpuProfiler.Init();
|
||||
}
|
||||
|
||||
void ProfilerSystemComponent::Deactivate()
|
||||
{
|
||||
m_cpuProfiler.Shutdown();
|
||||
|
||||
ProfilerRequestBus::Handler::BusDisconnect();
|
||||
|
||||
// Block deactivation until the IO thread has finished serializing the CPU data
|
||||
if (m_cpuDataSerializationThread.joinable())
|
||||
{
|
||||
m_cpuDataSerializationThread.join();
|
||||
}
|
||||
}
|
||||
|
||||
void ProfilerSystemComponent::SetProfilerEnabled(bool enabled)
|
||||
{
|
||||
m_cpuProfiler.SetProfilerEnabled(enabled);
|
||||
}
|
||||
|
||||
bool ProfilerSystemComponent::CaptureCpuProfilingStatistics(const AZStd::string& outputFilePath)
|
||||
{
|
||||
bool expected = false;
|
||||
if (!m_cpuCaptureInProgress.compare_exchange_strong(expected, true))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Start the cpu profiling
|
||||
bool wasEnabled = m_cpuProfiler.IsProfilerEnabled();
|
||||
if (!wasEnabled)
|
||||
{
|
||||
m_cpuProfiler.SetProfilerEnabled(true);
|
||||
}
|
||||
|
||||
const int frameDelay = 5; // arbitrary number
|
||||
DeplayedFunction delayedFunc(frameDelay,
|
||||
[this, outputFilePath, wasEnabled]()
|
||||
{
|
||||
// Blocking call for a single frame of data, avoid thread overhead
|
||||
AZStd::ring_buffer<CpuProfiler::TimeRegionMap> 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<CpuProfiler::TimeRegionMap> captureResult;
|
||||
const bool captureEnded = m_cpuProfiler.EndContinuousCapture(captureResult);
|
||||
if (!captureEnded)
|
||||
{
|
||||
AZ_TracePrintf("ProfilerSystemComponent", "Could not end the continuous capture, is one in progress?\n");
|
||||
m_cpuDataSerializationInProgress.store(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
// cpuProfilingData could be 1GB+ once saved, so use an IO thread to write it to disk.
|
||||
auto threadIoFunction =
|
||||
[data = AZStd::move(captureResult), filePath = AZStd::string(outputFilePath), &flag = m_cpuDataSerializationInProgress]()
|
||||
{
|
||||
SerializeCpuProfilingData(data, filePath, true);
|
||||
flag.store(false);
|
||||
};
|
||||
|
||||
// If the thread object already exists (ex. we have already serialized data), join. This will not block since
|
||||
// m_cpuDataSerializationInProgress was false, meaning the IO thread has already completed execution.
|
||||
if (m_cpuDataSerializationThread.joinable())
|
||||
{
|
||||
m_cpuDataSerializationThread.join();
|
||||
}
|
||||
|
||||
auto thread = AZStd::thread(threadIoFunction);
|
||||
m_cpuDataSerializationThread = AZStd::move(thread);
|
||||
|
||||
return true;
|
||||
}
|
||||
} // namespace Profiler
|
||||
@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Copyright (c) Contributors to the Open 3D Engine Project.
|
||||
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
*
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <Profiler/ProfilerBus.h>
|
||||
#include <CpuProfilerImpl.h>
|
||||
|
||||
#include <AzCore/Component/Component.h>
|
||||
#include <AzCore/std/parallel/thread.h>
|
||||
|
||||
namespace Profiler
|
||||
{
|
||||
class ProfilerSystemComponent
|
||||
: public AZ::Component
|
||||
, protected ProfilerRequestBus::Handler
|
||||
{
|
||||
public:
|
||||
AZ_COMPONENT(ProfilerSystemComponent, "{3f52c1d7-d920-4781-8ed7-88077ec4f305}");
|
||||
|
||||
static void Reflect(AZ::ReflectContext* context);
|
||||
|
||||
static void GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided);
|
||||
static void GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible);
|
||||
static void GetRequiredServices(AZ::ComponentDescriptor::DependencyArrayType& required);
|
||||
static void GetDependentServices(AZ::ComponentDescriptor::DependencyArrayType& dependent);
|
||||
|
||||
ProfilerSystemComponent();
|
||||
~ProfilerSystemComponent();
|
||||
|
||||
protected:
|
||||
// AZ::Component interface implementation
|
||||
void Activate() override;
|
||||
void Deactivate() override;
|
||||
|
||||
// ProfilerRequestBus interface implementation
|
||||
void SetProfilerEnabled(bool enabled) override;
|
||||
bool CaptureCpuProfilingStatistics(const AZStd::string& outputFilePath) override;
|
||||
bool BeginContinuousCpuProfilingCapture() override;
|
||||
bool EndContinuousCpuProfilingCapture(const AZStd::string& outputFilePath) override;
|
||||
|
||||
|
||||
AZStd::thread m_cpuDataSerializationThread;
|
||||
AZStd::atomic_bool m_cpuDataSerializationInProgress{ false };
|
||||
|
||||
AZStd::atomic_bool m_cpuCaptureInProgress{ false };
|
||||
|
||||
CpuProfilerImpl m_cpuProfiler;
|
||||
};
|
||||
|
||||
} // namespace Profiler
|
||||
@ -0,0 +1,17 @@
|
||||
#
|
||||
# Copyright (c) Contributors to the Open 3D Engine Project.
|
||||
# For complete copyright and license terms please see the LICENSE at the root of this distribution.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
#
|
||||
#
|
||||
|
||||
set(FILES
|
||||
Include/Profiler/ProfilerBus.h
|
||||
Include/Profiler/ProfilerImGuiBus.h
|
||||
Source/CpuProfiler.h
|
||||
Source/CpuProfilerImpl.cpp
|
||||
Source/CpuProfilerImpl.h
|
||||
Source/ProfilerSystemComponent.cpp
|
||||
Source/ProfilerSystemComponent.h
|
||||
)
|
||||
@ -0,0 +1,15 @@
|
||||
#
|
||||
# Copyright (c) Contributors to the Open 3D Engine Project.
|
||||
# For complete copyright and license terms please see the LICENSE at the root of this distribution.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
#
|
||||
#
|
||||
|
||||
set(FILES
|
||||
Source/ImGuiCpuProfiler.cpp
|
||||
Source/ImGuiCpuProfiler.h
|
||||
Source/ProfilerImGuiModule.cpp
|
||||
Source/ProfilerImGuiSystemComponent.cpp
|
||||
Source/ProfilerImGuiSystemComponent.h
|
||||
)
|
||||
@ -0,0 +1,11 @@
|
||||
#
|
||||
# Copyright (c) Contributors to the Open 3D Engine Project.
|
||||
# For complete copyright and license terms please see the LICENSE at the root of this distribution.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
#
|
||||
#
|
||||
|
||||
set(FILES
|
||||
Source/ProfilerModule.cpp
|
||||
)
|
||||
@ -0,0 +1,19 @@
|
||||
{
|
||||
"gem_name": "Profiler",
|
||||
"display_name": "Profiler",
|
||||
"license": "Apache-2.0 Or MIT",
|
||||
"origin": "Open 3D Engine - o3de.org",
|
||||
"type": "Code",
|
||||
"summary": "A collection of utilities for capturing performance data",
|
||||
"canonical_tags": [
|
||||
"Gem"
|
||||
],
|
||||
"user_tags": [
|
||||
"Profiler"
|
||||
],
|
||||
"icon_path": "preview.png",
|
||||
"requirements": "",
|
||||
"dependencies": [
|
||||
"ImGui"
|
||||
]
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:7ac9dd09bde78f389e3725ac49d61eff109857e004840bc0bc3881739df9618d
|
||||
size 2217
|
||||
Loading…
Reference in New Issue