[development] Consolidated programmatic profiler capture API (#4969)

Merged overlapping profiler EBuses/Interfaces into AzCore
Fixed ambiguous LogLevel type in some unity file scenarios
Added cvar/console access for profiler capture
Added utilities for reflecting AZ::Interfaces through the BehaviorContext
Included profiler system sample python script

Signed-off-by: AMZN-ScottR 24445312+AMZN-ScottR@users.noreply.github.com
monroegm-disable-blank-issue-2
Scott Romero 4 years ago committed by GitHub
commit 797b8248e5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -0,0 +1,7 @@
#
# 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
#
#

@ -0,0 +1,30 @@
#
# 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
#
#
import azlmbr.debug as debug
import pathlib
def test_profiler_system():
if not debug.g_ProfilerSystem.IsValid():
print('g_ProfilerSystem is INVALID')
return
state = 'ACTIVE' if debug.g_ProfilerSystem.IsActive() else 'INACTIVE'
print(f'Profiler system is currently {state}')
capture_location = pathlib.Path(debug.g_ProfilerSystem.GetCaptureLocation())
print(f'Capture location set to {capture_location}')
print('Capturing single frame...' )
capture_file = str(capture_location / 'script_capture_frame.json')
debug.g_ProfilerSystem.CaptureFrame(capture_file)
# Invoke main function
if __name__ == '__main__':
test_profiler_system()

@ -7,4 +7,63 @@
*/ */
#include <AzCore/Debug/Profiler.h> #include <AzCore/Debug/Profiler.h>
#include <AzCore/Debug/ProfilerBus.h>
#include <AzCore/Console/IConsole.h>
#include <AzCore/Console/ILogger.h>
#include <AzCore/Settings/SettingsRegistry.h>
namespace AZ::Debug
{
AZStd::string GenerateOutputFile(const char* nameHint)
{
AZ::IO::FixedMaxPathString captureOutput = GetProfilerCaptureLocation();
return AZStd::string::format("%s/capture_%s_%lld.json", captureOutput.c_str(), nameHint, AZStd::GetTimeNowSecond());
}
void ProfilerCaptureFrame([[maybe_unused]] const AZ::ConsoleCommandContainer& arguments)
{
if (auto profilerSystem = ProfilerSystemInterface::Get(); profilerSystem)
{
AZStd::string captureFile = GenerateOutputFile("single");
AZLOG_INFO("Setting capture file to %s", captureFile.c_str());
profilerSystem->CaptureFrame(captureFile);
}
}
AZ_CONSOLEFREEFUNC(ProfilerCaptureFrame, AZ::ConsoleFunctorFlags::DontReplicate, "Capture a single frame of profiling data");
void ProfilerStartCapture([[maybe_unused]] const AZ::ConsoleCommandContainer& arguments)
{
if (auto profilerSystem = ProfilerSystemInterface::Get(); profilerSystem)
{
AZStd::string captureFile = GenerateOutputFile("multi");
AZLOG_INFO("Setting capture file to %s", captureFile.c_str());
profilerSystem->StartCapture(AZStd::move(captureFile));
}
}
AZ_CONSOLEFREEFUNC(ProfilerStartCapture, AZ::ConsoleFunctorFlags::DontReplicate, "Start a multi-frame capture of profiling data");
void ProfilerEndCapture([[maybe_unused]] const AZ::ConsoleCommandContainer& arguments)
{
if (auto profilerSystem = ProfilerSystemInterface::Get(); profilerSystem)
{
profilerSystem->EndCapture();
}
}
AZ_CONSOLEFREEFUNC(ProfilerEndCapture, AZ::ConsoleFunctorFlags::DontReplicate, "End and dump an in-progress continuous capture");
AZ::IO::FixedMaxPathString GetProfilerCaptureLocation()
{
AZ::IO::FixedMaxPathString captureOutput;
if (AZ::SettingsRegistryInterface* settingsRegistry = AZ::SettingsRegistry::Get(); settingsRegistry)
{
settingsRegistry->Get(captureOutput, RegistryKey_ProfilerCaptureLocation);
}
if (captureOutput.empty())
{
captureOutput = ProfilerCaptureLocationFallback;
}
return captureOutput;
}
} // namespace AZ::Debug

@ -9,11 +9,20 @@
#pragma once #pragma once
#include <AzCore/EBus/EBus.h> #include <AzCore/EBus/EBus.h>
#include <AzCore/Interface/Interface.h>
#include <AzCore/IO/Path/Path_fwd.h>
#include <AzCore/std/string/string.h>
namespace AZ namespace AZ
{ {
namespace Debug namespace Debug
{ {
//! settings registry entry for specifying where to output profiler captures
static constexpr const char* RegistryKey_ProfilerCaptureLocation = "/O3DE/AzCore/Debug/Profiler/CaptureLocation";
//! fallback value in the event the settings registry isn't ready or doesn't contain the key
static constexpr const char* ProfilerCaptureLocationFallback = "@user@/Profiler";
/** /**
* ProfilerNotifications provides a profiler event interface that can be used to update listeners on profiler status * ProfilerNotifications provides a profiler event interface that can be used to update listeners on profiler status
*/ */
@ -23,32 +32,38 @@ namespace AZ
public: public:
virtual ~ProfilerNotifications() = default; virtual ~ProfilerNotifications() = default;
virtual void OnProfileSystemInitialized() = 0; //! Notify when the current profiler 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 OnCaptureFinished(bool result, const AZStd::string& info) = 0;
}; };
using ProfilerNotificationBus = AZ::EBus<ProfilerNotifications>; using ProfilerNotificationBus = AZ::EBus<ProfilerNotifications>;
enum class ProfileFrameAdvanceType
{
Game,
Render,
Default = Game
};
/** /**
* ProfilerRequests provides an interface for making profiling system requests * ProfilerRequests provides an interface for making profiling system requests
*/ */
class ProfilerRequests class ProfilerRequests
: public AZ::EBusTraits
{ {
public: public:
// Allow multiple threads to concurrently make requests AZ_RTTI(ProfilerRequests, "{90AEC117-14C1-4BAE-9704-F916E49EF13F}");
using MutexType = AZStd::mutex;
virtual ~ProfilerRequests() = default; virtual ~ProfilerRequests() = default;
virtual bool IsActive() = 0; //! Getter/setter for the profiler active state
virtual void FrameAdvance(ProfileFrameAdvanceType type) = 0; virtual bool IsActive() const = 0;
virtual void SetActive(bool active) = 0;
//! Capture a single frame of profiling data
virtual bool CaptureFrame(const AZStd::string& outputFilePath) = 0;
//! Starting/ending a multi-frame capture of profiling data
virtual bool StartCapture(AZStd::string outputFilePath) = 0;
virtual bool EndCapture() = 0;
}; };
using ProfilerRequestBus = AZ::EBus<ProfilerRequests>;
} using ProfilerSystemInterface = AZ::Interface<ProfilerRequests>;
}
//! helper function for getting the profiler capture location from the settings registry that
//! includes fallback handing in the event the registry value can't be determined
AZ::IO::FixedMaxPathString GetProfilerCaptureLocation();
} // namespace Debug
} // namespace AZ

