Parcourir la source

Better improvements to the SerializeContextTools output (#10295)

* Better improvements to the SerializeContextTools output

When a command is successful only the output expected of the command is sent to stdout.
Otherwise the non-command output(AZ::Trace messages and random printfs) are sent to stderr

This is faciliated with the addition of a WriteBypassingCapture function on the FileDescriptorCapturer class to allow writing directly to location indicated by the original file descriptor

The CreateTypes and DumpTypes command now use an internal FunctorStream which implements the GenericStream interface to allow associating a function object with a streams Write function.
This is used to redirect those commands output to stdout even when it is being captured by a FileDescriptorCapturer

So the output for the dumptypes command looks as follows
```
D:\o3de\o3de>build\windows_vs2019\bin\profile\SerializeContextTools.exe dumptypes
Aabb {A54C2B36-D5B8-46A1-A529-4EBDBD2450E7}
AcesParameterOverrides {3EE8C0D4-3792-46C0-B91C-B89A81C36B91}
ActionManagerSystemComponent {47925132-7373-42EE-9131-F405EE4B0F1A}
AddressType {90752F2D-CBD3-4EE9-9CDD-447E797C8408}
...
XmlSchemaAsset {2DF35909-AF12-40A8-BED2-A033478D864D}
XmlSchemaAttribute {EE322552-0565-4D54-B022-9A9F134BF447}
XmlSchemaElement {DB03558D-9533-4426-B50F-3DB16F7AA686}
```

The output for the createtype command looks would only output the JSON
```
D:\o3de\o3de>build\windows_vs2019\bin\profile\SerializeContextTools.exe createtype --type-name AZ::RHI::PlatformLimitsDescriptor
{
    "TransientAttachmentPoolBudgets": {
        "BufferBudgetInBytes": 0,
        "ImageBudgetInBytes": 0,
        "RenderTargetBudgetInBytes": 0
    },
    "PlatformDefaultValues": {
        "StagingBufferBudgetInBytes": 134217728,
        "AsyncQueueStagingBufferSizeInBytes": 4194304,
        "MediumStagingBufferPageSizeInBytes": 2097152,
        "LargestStagingBufferPageSizeInBytes": 134217728,
        "ImagePoolPageSizeInBytes": 2097152,
        "BufferPoolPageSizeInBytes": 16777216
    },
    "PagingParameters": {
        "m_pageSizeInBytes": 67108864,
        "m_initialAllocationPercentage": 0.6000000238418579,
        "m_collectLatency": 1
    },
    "UsageHintParameters": {
        "m_minHeapSizeInBytes": 33554432,
        "m_collectLatency": 1,
        "m_heapSizeScaleFactor": 1.0,
        "m_maxHeapWastedPercentage": 0.3499999940395355
    },
    "HeapAllocationStrategy": 2
}
```

Signed-off-by: lumberyard-employee-dm <[email protected]>

* Moved output of the ScriptCanvas global performance report behind a CVar

Signed-off-by: lumberyard-employee-dm <[email protected]>
lumberyard-employee-dm il y a 3 ans
Parent
commit
86343d17f4

+ 5 - 0
Code/Framework/AzCore/AzCore/IO/SystemFile.cpp

@@ -416,4 +416,9 @@ namespace AZ::IO
         }
     }
 
+    int FileDescriptorCapturer::WriteBypassingCapture(const void* data, unsigned int size)
+    {
+        const int writeDescriptor = m_dupSourceDescriptor == -1 ? m_sourceDescriptor : m_dupSourceDescriptor;
+        return AZ::IO::PosixInternal::Write(writeDescriptor, data, size);
+    }
 } // namespace AZ::IO

+ 3 - 0
Code/Framework/AzCore/AzCore/IO/SystemFile.h

@@ -218,6 +218,9 @@ namespace AZ
             //! Stops capture of file descriptor and reset it back to it's previous value
             void Stop(const OutputRedirectVisitor& redirectCallback);
 
+            // Writes to the original source descriptor, bypassing the capture
+            int WriteBypassingCapture(const void* data, unsigned int size);
+
         private:
             void Reset();
             int m_sourceDescriptor = -1;