@ -0,0 +1,92 @@
/*
* 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 <AzCore/Debug/ProfilerBus.h>
#include <AzCore/RTTI/BehaviorContext.h>
#include <AzCore/RTTI/BehaviorInterfaceProxy.h>
#include <AzCore/Serialization/EditContext.h>
#include <AzCore/Serialization/EditContextConstants.inl>
namespace AZ::Debug
{
static constexpr const char* ProfilerScriptCategory = "Profiler";
static constexpr const char* ProfilerScriptModule = "debug";
static constexpr AZ::Script::Attributes::ScopeFlags ProfilerScriptScope = AZ::Script::Attributes::ScopeFlags::Automation;
class ProfilerNotificationBusHandler final
: public ProfilerNotificationBus::Handler
, public AZ::BehaviorEBusHandler
{
public:
AZ_EBUS_BEHAVIOR_BINDER(ProfilerNotificationBusHandler, "{44161459-B816-4876-95A4-BA16DEC767D6}", AZ::SystemAllocator,
OnCaptureFinished
);
void OnCaptureFinished(bool result, const AZStd::string& info) override
{
Call(FN_OnCaptureFinished, 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::Category, ProfilerScriptCategory)
->Attribute(AZ::Script::Attributes::Module, ProfilerScriptModule)
->Attribute(AZ::Script::Attributes::Scope, ProfilerScriptScope)
->Handler<ProfilerNotificationBusHandler>();
}
}
};
class ProfilerSystemScriptProxy
: public BehaviorInterfaceProxy<ProfilerRequests>
{
public:
AZ_RTTI(ProfilerSystemScriptProxy, "{D671FB70-8B09-4C3A-96CD-06A339F3138E}", BehaviorInterfaceProxy<ProfilerRequests>);
AZ_BEHAVIOR_INTERFACE(ProfilerSystemScriptProxy, ProfilerRequests);
};
void ProfilerReflect(AZ::ReflectContext* context)
{
if (AZ::BehaviorContext* behaviorContext = azrtti_cast<AZ::BehaviorContext*>(context))
{
behaviorContext->ConstantProperty("g_ProfilerSystem", ProfilerSystemScriptProxy::GetProxy)
->Attribute(AZ::Script::Attributes::Category, ProfilerScriptCategory)
->Attribute(AZ::Script::Attributes::Module, ProfilerScriptModule)
->Attribute(AZ::Script::Attributes::Scope, ProfilerScriptScope);
behaviorContext->Class<ProfilerSystemScriptProxy>("ProfilerSystemInterface")
->Attribute(AZ::Script::Attributes::Category, ProfilerScriptCategory)
->Attribute(AZ::Script::Attributes::Module, ProfilerScriptModule)
->Attribute(AZ::Script::Attributes::Scope, ProfilerScriptScope)
->Method("IsValid", &ProfilerSystemScriptProxy::IsValid)
->Method("GetCaptureLocation",
[](ProfilerSystemScriptProxy*) -> AZStd::string
{
AZ::IO::FixedMaxPathString captureOutput = GetProfilerCaptureLocation();
return AZStd::string(captureOutput.c_str(), captureOutput.length());
})
->Method("IsActive", ProfilerSystemScriptProxy::WrapMethod<&ProfilerRequests::IsActive>())
->Method("SetActive", ProfilerSystemScriptProxy::WrapMethod<&ProfilerRequests::SetActive>())
->Method("CaptureFrame", ProfilerSystemScriptProxy::WrapMethod<&ProfilerRequests::CaptureFrame>())
->Method("StartCapture", ProfilerSystemScriptProxy::WrapMethod<&ProfilerRequests::StartCapture>())
->Method("EndCapture", ProfilerSystemScriptProxy::WrapMethod<&ProfilerRequests::EndCapture>());
}
ProfilerNotificationBusHandler::Reflect(context);
}
} // namespace AZ::Debug

@ -0,0 +1,19 @@
/*
* 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
namespace AZ
{
class ReflectContext;
namespace Debug
{
//! Reflects the profiler bus script bindings
void ProfilerReflect(AZ::ReflectContext* context);
} // namespace Debug
} // namespace AZ

@ -27,26 +27,21 @@
#include <AzCore/Console/IConsole.h> #include <AzCore/Console/IConsole.h>
#include <AzCore/std/chrono/chrono.h> #include <AzCore/std/chrono/chrono.h>
namespace AZ namespace AZ::Debug
{ {
namespace Debug struct StackFrame;
{
struct StackFrame;
namespace Platform namespace Platform
{ {
#if defined(AZ_ENABLE_DEBUG_TOOLS) #if defined(AZ_ENABLE_DEBUG_TOOLS)
bool AttachDebugger(); bool AttachDebugger();
bool IsDebuggerPresent(); bool IsDebuggerPresent();
void HandleExceptions(bool isEnabled); void HandleExceptions(bool isEnabled);
void DebugBreak(); void DebugBreak();
#endif #endif
void Terminate(int exitCode); void Terminate(int exitCode);
}
} }
using namespace AZ::Debug;
namespace DebugInternal namespace DebugInternal
{ {
// other threads can trigger fatals and errors, but the same thread should not, to avoid stack overflow. // other threads can trigger fatals and errors, but the same thread should not, to avoid stack overflow.
@ -60,7 +55,7 @@ namespace AZ
// Globals // Globals
const int g_maxMessageLength = 4096; const int g_maxMessageLength = 4096;
static const char* g_dbgSystemWnd = "System"; static const char* g_dbgSystemWnd = "System";
Trace Debug::g_tracer; Trace g_tracer;
void* g_exceptionInfo = nullptr; void* g_exceptionInfo = nullptr;
// Environment var needed to track ignored asserts across systems and disable native UI under certain conditions // Environment var needed to track ignored asserts across systems and disable native UI under certain conditions
@ -616,4 +611,4 @@ namespace AZ
val.Set(level); val.Set(level);
} }
} }
} // namspace AZ } // namspace AZ::Debug

@ -156,7 +156,10 @@ namespace AZ
void StreamerComponent::OnTick([[maybe_unused]] float deltaTime, [[maybe_unused]] AZ::ScriptTimePoint time) void StreamerComponent::OnTick([[maybe_unused]] float deltaTime, [[maybe_unused]] AZ::ScriptTimePoint time)
{ {
bool isEnabled = false; bool isEnabled = false;
AZ::Debug::ProfilerRequestBus::BroadcastResult(isEnabled, &AZ::Debug::ProfilerRequests::IsActive); if (auto profilerSystem = AZ::Debug::ProfilerSystemInterface::Get(); profilerSystem)
{
isEnabled = profilerSystem->IsActive();
}
if (isEnabled) if (isEnabled)
{ {

@ -0,0 +1,126 @@
/*
* 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 <AzCore/Interface/Interface.h>
#include <AzCore/Memory/SystemAllocator.h>
#include <AzCore/RTTI/RTTI.h>
#include <AzCore/std/functional.h>
#include <AzCore/std/typetraits/function_traits.h>
namespace AZ
{
/**
* Utility class for reflecting an AZ::Interface through the BehaviorContext
*
* Example:
*
* class MyInterface
* {
* public:
* AZ_RTTI(MyInterface, "{BADDF000D-CDCD-CDCD-CDCD-BAAAADF0000D}");
* virtual ~MyInterface() = default;
*
* virtual AZStd::string Foo() = 0;
* virtual void Bar(float x, float y) = 0;
* };
*
* class MySystemProxy
* : public BehaviorInterfaceProxy<MyInterface>
* {
* public:
* AZ_RTTI(MySystemProxy, "{CDCDCDCD-BAAD-BADD-F00D-CDCDCDCDCDCD}", BehaviorInterfaceProxy<MyInterface>);
* AZ_BEHAVIOR_INTERFACE(MySystemProxy, MyInterface);
* };
*
* void Reflect(AZ::ReflectContext* context)
* {
* if (AZ::BehaviorContext* behaviorContext = azrtti_cast<AZ::BehaviorContext*>(context))
* {
* behaviorContext->ConstantProperty("g_MySystem", MySystemProxy::GetProxy)
* ->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Common)
* ->Attribute(AZ::Script::Attributes::Module, "MyModule");
*
* behaviorContext->Class<MySystemProxy>("MySystemInterface")
* ->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Common)
* ->Attribute(AZ::Script::Attributes::Module, "MyModule")
*
* ->Method("Foo", MySystemProxy::WrapMethod<&MyInterface::Foo>())
* ->Method("Bar", MySystemProxy::WrapMethod<&MyInterface::Bar>());
* }
* }
*/
template<typename T>
class BehaviorInterfaceProxy
{
public:
AZ_CLASS_ALLOCATOR(BehaviorInterfaceProxy, AZ::SystemAllocator, 0);
AZ_RTTI(BehaviorInterfaceProxy<T>, "{E7CC8D27-4499-454E-A7DF-3F72FBECD30D}");
BehaviorInterfaceProxy() = default;
virtual ~BehaviorInterfaceProxy() = default;
//! Stores the instance which will use the provided shared_ptr deleter when the reference count hits zero
BehaviorInterfaceProxy(AZStd::shared_ptr<T> sharedInstance)
: m_instance(AZStd::move(sharedInstance))
{
}
//! Stores the instance which will perform a no-op deleter when the reference count hits zero
BehaviorInterfaceProxy(T* rawIntance)
: m_instance(rawIntance, [](T*) {})
{
}
//! Returns if the m_instance shared pointer is non-nullptr
bool IsValid() const { return m_instance; }
protected:
//! Internal access for use in the derived GetProxy function
static T* GetInstance()
{
T* interfacePtr = AZ::Interface<T>::Get();
AZ_Warning("BehaviorInterfaceProxy", interfacePtr,
"There is currently no global %s registered with an AZ Interface<T>",
AzTypeInfo<T>::Name()
);
// Don't delete the global instance, it is not owned by the behavior context
return interfacePtr;
}
template<typename... Args>
struct MethodWrapper
{
template<typename Proxy, auto Method>
static auto WrapMethod()
{
using ReturnType = AZStd::function_traits_get_result_t<AZStd::remove_cvref_t<decltype(Method)>>;
return [](Proxy* proxy, Args... params) -> ReturnType
{
if (proxy && proxy->IsValid())
{
return AZStd::invoke(Method, proxy->m_instance, AZStd::forward<Args>(params)...);
}
return ReturnType();
};
}
};
AZStd::shared_ptr<T> m_instance;
};
#define AZ_BEHAVIOR_INTERFACE(ProxyType, InterfaceType) \
static ProxyType GetProxy() { return GetInstance(); } \
template<auto Method> \
static auto WrapMethod() { \
using FuncTraits = AZStd::function_traits<AZStd::remove_cvref_t<decltype(Method)>>; \
return FuncTraits::template expand_args<MethodWrapper>::template WrapMethod<ProxyType, Method>(); \
} \
ProxyType() = default; \
ProxyType(AZStd::shared_ptr<InterfaceType> sharedInstance) : BehaviorInterfaceProxy(sharedInstance) {} \
ProxyType(InterfaceType* rawIntance) : BehaviorInterfaceProxy(rawIntance) {}
} // namespace AZ

@ -14,6 +14,7 @@
#include <AzCore/Component/ComponentApplication.h> #include <AzCore/Component/ComponentApplication.h>
#include <AzCore/Component/Entity.h> #include <AzCore/Component/Entity.h>
#include <AzCore/Component/TickBus.h> #include <AzCore/Component/TickBus.h>
#include <AzCore/Debug/ProfilerReflection.h>
#include <AzCore/Debug/TraceReflection.h> #include <AzCore/Debug/TraceReflection.h>
#include <AzCore/IO/FileIO.h> #include <AzCore/IO/FileIO.h>
#include <AzCore/Math/MathReflection.h> #include <AzCore/Math/MathReflection.h>
@ -925,6 +926,7 @@ void ScriptSystemComponent::Reflect(ReflectContext* reflection)
// reflect default entity // reflect default entity
MathReflect(behaviorContext); MathReflect(behaviorContext);
ScriptDebug::Reflect(behaviorContext); ScriptDebug::Reflect(behaviorContext);
Debug::ProfilerReflect(behaviorContext);
Debug::TraceReflect(behaviorContext); Debug::TraceReflect(behaviorContext);
behaviorContext->Class<PlatformID>("Platform") behaviorContext->Class<PlatformID>("Platform")

@ -108,6 +108,8 @@ set(FILES
Debug/Profiler.inl Debug/Profiler.inl
Debug/Profiler.h Debug/Profiler.h
Debug/ProfilerBus.h Debug/ProfilerBus.h
Debug/ProfilerReflection.cpp
Debug/ProfilerReflection.h
Debug/StackTracer.h Debug/StackTracer.h
Debug/EventTrace.h Debug/EventTrace.h
Debug/EventTrace.cpp Debug/EventTrace.cpp
@ -456,6 +458,7 @@ set(FILES
RTTI/BehaviorContext.h RTTI/BehaviorContext.h
RTTI/BehaviorContextUtilities.h RTTI/BehaviorContextUtilities.h
RTTI/BehaviorContextUtilities.cpp RTTI/BehaviorContextUtilities.cpp
RTTI/BehaviorInterfaceProxy.h
RTTI/BehaviorObjectSignals.h RTTI/BehaviorObjectSignals.h
RTTI/TypeSafeIntegral.h RTTI/TypeSafeIntegral.h
Script/ScriptAsset.cpp Script/ScriptAsset.cpp

@ -17,7 +17,7 @@
#include <stdio.h> #include <stdio.h>
namespace AZ namespace AZ::Debug
{ {
#if defined(AZ_ENABLE_DEBUG_TOOLS) #if defined(AZ_ENABLE_DEBUG_TOOLS)
LONG WINAPI ExceptionHandler(PEXCEPTION_POINTERS ExceptionInfo); LONG WINAPI ExceptionHandler(PEXCEPTION_POINTERS ExceptionInfo);
@ -26,94 +26,91 @@ namespace AZ
constexpr int g_maxMessageLength = 4096; constexpr int g_maxMessageLength = 4096;
namespace Debug namespace Platform
{ {
namespace Platform
{
#if defined(AZ_ENABLE_DEBUG_TOOLS) #if defined(AZ_ENABLE_DEBUG_TOOLS)
bool IsDebuggerPresent() bool IsDebuggerPresent()
{
return ::IsDebuggerPresent() ? true : false;
}
void HandleExceptions(bool isEnabled)
{
if (isEnabled)
{ {
return ::IsDebuggerPresent() ? true : false; g_previousExceptionHandler = ::SetUnhandledExceptionFilter(&ExceptionHandler);
} }
else
void HandleExceptions(bool isEnabled)
{ {
if (isEnabled) ::SetUnhandledExceptionFilter(g_previousExceptionHandler);
{ g_previousExceptionHandler = NULL;
g_previousExceptionHandler = ::SetUnhandledExceptionFilter(&ExceptionHandler);
}
else
{
::SetUnhandledExceptionFilter(g_previousExceptionHandler);
g_previousExceptionHandler = NULL;
}
} }
}
bool AttachDebugger() bool AttachDebugger()
{
if (IsDebuggerPresent())
{ {
if (IsDebuggerPresent()) return true;
{
return true;
}
// Launch vsjitdebugger.exe, this app is always present in System32 folder
// with an installation of any version of visual studio.
// It will open a debugging dialog asking the user what debugger to use
STARTUPINFOW startupInfo = {0};
startupInfo.cb = sizeof(startupInfo);
PROCESS_INFORMATION processInfo = {0};
wchar_t cmdline[MAX_PATH];
swprintf_s(cmdline, L"vsjitdebugger.exe -p %li", ::GetCurrentProcessId());
bool success = ::CreateProcessW(
NULL, // No module name (use command line)
cmdline, // Command line
NULL, // Process handle not inheritable
NULL, // Thread handle not inheritable
FALSE, // No handle inheritance
0, // No creation flags
NULL, // Use parent's environment block
NULL, // Use parent's starting directory
&startupInfo, // Pointer to STARTUPINFO structure
&processInfo); // Pointer to PROCESS_INFORMATION structure
if (success)
{
::WaitForSingleObject(processInfo.hProcess, INFINITE);
::CloseHandle(processInfo.hProcess);
::CloseHandle(processInfo.hThread);
return true;
}
return false;
} }
void DebugBreak() // Launch vsjitdebugger.exe, this app is always present in System32 folder
// with an installation of any version of visual studio.
// It will open a debugging dialog asking the user what debugger to use
STARTUPINFOW startupInfo = {0};
startupInfo.cb = sizeof(startupInfo);
PROCESS_INFORMATION processInfo = {0};
wchar_t cmdline[MAX_PATH];
swprintf_s(cmdline, L"vsjitdebugger.exe -p %li", ::GetCurrentProcessId());
bool success = ::CreateProcessW(
NULL, // No module name (use command line)
cmdline, // Command line
NULL, // Process handle not inheritable
NULL, // Thread handle not inheritable
FALSE, // No handle inheritance
0, // No creation flags
NULL, // Use parent's environment block
NULL, // Use parent's starting directory
&startupInfo, // Pointer to STARTUPINFO structure
&processInfo); // Pointer to PROCESS_INFORMATION structure
if (success)
{ {
__debugbreak(); ::WaitForSingleObject(processInfo.hProcess, INFINITE);
::CloseHandle(processInfo.hProcess);
::CloseHandle(processInfo.hThread);
return true;
} }
return false;
}
void DebugBreak()
{
__debugbreak();
}
#endif // AZ_ENABLE_DEBUG_TOOLS #endif // AZ_ENABLE_DEBUG_TOOLS
void Terminate(int exitCode) void Terminate(int exitCode)
{ {
TerminateProcess(GetCurrentProcess(), exitCode); TerminateProcess(GetCurrentProcess(), exitCode);
} }
void OutputToDebugger([[maybe_unused]] const char* window, const char* message) void OutputToDebugger([[maybe_unused]] const char* window, const char* message)
{
AZStd::fixed_wstring<g_maxMessageLength> tmpW;
if(window)
{ {
AZStd::fixed_wstring<g_maxMessageLength> tmpW; AZStd::to_wstring(tmpW, window);
if(window) tmpW += L": ";
{
AZStd::to_wstring(tmpW, window);
tmpW += L": ";
OutputDebugStringW(tmpW.c_str());
tmpW.clear();
}
AZStd::to_wstring(tmpW, message);
OutputDebugStringW(tmpW.c_str()); OutputDebugStringW(tmpW.c_str());
tmpW.clear();
} }
AZStd::to_wstring(tmpW, message);
OutputDebugStringW(tmpW.c_str());
} }
} } // namespace Platform
#if defined(AZ_ENABLE_DEBUG_TOOLS) #if defined(AZ_ENABLE_DEBUG_TOOLS)
@ -211,4 +208,4 @@ namespace AZ
} }
#endif #endif
} } // namspace AZ::Debug