+ 95 - 88
Code/Tools/SerializeContextTools/Application.cpp

@@ -15,7 +15,7 @@
 #include <AzToolsFramework/Thumbnails/ThumbnailerNullComponent.h>
 #include <SliceConverterEditorEntityContextComponent.h>
 
-namespace AZ
+namespace AZ::SerializeContextTools
 {
     // SerializeContextTools is a full ToolsApplication that will load a project's Gem DLLs and initialize the system components.
     // This level of initialization is required to get all the serialization contexts and asset handlers registered, so that when
@@ -26,113 +26,120 @@ namespace AZ
     //   can still be started up, but the thumbnail service itself won't do anything.  The real ThumbnailerComponent uses Qt, which is why
     //   it isn't used.
 
-    namespace SerializeContextTools
+    Application::Application(int argc, char** argv, AZ::IO::FileDescriptorCapturer* stdoutCapturer)
+        : AzToolsFramework::ToolsApplication(&argc, &argv)
+        , m_stdoutCapturer(stdoutCapturer)
     {
-        Application::Application(int argc, char** argv)
-            : AzToolsFramework::ToolsApplication(&argc, &argv)
+        // We need a specialized variant of EditorEntityContextComponent for the SliceConverter, so we register the descriptor here.
+        RegisterComponentDescriptor(AzToolsFramework::SliceConverterEditorEntityContextComponent::CreateDescriptor());
+
+        AZ::IO::FixedMaxPath projectPath = AZ::Utils::GetProjectPath();
+        if (projectPath.empty())
         {
-            // We need a specialized variant of EditorEntityContextComponent for the SliceConverter, so we register the descriptor here.
-            RegisterComponentDescriptor(AzToolsFramework::SliceConverterEditorEntityContextComponent::CreateDescriptor());
+            AZ_TracePrintf("Serialize Context Tools", "Unable to determine the project path . "
+                "Make sure project has been set or provide via the -project-path option on the command line. (See -help for more info.)");
+            return;
+        }
 
-            AZ::IO::FixedMaxPath projectPath = AZ::Utils::GetProjectPath();
-            if (projectPath.empty())
+        size_t configSwitchCount = m_commandLine.GetNumSwitchValues("config");
+        if (configSwitchCount > 0)
+        {
+            AZ::IO::FixedMaxPath absConfigFilePath = projectPath / m_commandLine.GetSwitchValue("config", configSwitchCount - 1);
+            if (AZ::IO::SystemFile::Exists(absConfigFilePath.c_str()))
             {
-                AZ_TracePrintf("Serialize Context Tools", "Unable to determine the project path . "
-                    "Make sure project has been set or provide via the -project-path option on the command line. (See -help for more info.)");
-                return;
+                m_configFilePath = AZStd::move(absConfigFilePath);
             }
+        }
+
+        // Merge the build system generated setting registry file by using either "Editor" or
+        // and "${ProjectName}_GameLauncher" as a specialization
+        auto projectName = AZ::Utils::GetProjectName();
+        if (projectName.empty())
+        {
+            AZ_Error("Serialize Context Tools", false, "Unable to query the project name from settings registry");
+        }
+        else
+        {
+            AZ::SettingsRegistryInterface::Specializations projectSpecializations{ projectName };
 
-            size_t configSwitchCount = m_commandLine.GetNumSwitchValues("config");
-            if (configSwitchCount > 0)
+            // If a project specialization has been passed in via the command line, use it.
+            if (size_t specializationCount = m_commandLine.GetNumSwitchValues("specializations"); specializationCount > 0)
             {
-                AZ::IO::FixedMaxPath absConfigFilePath = projectPath / m_commandLine.GetSwitchValue("config", configSwitchCount - 1);
-                if (AZ::IO::SystemFile::Exists(absConfigFilePath.c_str()))
+                for (size_t specializationIndex = 0; specializationIndex < specializationCount; ++specializationIndex)
                 {
-                    m_configFilePath = AZStd::move(absConfigFilePath);
+                    projectSpecializations.Append(m_commandLine.GetSwitchValue("specializations", specializationIndex));
                 }
             }
-
-            // Merge the build system generated setting registry file by using either "Editor" or
-            // and "${ProjectName}_GameLauncher" as a specialization
-            auto projectName = AZ::Utils::GetProjectName();
-            if (projectName.empty())
-            {
-                AZ_Error("Serialize Context Tools", false, "Unable to query the project name from settings registry");
-            }
             else
             {
-                AZ::SettingsRegistryInterface::Specializations projectSpecializations{ projectName };
-
-                // If a project specialization has been passed in via the command line, use it.
-                if (size_t specializationCount = m_commandLine.GetNumSwitchValues("specializations"); specializationCount > 0)
+                // Otherwise, if a config file was passed in, auto-set the specialization based on the config file name.
+                AZ::IO::PathView configFilenameStem = m_configFilePath.Stem();
+                if (AZ::StringFunc::Equal(configFilenameStem.Native(), "Editor"))
                 {
-                    for (size_t specializationIndex = 0; specializationIndex < specializationCount; ++specializationIndex)
-                    {
-                        projectSpecializations.Append(m_commandLine.GetSwitchValue("specializations", specializationIndex));
-                    }
+                    projectSpecializations.Append("editor");
                 }
-                else
+                else if (AZ::StringFunc::Equal(configFilenameStem.Native(), "Game"))
                 {
-                    // Otherwise, if a config file was passed in, auto-set the specialization based on the config file name.
-                    AZ::IO::PathView configFilenameStem = m_configFilePath.Stem();
-                    if (AZ::StringFunc::Equal(configFilenameStem.Native(), "Editor"))
-                    {
-                        projectSpecializations.Append("editor");
-                    }
-                    else if (AZ::StringFunc::Equal(configFilenameStem.Native(), "Game"))
-                    {
-                        projectSpecializations.Append(projectName + "_GameLauncher");
-                    }
-
-                    // If the "dumptypes" or "createtype" supplied attempt to load the editor gem dependencies
-                    if (m_commandLine.GetNumMiscValues() > 0 &&
-                        (m_commandLine.GetMiscValue(0) == "dumptypes" || m_commandLine.GetMiscValue(0) == "createtype"))
-                    {
-                        projectSpecializations.Append("editor");
-                    }
+                    projectSpecializations.Append(projectName + "_GameLauncher");
                 }
 
-                // Used the project specializations to merge the gem modules dependencies *.setreg files
-                auto registry = AZ::SettingsRegistry::Get();
-                AZ::SettingsRegistryMergeUtils::MergeSettingsToRegistry_TargetBuildDependencyRegistry(*registry,
-                    AZ_TRAIT_OS_PLATFORM_CODENAME, projectSpecializations);
+                // If the "dumptypes" or "createtype" supplied attempt to load the editor gem dependencies
+                if (m_commandLine.GetNumMiscValues() > 0 &&
+                    (m_commandLine.GetMiscValue(0) == "dumptypes" || m_commandLine.GetMiscValue(0) == "createtype"))
+                {
+                    projectSpecializations.Append("editor");
+                }
             }
-        }
 
-        const char* Application::GetConfigFilePath() const
-        {
-            return m_configFilePath.c_str();
+            // Used the project specializations to merge the gem modules dependencies *.setreg files
+            auto registry = AZ::SettingsRegistry::Get();
+            AZ::SettingsRegistryMergeUtils::MergeSettingsToRegistry_TargetBuildDependencyRegistry(*registry,
+                AZ_TRAIT_OS_PLATFORM_CODENAME, projectSpecializations);
         }
+    }
 
-        void Application::QueryApplicationType(AZ::ApplicationTypeQuery& appType) const
-        {
-            appType.m_maskValue = AZ::ApplicationTypeQuery::Masks::Tool;
-        }
+    const char* Application::GetConfigFilePath() const
+    {
+        return m_configFilePath.c_str();
+    }
 
-        void Application::SetSettingsRegistrySpecializations(AZ::SettingsRegistryInterface::Specializations& specializations)
-        {
-            AZ::ComponentApplication::SetSettingsRegistrySpecializations(specializations);
-            specializations.Append("serializecontexttools");
-        }
+    void Application::QueryApplicationType(AZ::ApplicationTypeQuery& appType) const
+    {
+        appType.m_maskValue = AZ::ApplicationTypeQuery::Masks::Tool;
+    }
 
-        AZ::ComponentTypeList Application::GetRequiredSystemComponents() const
-        {
-            // By default, we use all of the standard system components.
-            AZ::ComponentTypeList components = AzToolsFramework::ToolsApplication::GetRequiredSystemComponents();
-
-            // Also add in the ThumbnailerNullComponent so that components requiring a ThumbnailService can still be started up.
-            components.emplace_back(azrtti_typeid<AzToolsFramework::Thumbnailer::ThumbnailerNullComponent>());
-
-            // The Slice Converter requires a specialized variant of the EditorEntityContextComponent that exposes the ability
-            // to disable the behavior of activating entities on creation.  During conversion, the creation flow will be triggered,
-            // but entity activation requires a significant amount of subsystem initialization that's unneeded for conversion.
-            // So, to get around this, we swap out EditorEntityContextComponent with SliceConverterEditorEntityContextComponent.
-            components.erase(
-                AZStd::remove(
-                    components.begin(), components.end(), azrtti_typeid<AzToolsFramework::EditorEntityContextComponent>()),
-                components.end());
-            components.emplace_back(azrtti_typeid<AzToolsFramework::SliceConverterEditorEntityContextComponent>());
-            return components;
-        }
-    } // namespace SerializeContextTools
-} // namespace AZ
+    void Application::SetSettingsRegistrySpecializations(AZ::SettingsRegistryInterface::Specializations& specializations)
+    {
+        AZ::ComponentApplication::SetSettingsRegistrySpecializations(specializations);
+        specializations.Append("serializecontexttools");
+    }
+
+    AZ::ComponentTypeList Application::GetRequiredSystemComponents() const
+    {
+        // By default, we use all of the standard system components.
+        AZ::ComponentTypeList components = AzToolsFramework::ToolsApplication::GetRequiredSystemComponents();
+
+        // Also add in the ThumbnailerNullComponent so that components requiring a ThumbnailService can still be started up.
+        components.emplace_back(azrtti_typeid<AzToolsFramework::Thumbnailer::ThumbnailerNullComponent>());
+
+        // The Slice Converter requires a specialized variant of the EditorEntityContextComponent that exposes the ability
+        // to disable the behavior of activating entities on creation.  During conversion, the creation flow will be triggered,
+        // but entity activation requires a significant amount of subsystem initialization that's unneeded for conversion.
+        // So, to get around this, we swap out EditorEntityContextComponent with SliceConverterEditorEntityContextComponent.
+        components.erase(
+            AZStd::remove(
+                components.begin(), components.end(), azrtti_typeid<AzToolsFramework::EditorEntityContextComponent>()),
+            components.end());
+        components.emplace_back(azrtti_typeid<AzToolsFramework::SliceConverterEditorEntityContextComponent>());
+        return components;
+    }
+
+    void Application::SetStdoutCapturer(AZ::IO::FileDescriptorCapturer* stdoutCapturer)
+    {
+        m_stdoutCapturer = stdoutCapturer;
+    }
+    AZ::IO::FileDescriptorCapturer* Application::GetStdoutCapturer() const
+    {
+        return m_stdoutCapturer;
+    }
+} // namespace AZ::SerializeContextTools

+ 29 - 20
Code/Tools/SerializeContextTools/Application.h

@@ -11,25 +11,34 @@
 #include <AzToolsFramework/Application/ToolsApplication.h>
 #include <AzCore/IO/Path/Path.h>
 
-namespace AZ
+namespace AZ::IO
 {
-    namespace SerializeContextTools
+    class FileDescriptorCapturer;
+}
+
+namespace AZ::SerializeContextTools
+{
+    class Application final
+        : public AzToolsFramework::ToolsApplication
     {
-        class Application final
-            : public AzToolsFramework::ToolsApplication
-        {
-        public:
-            Application(int argc, char** argv);
-            ~Application() override = default;
-            
-            const char* GetConfigFilePath() const;
-            AZ::ComponentTypeList GetRequiredSystemComponents() const override;
-            void QueryApplicationType(AZ::ApplicationTypeQuery& appType) const override;
-
-        protected:
-            void SetSettingsRegistrySpecializations(AZ::SettingsRegistryInterface::Specializations& specializations) override;
-
-            AZ::IO::FixedMaxPath m_configFilePath;
-        };
-    } // namespace SerializeContextTools
-} // namespace AZ
+    public:
+        Application(int argc, char** argv, AZ::IO::FileDescriptorCapturer* stdoutCapturer = {});
+        ~Application() override = default;
+
+        const char* GetConfigFilePath() const;
+        AZ::ComponentTypeList GetRequiredSystemComponents() const override;
+        void QueryApplicationType(AZ::ApplicationTypeQuery& appType) const override;
+
+        //! Associates a FileDescriptorCapturer with the SerializeContextApplication that
+        //! redirects stdout to a visitor callback.
+        //! The FileDescriptorCapturer supports a write bypass to force writing to stdout if need be
+        void SetStdoutCapturer(AZ::IO::FileDescriptorCapturer* stdoutCapturer);
+        AZ::IO::FileDescriptorCapturer* GetStdoutCapturer() const;
+
+    protected:
+        void SetSettingsRegistrySpecializations(AZ::SettingsRegistryInterface::Specializations& specializations) override;
+
+        AZ::IO::FixedMaxPath m_configFilePath;
+        AZ::IO::FileDescriptorCapturer* m_stdoutCapturer;
+    };
+} // namespace AZ::SerializeContextTools