@ -16,7 +16,7 @@
#include <CryAssert.h> #include <CryAssert.h>
namespace AZ namespace AZ::Debug
{ {
AZ_CVAR_EXTERNED(int, bg_traceLogLevel); AZ_CVAR_EXTERNED(int, bg_traceLogLevel);
} }
@ -64,7 +64,7 @@ public:
if(!hasSetCVar && ready) if(!hasSetCVar && ready)
{ {
// AZ logging only has a concept of 3 levels (error, warning, info) but cry logging has 4 levels (..., messaging). If info level is set, we'll turn on messaging as well // AZ logging only has a concept of 3 levels (error, warning, info) but cry logging has 4 levels (..., messaging). If info level is set, we'll turn on messaging as well
int logLevel = AZ::bg_traceLogLevel == AZ::Debug::LogLevel::Info ? 4 : AZ::bg_traceLogLevel; int logLevel = AZ::Debug::bg_traceLogLevel == AZ::Debug::LogLevel::Info ? 4 : AZ::Debug::bg_traceLogLevel;
gEnv->pConsole->GetCVar("log_WriteToFileVerbosity")->Set(logLevel); gEnv->pConsole->GetCVar("log_WriteToFileVerbosity")->Set(logLevel);
hasSetCVar = true; hasSetCVar = true;

@ -1175,7 +1175,10 @@ namespace PhysX
using physx::PxGeometryType; using physx::PxGeometryType;
bool isProfilingActive = false; bool isProfilingActive = false;
AZ::Debug::ProfilerRequestBus::BroadcastResult(isProfilingActive, &AZ::Debug::ProfilerRequests::IsActive); if (auto profilerSystem = AZ::Debug::ProfilerSystemInterface::Get(); profilerSystem)
{
isProfilingActive = profilerSystem->IsActive();
}
if (!isProfilingActive) if (!isProfilingActive)
{ {

@ -1,59 +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/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

@ -10,9 +10,9 @@
#include <ImGuiCpuProfiler.h> #include <ImGuiCpuProfiler.h>
#include <Profiler/ProfilerBus.h>
#include <CpuProfilerImpl.h> #include <CpuProfilerImpl.h>
#include <AzCore/Debug/ProfilerBus.h>
#include <AzCore/IO/FileIO.h> #include <AzCore/IO/FileIO.h>
#include <AzCore/JSON/filereadstream.h> #include <AzCore/JSON/filereadstream.h>
#include <AzCore/Outcome/Outcome.h> #include <AzCore/Outcome/Outcome.h>
@ -26,8 +26,6 @@
namespace Profiler namespace Profiler
{ {
static constexpr const char* defaultSaveLocation = "@user@/Profiler";
namespace CpuProfilerImGuiHelper namespace CpuProfilerImGuiHelper
{ {
float TicksToMs(double ticks) float TicksToMs(double ticks)
@ -156,16 +154,7 @@ namespace Profiler
if (m_captureToFile) if (m_captureToFile)
{ {
AZStd::string timeString; AZ::Debug::ProfilerSystemInterface::Get()->CaptureFrame(GenerateOutputFile("single"));
AZStd::to_string(timeString, AZStd::GetTimeNowSecond());
const AZStd::string frameDataFilePath = AZStd::string::format("%s/cpu_single_%s.json", defaultSaveLocation, timeString.c_str());
char resolvedPath[AZ::IO::MaxPathLength];
AZ::IO::FileIOBase::GetInstance()->ResolvePath(frameDataFilePath.c_str(), resolvedPath, AZ::IO::MaxPathLength);
m_lastCapturedFilePath = resolvedPath;
ProfilerRequestBus::Broadcast(&ProfilerRequestBus::Events::CaptureCpuProfilingStatistics, frameDataFilePath);
} }
m_captureToFile = false; m_captureToFile = false;
@ -206,24 +195,15 @@ namespace Profiler
bool isInProgress = CpuProfiler::Get()->IsContinuousCaptureInProgress(); bool isInProgress = CpuProfiler::Get()->IsContinuousCaptureInProgress();
if (ImGui::Button(isInProgress ? "End" : "Begin")) if (ImGui::Button(isInProgress ? "End" : "Begin"))
{ {
auto profilerSystem = AZ::Debug::ProfilerSystemInterface::Get();
if (isInProgress) if (isInProgress)
{ {
AZStd::string timeString; profilerSystem->EndCapture();
AZStd::to_string(timeString, AZStd::GetTimeNowSecond());
const AZStd::string frameDataFilePath = AZStd::string::format("%s/cpu_multi_%s.json", defaultSaveLocation, timeString.c_str());
char resolvedPath[AZ::IO::MaxPathLength];
AZ::IO::FileIOBase::GetInstance()->ResolvePath(frameDataFilePath.c_str(), resolvedPath, AZ::IO::MaxPathLength);
m_lastCapturedFilePath = resolvedPath;
ProfilerRequestBus::Broadcast(&ProfilerRequestBus::Events::EndContinuousCpuProfilingCapture, frameDataFilePath);
m_paused = true; m_paused = true;
} }
else else
{ {
ProfilerRequestBus::Broadcast(&ProfilerRequestBus::Events::BeginContinuousCpuProfilingCapture); profilerSystem->StartCapture(GenerateOutputFile("multi"));
} }
} }
@ -235,8 +215,10 @@ namespace Profiler
// Only update the cached file list when opened so that we aren't making IO calls on every frame. // Only update the cached file list when opened so that we aren't making IO calls on every frame.
m_cachedCapturePaths.clear(); m_cachedCapturePaths.clear();
AZ::IO::FixedMaxPathString captureOutput = AZ::Debug::GetProfilerCaptureLocation();
auto* base = AZ::IO::FileIOBase::GetInstance(); auto* base = AZ::IO::FileIOBase::GetInstance();
base->FindFiles(defaultSaveLocation, "*.json", base->FindFiles(captureOutput.c_str(), "*.json",
[&paths = m_cachedCapturePaths](const char* path) -> bool [&paths = m_cachedCapturePaths](const char* path) -> bool
{ {
auto foundPath = AZ::IO::Path(path); auto foundPath = AZ::IO::Path(path);
@ -418,6 +400,18 @@ namespace Profiler
ImGui::End(); ImGui::End();
} }
AZStd::string ImGuiCpuProfiler::GenerateOutputFile(const char* nameHint)
{
AZ::IO::FixedMaxPathString captureOutput = AZ::Debug::GetProfilerCaptureLocation();
const AZ::IO::FixedMaxPathString frameDataFilePath =
AZ::IO::FixedMaxPathString::format("%s/cpu_%s_%lld.json", captureOutput.c_str(), nameHint, AZStd::GetTimeNowSecond());
AZ::IO::FileIOBase::GetInstance()->ResolvePath(m_lastCapturedFilePath, frameDataFilePath.c_str());
return m_lastCapturedFilePath.String();
}
void ImGuiCpuProfiler::LoadFile() void ImGuiCpuProfiler::LoadFile()
{ {
const AZ::IO::Path& pathToLoad = m_cachedCapturePaths[m_currentFileIndex]; const AZ::IO::Path& pathToLoad = m_cachedCapturePaths[m_currentFileIndex];

@ -107,6 +107,9 @@ namespace Profiler
//! Draws the statistical view of the CPU profiling data. //! Draws the statistical view of the CPU profiling data.
void DrawStatisticsView(); void DrawStatisticsView();
//! Generates the full output timestamped file path based on nameHint
AZStd::string GenerateOutputFile(const char* nameHint);
//! Callback invoked when the "Load File" button is pressed in the file picker. //! Callback invoked when the "Load File" button is pressed in the file picker.
void LoadFile(); void LoadFile();
@ -214,7 +217,7 @@ namespace Profiler
AZStd::vector<CpuTimingEntry> m_cpuTimingStatisticsWhenPause; AZStd::vector<CpuTimingEntry> m_cpuTimingStatisticsWhenPause;
AZStd::sys_time_t m_frameToFrameTime{}; AZStd::sys_time_t m_frameToFrameTime{};
AZStd::string m_lastCapturedFilePath; AZ::IO::FixedMaxPath m_lastCapturedFilePath;
bool m_showFilePicker = false; bool m_showFilePicker = false;

@ -51,32 +51,6 @@ namespace Profiler
int m_framesLeft{ 0 }; 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) 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_TracePrintf("ProfilerSystemComponent", "Beginning serialization of %zu frames of profiling data\n", data.size());
@ -107,8 +81,8 @@ namespace Profiler
CpuProfiler::Get()->SetProfilerEnabled(false); CpuProfiler::Get()->SetProfilerEnabled(false);
} }
// Notify listeners that the pass' PipelineStatistics queries capture has finished. // Notify listeners that the profiler capture has finished.
ProfilerNotificationBus::Broadcast(&ProfilerNotificationBus::Events::OnCaptureCpuProfilingStatisticsFinished, AZ::Debug::ProfilerNotificationBus::Broadcast(&AZ::Debug::ProfilerNotificationBus::Events::OnCaptureFinished,
saveResult.IsSuccess(), saveResult.IsSuccess(),
captureInfo); captureInfo);
@ -128,21 +102,9 @@ namespace Profiler
->ClassElement(AZ::Edit::ClassElements::EditorData, "") ->ClassElement(AZ::Edit::ClassElements::EditorData, "")
->Attribute(AZ::Edit::Attributes::AppearsInAddComponentMenu, AZ_CRC_CE("System")) ->Attribute(AZ::Edit::Attributes::AppearsInAddComponentMenu, AZ_CRC_CE("System"))
->Attribute(AZ::Edit::Attributes::AutoExpand, true); ->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); CpuProfilingStatisticsSerializer::Reflect(context);
} }
@ -166,24 +128,22 @@ namespace Profiler
ProfilerSystemComponent::ProfilerSystemComponent() ProfilerSystemComponent::ProfilerSystemComponent()
{ {
if (ProfilerInterface::Get() == nullptr) if (AZ::Debug::ProfilerSystemInterface::Get() == nullptr)
{ {
ProfilerInterface::Register(this); AZ::Debug::ProfilerSystemInterface::Register(this);
} }
} }
ProfilerSystemComponent::~ProfilerSystemComponent() ProfilerSystemComponent::~ProfilerSystemComponent()
{ {
if (ProfilerInterface::Get() == this) if (AZ::Debug::ProfilerSystemInterface::Get() == this)
{ {
ProfilerInterface::Unregister(this); AZ::Debug::ProfilerSystemInterface::Unregister(this);
} }
} }
void ProfilerSystemComponent::Activate() void ProfilerSystemComponent::Activate()
{ {
ProfilerRequestBus::Handler::BusConnect();
m_cpuProfiler.Init(); m_cpuProfiler.Init();
} }
@ -191,8 +151,6 @@ namespace Profiler
{ {
m_cpuProfiler.Shutdown(); m_cpuProfiler.Shutdown();
ProfilerRequestBus::Handler::BusDisconnect();
// Block deactivation until the IO thread has finished serializing the CPU data // Block deactivation until the IO thread has finished serializing the CPU data
if (m_cpuDataSerializationThread.joinable()) if (m_cpuDataSerializationThread.joinable())
{ {
@ -200,12 +158,17 @@ namespace Profiler
} }
} }
void ProfilerSystemComponent::SetProfilerEnabled(bool enabled) bool ProfilerSystemComponent::IsActive() const
{
return m_cpuProfiler.IsProfilerEnabled();
}
void ProfilerSystemComponent::SetActive(bool enabled)
{ {
m_cpuProfiler.SetProfilerEnabled(enabled); m_cpuProfiler.SetProfilerEnabled(enabled);
} }
bool ProfilerSystemComponent::CaptureCpuProfilingStatistics(const AZStd::string& outputFilePath) bool ProfilerSystemComponent::CaptureFrame(const AZStd::string& outputFilePath)
{ {
bool expected = false; bool expected = false;
if (!m_cpuCaptureInProgress.compare_exchange_strong(expected, true)) if (!m_cpuCaptureInProgress.compare_exchange_strong(expected, true))
@ -236,12 +199,13 @@ namespace Profiler
return true; return true;
} }
bool ProfilerSystemComponent::BeginContinuousCpuProfilingCapture() bool ProfilerSystemComponent::StartCapture(AZStd::string outputFilePath)
{ {
m_captureFile = AZStd::move(outputFilePath);
return m_cpuProfiler.BeginContinuousCapture(); return m_cpuProfiler.BeginContinuousCapture();
} }
bool ProfilerSystemComponent::EndContinuousCpuProfilingCapture(const AZStd::string& outputFilePath) bool ProfilerSystemComponent::EndCapture()
{ {
bool expected = false; bool expected = false;
if (!m_cpuDataSerializationInProgress.compare_exchange_strong(expected, true)) if (!m_cpuDataSerializationInProgress.compare_exchange_strong(expected, true))
@ -263,7 +227,7 @@ namespace Profiler
// cpuProfilingData could be 1GB+ once saved, so use an IO thread to write it to disk. // cpuProfilingData could be 1GB+ once saved, so use an IO thread to write it to disk.
auto threadIoFunction = auto threadIoFunction =
[data = AZStd::move(captureResult), filePath = AZStd::string(outputFilePath), &flag = m_cpuDataSerializationInProgress]() [data = AZStd::move(captureResult), filePath = m_captureFile, &flag = m_cpuDataSerializationInProgress]()
{ {
SerializeCpuProfilingData(data, filePath, true); SerializeCpuProfilingData(data, filePath, true);
flag.store(false); flag.store(false);

@ -8,17 +8,17 @@
#pragma once #pragma once
#include <Profiler/ProfilerBus.h>
#include <CpuProfilerImpl.h> #include <CpuProfilerImpl.h>
#include <AzCore/Component/Component.h> #include <AzCore/Component/Component.h>
#include <AzCore/Debug/ProfilerBus.h>
#include <AzCore/std/parallel/thread.h> #include <AzCore/std/parallel/thread.h>
namespace Profiler namespace Profiler
{ {
class ProfilerSystemComponent class ProfilerSystemComponent
: public AZ::Component : public AZ::Component
, protected ProfilerRequestBus::Handler , protected AZ::Debug::ProfilerRequests
{ {
public: public:
AZ_COMPONENT(ProfilerSystemComponent, "{3f52c1d7-d920-4781-8ed7-88077ec4f305}"); AZ_COMPONENT(ProfilerSystemComponent, "{3f52c1d7-d920-4781-8ed7-88077ec4f305}");
@ -38,11 +38,12 @@ namespace Profiler
void Activate() override; void Activate() override;
void Deactivate() override; void Deactivate() override;
// ProfilerRequestBus interface implementation // ProfilerRequests interface implementation
void SetProfilerEnabled(bool enabled) override; bool IsActive() const override;
bool CaptureCpuProfilingStatistics(const AZStd::string& outputFilePath) override; void SetActive(bool active) override;
bool BeginContinuousCpuProfilingCapture() override; bool CaptureFrame(const AZStd::string& outputFilePath) override;
bool EndContinuousCpuProfilingCapture(const AZStd::string& outputFilePath) override; bool StartCapture(AZStd::string outputFilePath) override;
bool EndCapture() override;
AZStd::thread m_cpuDataSerializationThread; AZStd::thread m_cpuDataSerializationThread;
@ -51,6 +52,7 @@ namespace Profiler
AZStd::atomic_bool m_cpuCaptureInProgress{ false }; AZStd::atomic_bool m_cpuCaptureInProgress{ false };
CpuProfilerImpl m_cpuProfiler; CpuProfilerImpl m_cpuProfiler;
AZStd::string m_captureFile;
}; };
} // namespace Profiler } // namespace Profiler

@ -7,7 +7,6 @@
# #
set(FILES set(FILES
Include/Profiler/ProfilerBus.h
Include/Profiler/ProfilerImGuiBus.h Include/Profiler/ProfilerImGuiBus.h
Source/CpuProfiler.h Source/CpuProfiler.h
Source/CpuProfilerImpl.cpp Source/CpuProfilerImpl.cpp

@ -0,0 +1,15 @@
{
"O3DE":
{
"AzCore":
{
"Debug":
{
"Profiler":
{
"CaptureLocation" : "@user@/Profiler"
}
}
}
}
}
Loading…
Cancel
Save