+ 124 - 4
Code/Tools/SerializeContextTools/Dumper.cpp

@@ -31,6 +31,124 @@
 
 namespace AZ::SerializeContextTools
 {
+    inline namespace Streams
+    {
+        // Using an inline namespace for the Function Object Stream
+
+        /*
+        * Implementation of the GenericStream interface
+        * that uses a function object for writing
+        */
+        class FunctorStream
+            : public AZ::IO::GenericStream
+        {
+        public:
+            using WriteCallback = AZStd::function<AZ::IO::SizeType(AZStd::span<AZStd::byte const>)>;
+            FunctorStream() = default;
+            FunctorStream(WriteCallback writeCallback);
+
+            bool IsOpen() const override;
+            bool CanSeek() const override;
+            bool CanRead() const override;
+            bool CanWrite() const override;
+            void Seek(AZ::IO::OffsetType, SeekMode) override;
+            AZ::IO::SizeType Read(AZ::IO::SizeType, void*) override;
+            AZ::IO::SizeType Write(AZ::IO::SizeType bytes, const void* iBuffer) override;
+            AZ::IO::SizeType GetCurPos() const override;
+            AZ::IO::SizeType GetLength() const override;
+            AZ::IO::OpenMode GetModeFlags() const override;
+            const char* GetFilename() const override;
+        private:
+            WriteCallback m_streamWriter;
+        };
+
+        FunctorStream::FunctorStream(WriteCallback writeCallback)
+            : m_streamWriter { AZStd::move(writeCallback)}
+        {}
+
+        bool FunctorStream::IsOpen() const
+        {
+            return static_cast<bool>(m_streamWriter);
+        }
+        bool FunctorStream::CanSeek() const
+        {
+            return false;
+        }
+        bool FunctorStream::CanRead() const
+        {
+            return false;
+        }
+
+        bool FunctorStream::CanWrite() const
+        {
+            return true;
+        }
+
+        void FunctorStream::Seek(AZ::IO::OffsetType, SeekMode)
+        {
+            AZ_Assert(false, "Cannot seek in stdout stream");
+        }
+
+        AZ::IO::SizeType FunctorStream::Read(AZ::IO::SizeType, void*)
+        {
+            AZ_Assert(false, "The stdout file handle cannot be read from");
+            return 0;
+        }
+
+        AZ::IO::SizeType FunctorStream::Write(AZ::IO::SizeType bytes, const void* iBuffer)
+        {
+            if (m_streamWriter)
+            {
+                return m_streamWriter(AZStd::span(reinterpret_cast<const AZStd::byte*>(iBuffer), bytes));
+            }
+
+            return 0;
+        }
+
+        AZ::IO::SizeType FunctorStream::GetCurPos() const
+        {
+            return 0;
+        }
+
+        AZ::IO::SizeType FunctorStream::GetLength() const
+        {
+            return 0;
+        }
+
+        AZ::IO::OpenMode FunctorStream::GetModeFlags() const
+        {
+            return AZ::IO::OpenMode::ModeWrite;
+        }
+
+        const char* FunctorStream::GetFilename() const
+        {
+            return "<function object>";
+        }
+    }
+} // namespace AZ::SerializeContextTools::inline Stream
+
+
+namespace AZ::SerializeContextTools
+{
+    static auto GetWriteBypassStdoutCapturerFunctor(Application& application)
+    {
+        return [&application](AZStd::span<AZStd::byte const> outputBytes)
+        {
+            // If the application is currently capturing stdout, use stdout capturer to write
+            // directly to the file descriptor of stdout
+            if (AZ::IO::FileDescriptorCapturer* stdoutCapturer = application.GetStdoutCapturer();
+                stdoutCapturer != nullptr)
+            {
+                return stdoutCapturer->WriteBypassingCapture(outputBytes.data(), aznumeric_caster(outputBytes.size()));
+            }
+            else
+            {
+                constexpr int StdoutDescriptor = 1;
+                return AZ::IO::PosixInternal::Write(StdoutDescriptor, outputBytes.data(), aznumeric_caster(outputBytes.size()));
+            }
+        };
+    }
+
     bool Dumper::DumpFiles(Application& application)
     {
         SerializeContext* sc = application.GetSerializeContext();
@@ -151,9 +269,10 @@ namespace AZ::SerializeContextTools
 
     bool Dumper::DumpTypes(Application& application)
     {
-        // outputStream defaults to a stdout stream
+        // outputStream defaults to writing to stdout
         AZ::IO::SystemFile systemFile;
-        AZStd::variant<AZ::IO::StdoutStream, AZ::IO::SystemFileStream> outputStream;
+        AZStd::variant<FunctorStream, AZ::IO::SystemFileStream> outputStream(AZStd::in_place_type<FunctorStream>,
+            GetWriteBypassStdoutCapturerFunctor(application));
 
         AZ::CommandLine& commandLine = *application.GetAzCommandLine();
         // If the output-file parameter has been supplied open the file path using FileIOStream
@@ -320,9 +439,10 @@ namespace AZ::SerializeContextTools
 
     bool Dumper::CreateType(Application& application)
     {
-        // outputStream defaults to a stdout stream
+        // outputStream defaults to writing to stdout
         AZ::IO::SystemFile systemFile;
-        AZStd::variant<AZ::IO::StdoutStream, AZ::IO::SystemFileStream> outputStream;
+        AZStd::variant<FunctorStream, AZ::IO::SystemFileStream> outputStream(AZStd::in_place_type<FunctorStream>,
+            GetWriteBypassStdoutCapturerFunctor(application));
 
         AZ::CommandLine& commandLine = *application.GetAzCommandLine();
         // If the output-file parameter has been supplied open the file path using FileIOStream

+ 55 - 25
Code/Tools/SerializeContextTools/main.cpp

@@ -8,6 +8,7 @@
 
 #include <AzCore/Console/IConsole.h>
 #include <AzCore/Debug/Trace.h>
+#include <AzCore/IO/SystemFile.h>
 #include <AzCore/StringFunc/StringFunc.h>
 #include <AzCore/Settings/CommandLine.h>
 #include <Application.h>
@@ -95,70 +96,90 @@ void PrintHelp()
     AZ_Printf("Help", "  This options can be used with any of the above actions:\n");
     AZ_Printf("Help", "    [opt] --regset <setreg_key>=<setreg_value>: Set setreg_value at key setreg_key within the settings registry.\n");
     AZ_Printf("Help", "    [opt] --project-path <project_path>: Sets the path to the active project. Used to load gems associated with project\n");
-    AZ_Printf("Help", "\n");
 }
 
 int main(int argc, char** argv)
 {
     using namespace AZ::SerializeContextTools;
 
-    bool result = false;
-    Application application(argc, argv);
-    // Direct Raw Debug Trace Messages to stderr
-    if (auto console = AZ::Interface<AZ::IConsole>::Get(); console != nullptr)
+    constexpr int StdoutDescriptor = 1;
+    AZ::IO::FileDescriptorCapturer stdoutCapturer(StdoutDescriptor);
+
+    // Send stdout output to stderr if the executed command returned a failure
+    bool suppressStderr = false;
+    auto SendStdoutToError = [&suppressStderr](AZStd::span<AZStd::byte const> outputBytes)
     {
-        constexpr auto traceRedirectStream = AZ::Debug::RedirectCStream::Stderr;
-        console->PerformCommand("bg_redirectrawoutput",
-            { AZ::CVarFixedString::format("%d", static_cast<int>(traceRedirectStream)) });
-    }
+        if (!suppressStderr)
+        {
+            constexpr int StderrDescriptor = 2;
+            AZ::IO::PosixInternal::Write(StderrDescriptor, outputBytes.data(), aznumeric_cast<int>(outputBytes.size()));
+        }
+    };
+
+    stdoutCapturer.Start();
+    Application application(argc, argv, &stdoutCapturer);
     AZ::ComponentApplication::StartupParameters startupParameters;
     application.Start({}, startupParameters);
 
+    bool result = false;
     const AZ::CommandLine* commandLine = application.GetAzCommandLine();
-    if (commandLine->GetNumMiscValues() < 1)
-    {
-        PrintHelp();
-        result = true;
-    }
-    else
+    bool commandExecuted = false;
+    if (commandLine->GetNumMiscValues() >= 1)
     {
-        const AZStd::string& action = commandLine->GetMiscValue(0);
-        if (AZ::StringFunc::Equal("dumpfiles", action.c_str()))
+        // Set the command executed boolean to true
+        commandExecuted = true;
+        AZStd::string_view action = commandLine->GetMiscValue(0);
+        if (AZ::StringFunc::Equal("dumpfiles", action))
         {
             result = Dumper::DumpFiles(application);
         }
-        else if (AZ::StringFunc::Equal("dumpsc", action.c_str()))
+        else if (AZ::StringFunc::Equal("dumpsc", action))
         {
             result = Dumper::DumpSerializeContext(application);
         }
-        else if (AZ::StringFunc::Equal("dumptypes", action.c_str()))
+        else if (AZ::StringFunc::Equal("dumptypes", action))
         {
             result = Dumper::DumpTypes(application);
         }
-        else if (AZ::StringFunc::Equal("convert", action.c_str()))
+        else if (AZ::StringFunc::Equal("convert", action))
         {
             result = Converter::ConvertObjectStreamFiles(application);
         }
-        else if (AZ::StringFunc::Equal("convert-ini", action.c_str()))
+        else if (AZ::StringFunc::Equal("convert-ini", action))
         {
             result = Converter::ConvertConfigFile(application);
         }
-        else if (AZ::StringFunc::Equal("convert-slice", action.c_str()))
+        else if (AZ::StringFunc::Equal("convert-slice", action))
         {
             SliceConverter sliceConverter;
             result = sliceConverter.ConvertSliceFiles(application);
         }
-        else if (AZ::StringFunc::Equal("createtype", action.c_str()))
+        else if (AZ::StringFunc::Equal("createtype", action))
         {
             result = Dumper::CreateType(application);
         }
         else
         {
-            PrintHelp();
-            result = true;
+            commandExecuted = false;
         }
     }
 
+    // If a command was executed, display the help options
+    if (!commandExecuted)
+    {
+        // Stop capture of stdout to allow the help command to output to stdout
+        // stderr messages are suppressed in this case
+        fflush(stdout);
+        suppressStderr = true;
+        stdoutCapturer.Stop(SendStdoutToError);
+        PrintHelp();
+        result = true;
+        // Flush stdout stream before restarting the capture to make sure
+        // all the help text is output
+        fflush(stdout);
+        stdoutCapturer.Start();
+    }
+
     if (!result)
     {
         AZ_Printf("SerializeContextTools", "Processing didn't complete fully as problems were encountered.\n");
@@ -166,5 +187,14 @@ int main(int argc, char** argv)
 
     application.Destroy();
 
+    // Write out any stdout to stderr at this point
+
+    // Because the FILE* stream is buffered, make sure to flush
+    // it before stopping the capture of stdout.
+    fflush(stdout);
+
+    suppressStderr = result;
+    stdoutCapturer.Stop(SendStdoutToError);
+
     return result ? 0 : -1;
 }

+ 42 - 6
Gems/ScriptCanvas/Code/Source/SystemComponent.cpp

@@ -8,6 +8,7 @@
 
 #include <iostream>
 #include <AzCore/Component/EntityUtils.h>
+#include <AzCore/Console/IConsole.h>
 #include <AzCore/Serialization/EditContext.h>
 #include <AzCore/Serialization/Json/RegistrationContext.h>
 #include <AzCore/Serialization/SerializeContext.h>
@@ -65,6 +66,19 @@ namespace ScriptCanvasSystemComponentCpp
 
 namespace ScriptCanvas
 {
+    AZ_ENUM_CLASS(PerformanceReportFileStream,
+        None,
+        Stdout,
+        Stderr
+    );
+}
+
+namespace ScriptCanvas
+{
+    // Console Variable to determine where the scriptcanvas output performance report is sent
+    AZ_CVAR(PerformanceReportFileStream, sc_outputperformancereport, PerformanceReportFileStream::None, {}, AZ::ConsoleFunctorFlags::Null,
+        "Determines where the Script Canvas performance report should be output.");
+
     void SystemComponent::Reflect(AZ::ReflectContext* context)
     {
         REFLECT_SCRIPTCANVAS_AUTOGEN(ScriptCanvasStatic, context);
@@ -170,12 +184,34 @@ namespace ScriptCanvas
         const double latent = aznumeric_caster(report.timing.latentTime);
         const double total = aznumeric_caster(report.timing.totalTime);
 
-        std::cerr << "Global ScriptCanvas Performance Report:\n";
-        std::cerr << "[ INITIALIZE] " << AZStd::string::format("%7.3f ms \n", ready / 1000.0).c_str();
-        std::cerr << "[  EXECUTION] " << AZStd::string::format("%7.3f ms \n", instant / 1000.0).c_str();
-        std::cerr << "[     LATENT] " << AZStd::string::format("%7.3f ms \n", latent / 1000.0).c_str();
-        std::cerr << "[      TOTAL] " << AZStd::string::format("%7.3f ms \n", total / 1000.0).c_str();
-
+        FILE* performanceReportStream = nullptr;
+        if (auto console = AZ::Interface<AZ::IConsole>::Get(); console != nullptr)
+        {
+            if (PerformanceReportFileStream performanceOutputOption;
+                console->GetCvarValue("sc_outputperformancereport", performanceOutputOption) == AZ::GetValueResult::Success)
+            {
+                switch (performanceOutputOption)
+                {
+                case PerformanceReportFileStream::None:
+                    performanceReportStream = nullptr;
+                    break;
+                case PerformanceReportFileStream::Stdout:
+                    performanceReportStream = stdout;
+                    break;
+                case PerformanceReportFileStream::Stderr:
+                    performanceReportStream = stderr;
+                    break;
+                }
+            }
+        }
+        if (performanceReportStream != nullptr)
+        {
+            fprintf(performanceReportStream, "Global ScriptCanvas Performance Report:\n");
+            fprintf(performanceReportStream, "[ INITIALIZE] %s\n", AZStd::fixed_string<32>::format("%7.3f ms", ready / 1000.0).c_str());
+            fprintf(performanceReportStream, "[  EXECUTION] %s\n", AZStd::fixed_string<32>::format("%7.3f ms", instant / 1000.0).c_str());
+            fprintf(performanceReportStream, "[     LATENT] %s\n", AZStd::fixed_string<32>::format("%7.3f ms", latent / 1000.0).c_str());
+            fprintf(performanceReportStream, "[      TOTAL] %s\n", AZStd::fixed_string<32>::format("%7.3f ms", total / 1000.0).c_str());
+        }
         SafeUnregisterPerformanceTracker();
     }