Browse Source

Asset Processor: Builder tab UI change & metrics (#10863)

* Change Builder tab layout

Signed-off-by: Sven Hwang <[email protected]>

* Create BuilderInfoPatternsModel

TODO: name of Type, disable triangle on the left of tree view

Signed-off-by: Sven Hwang <[email protected]>

* Builder pattern: Show type name; hide expand icon

Signed-off-by: Sven Hwang <[email protected]>

* remove unused header include

Signed-off-by: Sven Hwang <[email protected]>

* Create BuilderInfoMetrics model

Signed-off-by: Sven Hwang <[email protected]>

* Include BuilderInfoMetricsModel and builder info

Signed-off-by: Sven Hwang <[email protected]>

* Record builder GUID in ProcessJob stat

Signed-off-by: Sven Hwang <[email protected]>

* Add files to .cmake

Signed-off-by: Sven Hwang <[email protected]>

* change number of tokens expected to 5

Signed-off-by: Sven Hwang <[email protected]>

* make builder info metrics model sortable

Signed-off-by: Sven Hwang <[email protected]>

* Reset data members when model reset is called

Signed-off-by: Sven Hwang <[email protected]>

* JobProcessDurationChanged emits duration as int

Signed-off-by: Sven Hwang <[email protected]>

* Add a hierarchy in model to show builder stat

Signed-off-by: Sven Hwang <[email protected]>

* Update stat in builder tab on job reprocessing

Signed-off-by: Sven Hwang <[email protected]>

* Move data from BuilderMetrics model to BuilderData

Signed-off-by: Sven Hwang <[email protected]>

* put BuilderData class to its own files

Signed-off-by: Sven Hwang <[email protected]>

* Change Builder tab layout

Signed-off-by: Sven Hwang <[email protected]>

* Create BuilderInfoPatternsModel

TODO: name of Type, disable triangle on the left of tree view

Signed-off-by: Sven Hwang <[email protected]>

* Builder pattern: Show type name; hide expand icon

Signed-off-by: Sven Hwang <[email protected]>

* remove unused header include

Signed-off-by: Sven Hwang <[email protected]>

* Create BuilderInfoMetrics model

Signed-off-by: Sven Hwang <[email protected]>

* Include BuilderInfoMetricsModel and builder info

Signed-off-by: Sven Hwang <[email protected]>

* Record builder GUID in ProcessJob stat

Signed-off-by: Sven Hwang <[email protected]>

* Add files to .cmake

Signed-off-by: Sven Hwang <[email protected]>

* change number of tokens expected to 5

Signed-off-by: Sven Hwang <[email protected]>

* make builder info metrics model sortable

Signed-off-by: Sven Hwang <[email protected]>

* Reset data members when model reset is called

Signed-off-by: Sven Hwang <[email protected]>

* JobProcessDurationChanged emits duration as int

Signed-off-by: Sven Hwang <[email protected]>

* Add a hierarchy in model to show builder stat

Signed-off-by: Sven Hwang <[email protected]>

* Update stat in builder tab on job reprocessing

Signed-off-by: Sven Hwang <[email protected]>

* Add method to refresh CreateJobs duration

this method is not currently connected with any signal. After
the CreateJob duration refresh bug fix is merged, the connection can
be made.

Signed-off-by: Sven Hwang <[email protected]>

* Change method name "OnProcessJobDurationChanged"

Signed-off-by: Sven Hwang <[email protected]>

* Fix merging issues

Signed-off-by: Sven Hwang <[email protected]>

* Make BuilderData a QObject; move stat change slot

to BuilderData

Signed-off-by: Sven Hwang <[email protected]>

* BuilderData gets updated and signal when CreateJob/ProcessJob stats changed

Signed-off-by: Sven Hwang <[email protected]>

* Correct header text

Signed-off-by: Sven Hwang <[email protected]>

* update test data to match new ProcessJob statName

Signed-off-by: Sven Hwang <[email protected]>

* Name change: BuilderDataItem

Signed-off-by: Sven Hwang <[email protected]>

* Merge SortModel into Model.h|cpp

Signed-off-by: Sven Hwang <[email protected]>

* disable sort icon in Patterns tab

Signed-off-by: Sven Hwang <[email protected]>

* Correct the signal emit macro

Signed-off-by: Sven Hwang <[email protected]>

* Make clearer parameter name (durationMs)

Signed-off-by: Sven Hwang <[email protected]>

* fix headers

Signed-off-by: Sven Hwang <[email protected]>

* reorder files in .cmake

Signed-off-by: Sven Hwang <[email protected]>

* Fixes addressing PR review. See details below.

- Remove commented-out code (BuilderData.cpp)
- Put each initializer in one line (BuilderDataItem.cpp)
- Remove "maybe_unused" where arg is used (BuilderInfoPatternsModel.cpp)
- Set default tab index to 0 (Job tab)

Signed-off-by: Sven Hwang <[email protected]>

* Create/Mode BuilderData comments in/to header file

Signed-off-by: Sven Hwang <[email protected]>

* Create and use scoped enum in BuilderData to indicate
selected builder is "All Builders" or "Invalid Builder"

Paraphrase TODO comments in BuilderInfoMetricsModel::OnBuilderSelectionChanged

Signed-off-by: Sven Hwang <[email protected]>

* Update builder not found warning message

Signed-off-by: Sven Hwang <[email protected]>

* Initialize raw pointers to nullptr; Use QPointer when not owning the object

Signed-off-by: Sven Hwang <[email protected]>

* Change enum name from CreateJob to CreateJobs

Signed-off-by: Sven Hwang <[email protected]>

* BuilderDataItem use weak_ptr to store parent

Since child does not *own* parent.
Also, change SetChild() to SetBuilderChild() and add explanation

Signed-off-by: Sven Hwang <[email protected]>

* Explicitly set pointer to nullptr when invalid

Signed-off-by: Sven Hwang <[email protected]>

* Correct the headers

Signed-off-by: Sven Hwang <[email protected]>

* remove TODO comment

Signed-off-by: Sven Hwang <[email protected]>

* Add Q_DECLARE_METATYPE(AssetProcessor::BuilderDataItem*)

Signed-off-by: Sven Hwang <[email protected]>

* add #include <QMetaType>

Signed-off-by: Sven Hwang <[email protected]>

* Tidy up: use ternary and intuitive if-else logic

Signed-off-by: Sven Hwang <[email protected]>

* Improvement: use static constexpr, AZStd::string::format, AZStd::array; properlly name variable

Signed-off-by: Sven Hwang <[email protected]>

* Use AZStd::move in constructor

Signed-off-by: Sven Hwang <[email protected]>

* Correct AZStd::string::format usage

Signed-off-by: Sven Hwang <[email protected]>
Sven Hwang 3 years ago
parent
commit
6b5073dabb

+ 8 - 0
Code/Tools/AssetProcessor/assetprocessor_gui_files.cmake

@@ -35,8 +35,16 @@ set(FILES
     native/ui/MainWindow.h
     native/ui/MainWindow.cpp
     native/ui/MainWindow.ui
+    native/ui/BuilderData.h
+    native/ui/BuilderData.cpp
+    native/ui/BuilderDataItem.h
+    native/ui/BuilderDataItem.cpp
     native/ui/BuilderListModel.h
     native/ui/BuilderListModel.cpp
+    native/ui/BuilderInfoPatternsModel.h
+    native/ui/BuilderInfoPatternsModel.cpp
+    native/ui/BuilderInfoMetricsModel.h
+    native/ui/BuilderInfoMetricsModel.cpp
     native/ui/MessageWindow.h
     native/ui/MessageWindow.cpp
     native/ui/MessageWindow.ui

+ 7 - 4
Code/Tools/AssetProcessor/native/AssetManager/assetProcessorManager.cpp

@@ -226,7 +226,11 @@ namespace AssetProcessor
         }
         else
         {
-            QString statKey = QString("ProcessJob,%1,%2,%3").arg(jobEntry.m_databaseSourceName).arg(jobEntry.m_jobKey).arg(jobEntry.m_platformInfo.m_identifier.c_str());
+            QString statKey = QString("ProcessJob,%1,%2,%3,%4")
+                                  .arg(jobEntry.m_databaseSourceName)
+                                  .arg(jobEntry.m_jobKey)
+                                  .arg(jobEntry.m_platformInfo.m_identifier.c_str())
+                                  .arg(jobEntry.m_builderGuid.ToString<AZStd::string>().c_str());
 
             if (status == JobStatus::InProgress)
             {
@@ -247,8 +251,7 @@ namespace AssetProcessor
 
                 if (operationDuration)
                 {
-                    Q_EMIT JobProcessDurationChanged(
-                        jobEntry, QTime::fromMSecsSinceStartOfDay(aznumeric_cast<int>(operationDuration.value())));
+                    Q_EMIT JobProcessDurationChanged(jobEntry, aznumeric_cast<int>(operationDuration.value()));
                 }
 
                 m_jobRunKeyToJobInfoMap.erase(jobEntry.m_jobRunKey);
@@ -3941,7 +3944,7 @@ namespace AssetProcessor
         UpdateSourceFileDependenciesDatabase(entry);
         m_jobEntries.push_back(entry);
 
-        // Signals SourceAssetTreeModel so it can update the CreateJob duration change
+        // Signals SourceAssetTreeModel so it can update the CreateJobs duration change
         Q_EMIT CreateJobsDurationChanged(newSourceInfo.m_sourceRelativeToWatchFolder);
     }
 

+ 1 - 1
Code/Tools/AssetProcessor/native/AssetManager/assetProcessorManager.h

@@ -259,7 +259,7 @@ namespace AssetProcessor
         void JobRemoved(AzToolsFramework::AssetSystem::JobInfo jobInfo);
 
         void JobComplete(JobEntry jobEntry, AzToolsFramework::AssetSystem::JobStatus status);
-        void JobProcessDurationChanged(JobEntry jobEntry, QTime duration);
+        void JobProcessDurationChanged(JobEntry jobEntry, int durationMs);
         void CreateJobsDurationChanged(QString sourceName);
 
         //! Send a message when a new path dependency is resolved, so that downstream tools know the AssetId of the resolved dependency.

+ 3 - 3
Code/Tools/AssetProcessor/native/resourcecompiler/JobsModel.cpp

@@ -360,7 +360,7 @@ namespace AssetProcessor
             AZStd::unordered_map<QueueElementID, AZ::s64> historicalStats;
             auto statsFunction = [&historicalStats](AzToolsFramework::AssetDatabase::StatDatabaseEntry entry)
             {
-                static constexpr int numTokensExpected = 4;
+                static constexpr int numTokensExpected = 5;
                 AZStd::vector<AZStd::string> tokens;
                 AZ::StringFunc::Tokenize(entry.m_statName, tokens, ',');
 
@@ -511,7 +511,7 @@ namespace AssetProcessor
         }
     }
 
-    void JobsModel::OnJobProcessDurationChanged(JobEntry jobEntry, QTime duration)
+    void JobsModel::OnJobProcessDurationChanged(JobEntry jobEntry, int durationMs)
     {
         QueueElementID elementId(jobEntry.m_databaseSourceName, jobEntry.m_platformInfo.m_identifier.c_str(), jobEntry.m_jobKey);
 
@@ -519,7 +519,7 @@ namespace AssetProcessor
         {
             unsigned int jobIndex = iter.value();
             CachedJobInfo* jobInfo = m_cachedJobs[jobIndex];
-            jobInfo->m_processDuration = duration;
+            jobInfo->m_processDuration = QTime::fromMSecsSinceStartOfDay(durationMs);
             Q_EMIT dataChanged(
                 index(jobIndex, ColumnProcessDuration, QModelIndex()), index(jobIndex, ColumnProcessDuration, QModelIndex()));
         }

+ 1 - 1
Code/Tools/AssetProcessor/native/resourcecompiler/JobsModel.h

@@ -92,7 +92,7 @@ namespace AssetProcessor
 
 public Q_SLOTS:
         void OnJobStatusChanged(JobEntry entry, AzToolsFramework::AssetSystem::JobStatus status);
-        void OnJobProcessDurationChanged(JobEntry jobEntry, QTime duration);
+        void OnJobProcessDurationChanged(JobEntry jobEntry, int durationMs);
         void OnJobRemoved(AzToolsFramework::AssetSystem::JobInfo jobInfo);
         void OnSourceRemoved(QString sourceDatabasePath);
 

+ 9 - 3
Code/Tools/AssetProcessor/native/tests/utilities/JobModelTest.cpp

@@ -292,13 +292,19 @@ void JobModelUnitTests::CreateDatabaseTestData()
     //! Insert valid stat entries, one per job
     for (const auto& jobEntry : m_data->m_jobEntries)
     {
-        statEntry = { "ProcessJob," + m_data->m_sourceName + "," + jobEntry.m_jobKey + "," + jobEntry.m_platform /* StatName */,
+        AZStd::string statName = AZStd::string::format(
+            "ProcessJob,%s,%s,%s,%s",
+            m_data->m_sourceName.c_str(),
+            jobEntry.m_jobKey.c_str(),
+            jobEntry.m_platform.c_str(),
+            jobEntry.m_builderGuid.ToString<AZStd::string>().c_str());
+        statEntry = { statName /* StatName */,
                       aznumeric_cast<AZ::s64>(jobEntry.m_fingerprint) /* StatValue */,
                       aznumeric_cast<AZ::s64>(jobEntry.m_jobRunKey) /* LastLogTime */ };
         ASSERT_TRUE(m_data->m_connection.ReplaceStat(statEntry));
     }
 
-    //! Insert an invalid stat entry (5 tokens)
-    statEntry = { "ProcessJob,apple,banana,carrot,dog", 123, 456 };
+    //! Insert an invalid stat entry (6 tokens)
+    statEntry = { "ProcessJob,apple,banana,carrot,dog,{FDAF4363-C530-476C-B382-579A43B3E2FC}", 123, 456 };
     ASSERT_TRUE(m_data->m_connection.ReplaceStat(statEntry));
 }

+ 158 - 0
Code/Tools/AssetProcessor/native/ui/BuilderData.cpp

@@ -0,0 +1,158 @@
+/*
+ * 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 <native/ui/BuilderData.h>
+#include <utilities/AssetUtilEBusHelper.h>
+#include <native/ui/BuilderDataItem.h>
+#include <AzToolsFramework/AssetDatabase/AssetDatabaseConnection.h>
+#include <AzCore/StringFunc/StringFunc.h>
+#include <AzCore/std/smart_ptr/weak_ptr.h>
+
+namespace AssetProcessor
+{
+    static constexpr char builderNotFoundWarningMessage[] =
+        "Found a %s metric entry with builder %s \"%s\", but Asset Processor does not recognize this "
+        "builder. Ensure this builder is in the asset folders and its name is shown "
+        "in the Builders tab. If this builder was removed intentionally in the past, you can safely ignore this "
+        "warning.\n";
+
+    BuilderData::BuilderData(AZStd::shared_ptr<AzToolsFramework::AssetDatabase::AssetDatabaseConnection> dbConnection, QObject* parent)
+        : m_dbConnection(dbConnection)
+        , QObject(parent)
+    {
+    }
+
+    void BuilderData::Reset()
+    {
+        BuilderInfoList builders;
+        AssetBuilderInfoBus::Broadcast(&AssetBuilderInfoBus::Events::GetAllBuildersInfo, builders);
+
+        m_root.reset(new BuilderDataItem(BuilderDataItem::ItemType::InvisibleRoot, "", 0, 0, AZStd::weak_ptr<BuilderDataItem>()));
+
+        m_allBuildersMetrics.reset(new BuilderDataItem(BuilderDataItem::ItemType::Builder, "All builders", 0, 0, m_root));
+        m_allBuildersMetrics->InitializeBuilder(m_allBuildersMetrics);
+        m_singleBuilderMetrics.clear();
+        m_builderGuidToIndex.clear();
+        m_builderNameToIndex.clear();
+        m_currentSelectedBuilderIndex = aznumeric_cast<int>(BuilderSelection::AllBuilders);
+
+        for (int i = 0; i < builders.size(); ++i)
+        {
+            auto builder = m_singleBuilderMetrics.emplace_back(
+                new BuilderDataItem(BuilderDataItem::ItemType::Builder, builders[i].m_name, 0, 0, m_root));
+            builder->InitializeBuilder(builder);
+            m_builderGuidToIndex[builders[i].m_busId] = i;
+            m_builderNameToIndex[builders[i].m_name] = i;
+        }
+
+        // CreateJobs stat
+        m_dbConnection->QueryStatLikeStatName(
+            "CreateJobs,%",
+            [this](AzToolsFramework::AssetDatabase::StatDatabaseEntry entry)
+            {
+                AZStd::vector<AZStd::string> tokens;
+                AZ::StringFunc::Tokenize(entry.m_statName, tokens, ',');
+                if (tokens.size() == 3) // CreateJobs,filePath,builderName
+                {
+                    const auto& sourceName = tokens[1];
+                    const auto& builderName = tokens[2];
+                    if (m_builderNameToIndex.contains(builderName))
+                    {
+                        m_singleBuilderMetrics[m_builderNameToIndex[builderName]]->UpdateOrInsertEntry(
+                            BuilderDataItem::JobType::CreateJobs, sourceName, 1, entry.m_statValue);
+                        m_allBuildersMetrics->UpdateOrInsertEntry(
+                            BuilderDataItem::JobType::CreateJobs, builderName + "," + sourceName, 1, entry.m_statValue);
+                    }
+                    else
+                    {
+                        AZ_Warning("AssetProcessor", false, builderNotFoundWarningMessage, "CreateJobs", "name", builderName.c_str());
+                    }
+                }
+                return true;
+            });
+
+        // ProcessJob stat
+        m_dbConnection->QueryStatLikeStatName(
+            "ProcessJob,%",
+            [this](AzToolsFramework::AssetDatabase::StatDatabaseEntry stat)
+            {
+                AZStd::vector<AZStd::string> tokens;
+                AZ::StringFunc::Tokenize(stat.m_statName, tokens, ',');
+                if (tokens.size() == 5) // ProcessJob,sourceName,jobKey,platform,builderGuid
+                {
+                    const auto& builderGuid = AZ::Uuid::CreateString(tokens[4].c_str());
+
+                    if (m_builderGuidToIndex.contains(builderGuid))
+                    {
+                        AZStd::string entryName;
+                        AZ::StringFunc::Join(entryName, tokens.begin() + 1, tokens.begin() + 4, ',');
+                        m_singleBuilderMetrics[m_builderGuidToIndex[builderGuid]]->UpdateOrInsertEntry(
+                            BuilderDataItem::JobType::ProcessJob, entryName, 1, stat.m_statValue);
+                        m_allBuildersMetrics->UpdateOrInsertEntry(
+                            BuilderDataItem::JobType::ProcessJob, entryName, 1, stat.m_statValue);
+                    }
+                    else
+                    {
+                        AZ_Warning("AssetProcessor", false, builderNotFoundWarningMessage, "ProcessJob", "bus ID", tokens[4].c_str());
+                    }
+                }
+                return true;
+            });
+    }
+
+    void BuilderData::OnCreateJobsDurationChanged(QString sourceName)
+    {
+        QString statKey = QString("CreateJobs,").append(sourceName).append("%");
+        m_dbConnection->QueryStatLikeStatName(
+            statKey.toUtf8().constData(),
+            [this](AzToolsFramework::AssetDatabase::StatDatabaseEntry entry)
+            {
+                AZStd::vector<AZStd::string> tokens;
+                AZ::StringFunc::Tokenize(entry.m_statName, tokens, ',');
+                if (tokens.size() == 3) // CreateJobs,filePath,builderName
+                {
+                    const auto& sourceName = tokens[1];
+                    const auto& builderName = tokens[2];
+                    if (m_builderNameToIndex.contains(builderName))
+                    {
+                        AZStd::shared_ptr<BuilderDataItem> item = nullptr;
+                        item = m_singleBuilderMetrics[m_builderNameToIndex[builderName]]->UpdateOrInsertEntry(
+                            BuilderDataItem::JobType::CreateJobs, sourceName, 1, entry.m_statValue);
+                        Q_EMIT DurationChanged(item.get());
+                        item = m_allBuildersMetrics->UpdateOrInsertEntry(
+                            BuilderDataItem::JobType::CreateJobs, builderName + "," + sourceName, 1, entry.m_statValue);
+                        Q_EMIT DurationChanged(item.get());
+                    }
+                    else
+                    {
+                        AZ_Warning("AssetProcessor", false, builderNotFoundWarningMessage, "CreateJobs", "name", builderName.c_str());
+                    }
+                }
+                return true;
+            });
+    }
+
+    void BuilderData::OnProcessJobDurationChanged(JobEntry jobEntry, int value)
+    {
+        if (m_builderGuidToIndex.contains(jobEntry.m_builderGuid))
+        {
+            int builderIndex = m_builderGuidToIndex[jobEntry.m_builderGuid];
+
+            AZStd::string entryName = AZStd::string::format(
+                "%s,%s,%s",
+                jobEntry.m_databaseSourceName.toUtf8().constData(),
+                jobEntry.m_jobKey.toUtf8().constData(),
+                jobEntry.m_platformInfo.m_identifier.c_str());
+
+            AZStd::shared_ptr<BuilderDataItem> item = nullptr;
+            item = m_singleBuilderMetrics[builderIndex]->UpdateOrInsertEntry(BuilderDataItem::JobType::ProcessJob, entryName, 1, value);
+            Q_EMIT DurationChanged(item.get());
+            item = m_allBuildersMetrics->UpdateOrInsertEntry(BuilderDataItem::JobType::ProcessJob, entryName, 1, value);
+            Q_EMIT DurationChanged(item.get());
+        }
+    }
+}

+ 66 - 0
Code/Tools/AssetProcessor/native/ui/BuilderData.h

@@ -0,0 +1,66 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#pragma once
+
+#if !defined(Q_MOC_RUN)
+#include <native/assetprocessor.h>
+#include <AzCore/std/containers/vector.h>
+#include <AzCore/std/containers/unordered_map.h>
+#include <AzCore/std/string/string.h>
+#include <AzCore/std/smart_ptr/shared_ptr.h>
+#include <QObject>
+#endif
+
+namespace AzToolsFramework
+{
+    namespace AssetDatabase
+    {
+        class AssetDatabaseConnection;
+    }
+} // namespace AzToolsFramework
+
+namespace AssetProcessor
+{
+    class BuilderDataItem;
+
+    //! BuilderData is a class that contains all jobs' metrics, categorized by builders. It is shared by BuilderInfoMetricsModel and
+    //! BuilderListModel as the source of data.
+    class BuilderData : public QObject
+    {
+        Q_OBJECT
+    public:
+        BuilderData(AZStd::shared_ptr<AzToolsFramework::AssetDatabase::AssetDatabaseConnection> dbConnection, QObject* parent = nullptr);
+        //! This method runs when this model is initialized. It gets the list of builders, gets existing stats about analysis jobs and
+        //! processing jobs, and matches stats with builders and save them appropriately for future use.
+        void Reset();
+
+        enum class BuilderSelection: int
+        {
+            Invalid = -2,
+            AllBuilders = -1,
+            FirstBuilderIndex = 0
+        };
+
+        AZStd::shared_ptr<AzToolsFramework::AssetDatabase::AssetDatabaseConnection> m_dbConnection;
+        AZStd::shared_ptr<BuilderDataItem> m_root;
+        AZStd::shared_ptr<BuilderDataItem> m_allBuildersMetrics;
+        AZStd::vector<AZStd::shared_ptr<BuilderDataItem>> m_singleBuilderMetrics;
+        AZStd::unordered_map<AZStd::string, int> m_builderNameToIndex;
+        AZStd::unordered_map<AZ::Uuid, int> m_builderGuidToIndex;
+        //! This value, when being non-negative, refers to index of m_singleBuilderMetrics.
+        //! When it is BuilderSelection::AllBuilders, currently selects m_allBuildersMetrics.
+        //! When it is BuilderSelection::Invalid, it means BuilderInfoMetricsModel cannot find the selected builder in m_builderGuidToIndex.
+        int m_currentSelectedBuilderIndex = aznumeric_cast<int>(BuilderSelection::AllBuilders);
+    Q_SIGNALS:
+        void DurationChanged(BuilderDataItem* itemChanged);
+    public Q_SLOTS:
+        void OnProcessJobDurationChanged(JobEntry jobEntry, int value);
+        void OnCreateJobsDurationChanged(QString sourceName);
+    };
+}

+ 157 - 0
Code/Tools/AssetProcessor/native/ui/BuilderDataItem.cpp

@@ -0,0 +1,157 @@
+/*
+ * 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 <native/ui/BuilderDataItem.h>
+
+namespace AssetProcessor
+{
+    static constexpr AZStd::array jobTypeDisplayNames{ "Analysis Jobs", "Processing Jobs" };
+    static constexpr char invalidJobTypeDisplayName[] = "Invalid Job Type";
+
+    BuilderDataItem::BuilderDataItem(
+        ItemType itemType, AZStd::string name,
+        AZ::s64 jobCount,
+        AZ::s64 totalDuration,
+        AZStd::weak_ptr<BuilderDataItem> parent)
+        : m_itemType(itemType)
+        , m_name(AZStd::move(name))
+        , m_jobCount(jobCount)
+        , m_totalDuration(totalDuration)
+        , m_parent(parent)
+    {
+    }
+
+    int BuilderDataItem::ChildCount() const
+    {
+        return aznumeric_cast<int>(m_children.size());
+    }
+
+    const char* BuilderDataItem::GetName() const
+    {
+        return m_name.c_str();
+    }
+
+    AZ::s64 BuilderDataItem::GetJobCount() const
+    {
+        return m_jobCount;
+    }
+
+    AZ::s64 BuilderDataItem::GetTotalDuration() const
+    {
+        return m_totalDuration;
+    }
+
+    AZStd::shared_ptr<BuilderDataItem> BuilderDataItem::GetChild(int row) const
+    {
+        if (row >= m_children.size())
+        {
+            return nullptr;
+        }
+
+        return m_children[row];
+    }
+
+    AZStd::weak_ptr<BuilderDataItem> BuilderDataItem::GetParent() const
+    {
+        return m_parent;
+    }
+
+    AZStd::shared_ptr<BuilderDataItem> BuilderDataItem::UpdateOrInsertEntry(
+        JobType entryjobType, const AZStd::string& entryName, AZ::s64 entryJobCount, AZ::s64 entryTotalDuration)
+    {
+        //! only allowed to insert from builder, with a valid JobType
+        if (m_itemType != ItemType::Builder || entryjobType >= JobType::Max)
+        {
+            return nullptr;
+        }
+
+        // jobType is either CreateJobs or ProcessJob
+        const auto& jobType = m_children[aznumeric_cast<int>(entryjobType)];
+
+        AZStd::shared_ptr<BuilderDataItem> entry = nullptr;
+        if (jobType->m_childNameToIndex.contains(entryName))
+        {
+            entry = jobType->m_children[jobType->m_childNameToIndex[entryName]];
+            AZ::s64 jobCountDiff = entryJobCount - entry->m_jobCount;
+            AZ::s64 totalDurationDiff = entryTotalDuration - entry->m_totalDuration;
+            entry->m_jobCount = entryJobCount;
+            entry->m_totalDuration= entryTotalDuration;
+            jobType->UpdateMetrics(jobCountDiff, totalDurationDiff);
+        }
+        else
+        {
+            entry = jobType->m_children.emplace_back(
+                new BuilderDataItem(ItemType::Entry, entryName, entryJobCount, entryTotalDuration, jobType));
+            jobType->m_childNameToIndex[entryName] = aznumeric_cast<int>(jobType->m_children.size() - 1);
+            jobType->UpdateMetrics(entryJobCount, entryTotalDuration);
+        }
+
+        return entry;
+    }
+    bool BuilderDataItem::InitializeBuilder(AZStd::weak_ptr<BuilderDataItem> builderWeakPointer)
+    {
+        if (m_itemType != ItemType::Builder)
+        {
+            return false;
+        }
+
+        for (int jobTypeIndex = 0; jobTypeIndex < aznumeric_cast<int>(JobType::Max); ++jobTypeIndex)
+        {
+            const AZStd::string& jobTypeDisplayName =
+                jobTypeIndex < jobTypeDisplayNames.size() ? jobTypeDisplayNames[jobTypeIndex] : invalidJobTypeDisplayName;
+            if (jobTypeIndex >= jobTypeDisplayNames.size())
+            {
+                AZ_Warning(
+                    "Asset Processor",
+                    false,
+                    "Invalid job type name. Job type indexed %d in scoped enum JobType does not have a matching display name in "
+                    "jobTypeDisplayNames. Update jobTypeDisplayNames vector in BuilderDataItem.cpp.",
+                    jobTypeIndex);
+            }
+
+            m_children.emplace_back(new BuilderDataItem(ItemType::JobType, jobTypeDisplayName, 0, 0, builderWeakPointer));
+        }
+
+        return true;
+    }
+    void BuilderDataItem::UpdateMetrics(AZ::s64 jobCountDiff, AZ::s64 totalDurationDiff)
+    {
+        m_jobCount += jobCountDiff;
+        m_totalDuration += totalDurationDiff;
+        if (auto sharedParent = m_parent.lock())
+        {
+            sharedParent->UpdateMetrics(jobCountDiff, totalDurationDiff);
+        }
+    }
+    int BuilderDataItem::GetRow() const
+    {
+        if (auto sharedParent = m_parent.lock())
+        {
+            int index = 0;
+            for (const auto& item : sharedParent->m_children)
+            {
+                if (item.get() == this)
+                {
+                    return index;
+                }
+                ++index;
+            }
+        }
+        return 0;
+    }
+    bool BuilderDataItem::SetBuilderChild(AZStd::shared_ptr<BuilderDataItem> builder)
+    {
+        if (m_itemType != ItemType::InvisibleRoot)
+        {
+            return false;
+        }
+
+        m_children.resize(1);
+        m_children[0] = builder;
+        return true;
+    }
+}

+ 67 - 0
Code/Tools/AssetProcessor/native/ui/BuilderDataItem.h

@@ -0,0 +1,67 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#pragma once
+#include <AzCore/std/containers/vector.h>
+#include <AzCore/std/containers/unordered_map.h>
+#include <AzCore/std/smart_ptr/shared_ptr.h>
+#include <AzCore/std/smart_ptr/weak_ptr.h>
+#include <AzCore/std/string/string.h>
+#include <QMetaType>
+
+namespace AssetProcessor
+{
+    class BuilderDataItem
+    {
+    public:
+        enum class JobType
+        {
+            CreateJobs,
+            ProcessJob,
+            Max
+        };
+
+        enum class ItemType
+        {
+            InvisibleRoot,
+            Builder,
+            JobType,
+            Entry,
+            Max
+        };
+
+        BuilderDataItem(
+            ItemType itemType, AZStd::string name, AZ::s64 jobCount, AZ::s64 totalDuration, AZStd::weak_ptr<BuilderDataItem> parent);
+        int ChildCount() const;
+        const char* GetName() const;
+        AZ::s64 GetJobCount() const;
+        AZ::s64 GetTotalDuration() const;
+        AZStd::shared_ptr<BuilderDataItem> GetChild(int row) const;
+        AZStd::weak_ptr<BuilderDataItem> GetParent() const;
+        //! Returns this item's row number in its parent's children list.
+        int GetRow() const; 
+        //! This method is only called on InvisibleRoot: set the passed-in builder as the only child.
+        bool SetBuilderChild(AZStd::shared_ptr<BuilderDataItem> builder);
+        //! This method is only called on Builder: create JobType children.
+        bool InitializeBuilder(AZStd::weak_ptr<BuilderDataItem> builderWeakPointer);
+        AZStd::shared_ptr<BuilderDataItem> UpdateOrInsertEntry(
+            JobType jobType, const AZStd::string& name, AZ::s64 jobCount, AZ::s64 totalDuration);
+    private:
+        void UpdateMetrics(AZ::s64 jobCountDiff, AZ::s64 totalDurationDiff);
+
+        AZStd::vector<AZStd::shared_ptr<BuilderDataItem>> m_children;
+        AZStd::weak_ptr<BuilderDataItem> m_parent;
+        AZStd::unordered_map<AZStd::string, int> m_childNameToIndex;
+        AZStd::string m_name;
+        AZ::s64 m_jobCount = 0;
+        AZ::s64 m_totalDuration = 0;
+        ItemType m_itemType = ItemType::Max;
+    };
+}
+
+Q_DECLARE_METATYPE(AssetProcessor::BuilderDataItem*)

+ 243 - 0
Code/Tools/AssetProcessor/native/ui/BuilderInfoMetricsModel.cpp

@@ -0,0 +1,243 @@
+/*
+ * 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 <ui/BuilderInfoMetricsModel.h>
+#include <ui/BuilderDataItem.h>
+#include <ui/BuilderData.h>
+#include <AssetBuilderSDK/AssetBuilderSDK.h>
+#include <QTime>
+
+namespace AssetProcessor
+{
+    QString DurationToQString(AZ::s64 durationInMs)
+    {
+        const AZ::s64 dayInMs = 86400000;
+        if (durationInMs < 0)
+        {
+            return QString();
+        }
+
+        AZ::s64 dayCount = durationInMs / dayInMs;
+        QTime duration = QTime::fromMSecsSinceStartOfDay(durationInMs % dayInMs);
+        if (dayCount > 0)
+        {
+            return duration.toString("zzz' ms, 'ss' sec, 'mm' min, 'hh' hr, %1 day'").arg(dayCount);
+        }
+        
+        if (duration.isValid())
+        {
+            if (duration.hour() > 0)
+            {
+                return duration.toString("zzz' ms, 'ss' sec, 'mm' min, 'hh' hr'");
+            }
+            if (duration.minute() > 0)
+            {
+                return duration.toString("zzz' ms, 'ss' sec, 'mm' min'");
+            }
+            if (duration.second() > 0)
+            {
+                return duration.toString("zzz' ms, 'ss' sec'");
+            }
+            return duration.toString("zzz' ms'");
+        }
+
+        return QString();
+    }
+
+    BuilderInfoMetricsModel::BuilderInfoMetricsModel(BuilderData* builderData, QObject* parent)
+        : QAbstractItemModel(parent)
+        , m_data(builderData)
+    {
+    }
+
+    void BuilderInfoMetricsModel::Reset()
+    {
+        beginResetModel();
+        m_data->Reset();
+        endResetModel();
+    }
+
+    void BuilderInfoMetricsModel::OnBuilderSelectionChanged(const AssetBuilderSDK::AssetBuilderDesc& builder)
+    {
+        beginResetModel();
+        
+        if (m_data->m_builderGuidToIndex.contains(builder.m_busId))
+        {
+            m_data->m_currentSelectedBuilderIndex = m_data->m_builderGuidToIndex[builder.m_busId];
+            m_data->m_root->SetBuilderChild(m_data->m_singleBuilderMetrics[m_data->m_currentSelectedBuilderIndex]);
+        }
+        else
+        {
+            AZ_Warning(
+                "Asset Processor",
+                false,
+                "BuilderInfoMetricsModel cannot find the GUID of the builder selected by the user (%s) in itself. No metrics will be "
+                "shown in the builder tab.",
+                builder.m_busId.ToString<AZStd::string>().c_str());
+            m_data->m_currentSelectedBuilderIndex = aznumeric_cast<int>(BuilderData::BuilderSelection::Invalid);
+        }
+        
+        endResetModel();
+    }
+
+    QModelIndex BuilderInfoMetricsModel::index(int row, int column, const QModelIndex& parent) const
+    {
+        if (!hasIndex(row, column, parent))
+        {
+            return QModelIndex();
+        }
+
+        BuilderDataItem* const parentItem =
+            parent.isValid() ? static_cast<BuilderDataItem*>(parent.internalPointer()) : m_data->m_root.get();
+
+        if (parentItem)
+        {
+            AZStd::shared_ptr<BuilderDataItem> childItem = parentItem->GetChild(row);
+            if (childItem)
+            {
+                QModelIndex index = createIndex(row, column, childItem.get());
+                Q_ASSERT(checkIndex(index));
+                return index;
+            }
+        }
+
+        return QModelIndex();
+    }
+
+    int BuilderInfoMetricsModel::rowCount(const QModelIndex& parent) const
+    {
+        BuilderDataItem* const parentItem =
+            parent.isValid() ? static_cast<BuilderDataItem*>(parent.internalPointer()) : m_data->m_root.get();
+
+        if (!parentItem)
+        {
+            return 0;
+        }
+        return parentItem->ChildCount();
+    }
+
+    int BuilderInfoMetricsModel::columnCount([[maybe_unused]] const QModelIndex& parent) const
+    {
+        return aznumeric_cast<int>(Column::Max);
+    }
+
+    QVariant BuilderInfoMetricsModel::data(const QModelIndex& index, int role) const
+    {
+        if (!index.isValid())
+        {
+            return QVariant();
+        }
+
+        BuilderDataItem* item = static_cast<BuilderDataItem*>(index.internalPointer());
+        switch (role)
+        {
+        case aznumeric_cast<int>(Role::SortRole):
+            switch (index.column())
+            {
+            case aznumeric_cast<int>(Column::AverageDuration):
+                if (item->GetJobCount() == 0)
+                {
+                    return QVariant();
+                }
+                return item->GetTotalDuration() / item->GetJobCount();
+            case aznumeric_cast<int>(Column::TotalDuration):
+                return item->GetTotalDuration();
+            // Other columns are sorted by Qt::DisplayRole immediately below
+            }
+        case Qt::DisplayRole:
+            switch (index.column())
+            {
+            case aznumeric_cast<int>(Column::Name):
+                return item->GetName();
+            case aznumeric_cast<int>(Column::JobCount):
+                return item->GetJobCount();
+            case aznumeric_cast<int>(Column::AverageDuration):
+                if (item->GetJobCount() == 0)
+                {
+                    return QVariant();
+                }
+                return DurationToQString(item->GetTotalDuration() / item->GetJobCount());
+            case aznumeric_cast<int>(Column::TotalDuration):
+                return DurationToQString(item->GetTotalDuration());
+            default:
+                break;
+            }
+        default:
+            break;
+        }
+
+        return QVariant();
+    }
+
+    QVariant BuilderInfoMetricsModel::headerData(int section, Qt::Orientation orientation, int role) const
+    {
+        if (orientation != Qt::Horizontal || role != Qt::DisplayRole || section < 0 || section >= aznumeric_cast<int>(Column::Max))
+        {
+            return QVariant();
+        }
+
+        switch (section)
+        {
+        case aznumeric_cast<int>(Column::Name):
+            return tr("Name");
+        case aznumeric_cast<int>(Column::JobCount):
+            return tr("Job Count");
+        case aznumeric_cast<int>(Column::AverageDuration):
+            return tr("Average Duration");
+        case aznumeric_cast<int>(Column::TotalDuration):
+            return tr("Total Duration");
+        default:
+            AZ_Warning("Asset Processor", false, "Unhandled BuilderInfoMetricsModel header %d", section);
+            break;
+        }
+        return QVariant();
+    }
+
+    QModelIndex BuilderInfoMetricsModel::parent(const QModelIndex& index) const
+    {
+        if (!index.isValid())
+        {
+            return QModelIndex();
+        }
+
+        auto currentItem = static_cast<BuilderDataItem*>(index.internalPointer());
+        auto parentItem = currentItem->GetParent();
+        auto rootItem = m_data->m_root;
+            
+        if (parentItem.expired())
+        {
+            return QModelIndex();
+        }
+
+        auto sharedParentitem = parentItem.lock();
+        if (sharedParentitem == rootItem || sharedParentitem == nullptr)
+        {
+            return QModelIndex();
+        }
+
+        QModelIndex parentIndex = createIndex(sharedParentitem->GetRow(), 0, sharedParentitem.get());
+        Q_ASSERT(checkIndex(parentIndex));
+        return parentIndex;
+    }
+
+    void BuilderInfoMetricsModel::OnDurationChanged(BuilderDataItem* item)
+    {
+        while (item)
+        {
+            const int rowNum = item->GetRow();
+            dataChanged(
+                createIndex(rowNum, aznumeric_cast<int>(Column::JobCount), item),
+                createIndex(rowNum, aznumeric_cast<int>(Column::AverageDuration), item));
+            item = item->GetParent().expired() ? nullptr : item->GetParent().lock().get();
+        }
+    }
+
+    BuilderInfoMetricsSortModel::BuilderInfoMetricsSortModel(QObject* parent)
+        : QSortFilterProxyModel(parent)
+    {
+    }
+}

+ 70 - 0
Code/Tools/AssetProcessor/native/ui/BuilderInfoMetricsModel.h

@@ -0,0 +1,70 @@
+/*
+ * 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/std/string/string.h>
+#include <QPointer>
+#include <QAbstractItemModel>
+#include <QSortFilterProxyModel>
+#include <AzToolsFramework/AssetDatabase/AssetDatabaseConnection.h>
+
+namespace AssetBuilderSDK
+{
+    struct AssetBuilderDesc;
+} // namespace AssetBuilderSDK
+
+namespace AssetProcessor
+{
+    class BuilderDataItem;
+    class BuilderData;
+    class JobEntry;
+
+    class BuilderInfoMetricsModel
+        : public QAbstractItemModel
+    {
+    public:
+        enum class Column
+        {
+            Name,
+            JobCount,
+            TotalDuration,
+            AverageDuration,
+            Max
+        };
+
+        enum class Role
+        {
+            SortRole = Qt::UserRole
+        };
+
+        BuilderInfoMetricsModel(BuilderData* builderData, QObject* parent = nullptr);
+        void Reset();
+        void OnBuilderSelectionChanged(const AssetBuilderSDK::AssetBuilderDesc& builder);
+
+        // QAbstractItemModel
+        QModelIndex index(int row, int column, const QModelIndex& parent = QModelIndex()) const override;
+        int rowCount(const QModelIndex& parent = QModelIndex()) const override;
+        int columnCount(const QModelIndex& parent = QModelIndex()) const override;
+        QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
+        QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
+        QModelIndex parent(const QModelIndex& index) const override;
+
+    public Q_SLOTS:
+        void OnDurationChanged(BuilderDataItem* item);
+
+    private:
+        QPointer<BuilderData> m_data;
+    };
+
+    class BuilderInfoMetricsSortModel : public QSortFilterProxyModel
+    {
+    public:
+        BuilderInfoMetricsSortModel(QObject* parent = nullptr);
+    };
+} // namespace AssetProcessor

+ 97 - 0
Code/Tools/AssetProcessor/native/ui/BuilderInfoPatternsModel.cpp

@@ -0,0 +1,97 @@
+/*
+ * 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 <ui/BuilderInfoPatternsModel.h>
+#include <AssetBuilderSDK/AssetBuilderSDK.h>
+
+namespace AssetProcessor
+{
+    BuilderInfoPatternsModel::BuilderInfoPatternsModel(QObject* parent)
+        : QAbstractItemModel(parent)
+    {
+    }
+
+    QModelIndex BuilderInfoPatternsModel::index(int row, int column, const QModelIndex& parent) const
+    {
+        if (row < 0 || row >= rowCount(parent) || column < 0 || column >= columnCount(parent))
+        {
+            return QModelIndex();
+        }
+        return createIndex(row, column);
+    }
+
+    int BuilderInfoPatternsModel::rowCount([[maybe_unused]] const QModelIndex& parent) const
+    {
+        return aznumeric_cast<int>(m_data.size());
+    }
+
+    int BuilderInfoPatternsModel::columnCount([[maybe_unused]] const QModelIndex& parent) const
+    {
+        return aznumeric_cast<int>(Column::Max);
+    }
+
+    QVariant BuilderInfoPatternsModel::data(const QModelIndex& index, int role) const
+    {
+        if (!index.isValid() || index.row() >= m_data.size())
+        {
+            return QVariant();
+        }
+        if (role == Qt::DisplayRole)
+        {
+            switch (index.column())
+            {
+            case aznumeric_cast<int>(Column::Type):
+                switch (m_data[index.row()].m_type)
+                {
+                case AssetBuilderSDK::AssetBuilderPattern::Regex:
+                    return tr("Regex");
+                case AssetBuilderSDK::AssetBuilderPattern::Wildcard:
+                    return tr("Wildcard");
+                default:
+                    return QVariant();
+                }
+                break;
+            case aznumeric_cast<int>(Column::Extension):
+                return m_data[index.row()].m_pattern.c_str();
+            default:
+                break;
+            }
+        }
+
+        return QVariant();
+    }
+
+    QVariant BuilderInfoPatternsModel::headerData(int section, Qt::Orientation orientation, int role) const
+    {
+        if (orientation == Qt::Horizontal && role == Qt::DisplayRole)
+        {
+            switch (section)
+            {
+            case aznumeric_cast<int>(Column::Type):
+                return tr("Type");
+            case aznumeric_cast<int>(Column::Extension):
+                return tr("Extension");
+            default:
+                break;
+            }
+        }
+
+        return QAbstractItemModel::headerData(section, orientation, role);
+    }
+
+    QModelIndex BuilderInfoPatternsModel::parent([[maybe_unused]] const QModelIndex& index) const
+    {
+        return QModelIndex();
+    }
+
+    void BuilderInfoPatternsModel::Reset(const AssetBuilderSDK::AssetBuilderDesc& builder)
+    {
+        beginResetModel();
+        m_data = builder.m_patterns;
+        endResetModel();
+    }
+} // namespace AssetProcessor

+ 48 - 0
Code/Tools/AssetProcessor/native/ui/BuilderInfoPatternsModel.h

@@ -0,0 +1,48 @@
+/*
+ * 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 <QAbstractItemModel>
+#include <AzCore/std/containers/vector.h>
+
+namespace AssetBuilderSDK
+{
+    struct AssetBuilderDesc;
+    struct AssetBuilderPattern;
+}
+
+namespace AssetProcessor
+{
+    class BuilderInfoPatternsModel : public QAbstractItemModel
+    {
+        Q_OBJECT
+    public:
+        enum class Column
+        {
+            Type,
+            Extension,
+            Max
+        };
+
+        BuilderInfoPatternsModel(QObject* parent = nullptr);
+
+        // QAbstractItemModel
+        QModelIndex index(int row, int column, const QModelIndex& parent) const override;
+
+        int rowCount(const QModelIndex& parent = QModelIndex()) const override;
+        int columnCount(const QModelIndex& parent = QModelIndex()) const override;
+        QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
+        QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
+        QModelIndex parent(const QModelIndex& index) const override;
+
+        void Reset(const AssetBuilderSDK::AssetBuilderDesc& builder);
+    private:
+        AZStd::vector<AssetBuilderSDK::AssetBuilderPattern> m_data;
+    };
+}

+ 43 - 24
Code/Tools/AssetProcessor/native/ui/MainWindow.cpp

@@ -15,7 +15,11 @@
 #include "ProductDependencyTreeItemData.h"
 #include "SourceAssetTreeItemData.h"
 #include "SourceAssetTreeModel.h"
-#include <ui/SourceAssetTreeFilterModel.h>
+#include <native/ui/BuilderData.h>
+#include <native/ui/BuilderDataItem.h>
+#include <native/ui/BuilderInfoPatternsModel.h>
+#include <native/ui/BuilderInfoMetricsModel.h>
+#include <native/ui/SourceAssetTreeFilterModel.h>
 
 #include <AzFramework/Asset/AssetSystemBus.h>
 #include <AzCore/JSON/stringbuffer.h>
@@ -166,6 +170,7 @@ MainWindow::MainWindow(GUIApplicationManager* guiApplicationManager, QWidget* pa
     , m_fileSystemWatcher(new QFileSystemWatcher(this))
     , m_builderList(new BuilderListModel(this))
     , m_builderListSortFilterProxy(new BuilderListSortFilterProxy(this))
+    , m_builderInfoPatterns(new AssetProcessor::BuilderInfoPatternsModel(this))
 {
     ui->setupUi(this);
 
@@ -485,11 +490,21 @@ void MainWindow::Activate()
     m_jobsModel->PopulateJobsFromDatabase();
 
     // Builders Tab:
-
+    m_builderData = new BuilderData(m_sharedDbConnection, this);
     m_builderListSortFilterProxy->setDynamicSortFilter(true);
     m_builderListSortFilterProxy->setSourceModel(m_builderList);
     m_builderListSortFilterProxy->sort(0);
     ui->builderList->setModel(m_builderListSortFilterProxy);
+    ui->builderInfoPatternsTableView->setModel(m_builderInfoPatterns);
+    m_builderInfoMetrics = new BuilderInfoMetricsModel(m_builderData, this);
+    m_builderInfoMetricsSort = new BuilderInfoMetricsSortModel(this);
+    m_builderInfoMetricsSort->setSourceModel(m_builderInfoMetrics);
+    m_builderInfoMetricsSort->setSortRole(aznumeric_cast<int>(BuilderInfoMetricsModel::Role::SortRole));
+    ui->builderInfoMetricsTreeView->setModel(m_builderInfoMetricsSort);
+    ui->builderInfoMetricsTreeView->setColumnWidth(0, 400);
+    ui->builderInfoMetricsTreeView->setColumnWidth(1, 70);
+    ui->builderInfoMetricsTreeView->setColumnWidth(2, 150);
+    ui->builderInfoMetricsTreeView->setColumnWidth(3, 150);
     connect(ui->builderList->selectionModel(), &QItemSelectionModel::selectionChanged, this, &MainWindow::BuilderTabSelectionChanged);
     connect(m_guiApplicationManager, &GUIApplicationManager::OnBuildersRegistered, [this]()
     {
@@ -502,7 +517,23 @@ void MainWindow::Activate()
                 m_builderListSortFilterProxy->sort(0);
             }
         }
+
+        if (m_builderInfoMetrics)
+        {
+            m_builderInfoMetrics->Reset();
+        }
     });
+    connect(
+        m_guiApplicationManager->GetAssetProcessorManager(),
+        &AssetProcessorManager::JobProcessDurationChanged,
+        m_builderData,
+        &BuilderData::OnProcessJobDurationChanged);
+    connect(
+        m_guiApplicationManager->GetAssetProcessorManager(),
+        &AssetProcessorManager::CreateJobsDurationChanged,
+        m_builderData,
+        &BuilderData::OnCreateJobsDurationChanged);
+    connect(m_builderData, &BuilderData::DurationChanged, m_builderInfoMetrics, &BuilderInfoMetricsModel::OnDurationChanged);
 
     // Tools tab:
     connect(ui->fullScanButton, &QPushButton::clicked, this, &MainWindow::OnRescanButtonClicked);
@@ -562,28 +593,16 @@ void MainWindow::BuilderTabSelectionChanged(const QItemSelection& selected, cons
 
         AZ_Assert(index.isValid(), "BuilderTabSelectionChanged index out of bounds");
 
-        const auto& builder = builders[index.row()];
-        QString patternString;
-
-        for (const auto& pattern : builder.m_patterns)
-        {
-            patternString.append("\n\t");
-            patternString.append(pattern.ToString().c_str());
-        }
-
-        ui->builderDetails->setPlainText(
-            QString("Name: %1\n"
-                    "Type: %2\n"
-                    "Fingerprint: %3\n"
-                    "Version Number: %4\n"
-                    "BusId: %5\n"
-                    "Patterns: %6")
-                .arg(builder.m_name.c_str())
-                .arg(builder.m_builderType == AssetBuilderSDK::AssetBuilderDesc::AssetBuilderType::Internal ? "Internal" : "External")
-                .arg(builder.m_analysisFingerprint.c_str())
-                .arg(builder.m_version)
-                .arg(builder.m_busId.ToString<QString>())
-                .arg(patternString));
+        const auto builder = builders[index.row()];
+        m_builderInfoPatterns->Reset(builder);
+        m_builderInfoMetrics->OnBuilderSelectionChanged(builder);
+        ui->builderInfoMetricsTreeView->expandToDepth(0);
+        ui->builderInfoHeaderValueName->setText(builder.m_name.c_str());
+        ui->builderInfoHeaderValueType->setText(
+            builder.m_builderType == AssetBuilderSDK::AssetBuilderDesc::AssetBuilderType::Internal ? "Internal" : "External");
+        ui->builderInfoHeaderValueFingerprint->setText(builder.m_analysisFingerprint.c_str());
+        ui->builderInfoHeaderValueVersionNumber->setText(QString::number(builder.m_version));
+        ui->builderInfoHeaderValueBusId->setText(builder.m_busId.ToString<QString>());        
     }
 }
 

+ 11 - 2
Code/Tools/AssetProcessor/native/ui/MainWindow.h

@@ -47,6 +47,10 @@ namespace AssetProcessor
     class SourceAssetTreeModel;
     class ProductDependencyTreeItem;
     class JobEntry;
+    class BuilderData;
+    class BuilderInfoPatternsModel;
+    class BuilderInfoMetricsModel;
+    class BuilderInfoMetricsSortModel;
 }
 
 class MainWindow
@@ -151,8 +155,13 @@ private:
     int m_createJobCount = 0;
     QFileSystemWatcher* m_fileSystemWatcher;
     Config m_config;
-    BuilderListModel* m_builderList;
-    BuilderListSortFilterProxy* m_builderListSortFilterProxy;
+
+    AssetProcessor::BuilderData* m_builderData = nullptr;
+    BuilderListModel* m_builderList = nullptr;
+    BuilderListSortFilterProxy* m_builderListSortFilterProxy = nullptr;
+    AssetProcessor::BuilderInfoPatternsModel* m_builderInfoPatterns = nullptr;
+    AssetProcessor::BuilderInfoMetricsModel* m_builderInfoMetrics = nullptr;
+    AssetProcessor::BuilderInfoMetricsSortModel* m_builderInfoMetricsSort = nullptr;
     AssetProcessor::CacheServerData m_cacheServerData;
 
     void SetContextLogDetailsVisible(bool visible);

+ 276 - 40
Code/Tools/AssetProcessor/native/ui/MainWindow.ui

@@ -6,8 +6,8 @@
    <rect>
     <x>0</x>
     <y>0</y>
-    <width>1177</width>
-    <height>793</height>
+    <width>800</width>
+    <height>600</height>
    </rect>
   </property>
   <property name="minimumSize">
@@ -1189,7 +1189,10 @@
          </layout>
         </widget>
         <widget class="QWidget" name="BuildersPage">
-         <layout class="QHBoxLayout" name="horizontalLayout_4">
+         <layout class="QVBoxLayout" name="BuildersPageVerticalLayout">
+          <property name="spacing">
+           <number>6</number>
+          </property>
           <property name="leftMargin">
            <number>0</number>
           </property>
@@ -1203,54 +1206,287 @@
            <number>0</number>
           </property>
           <item>
-           <layout class="QVBoxLayout" name="verticalLayout">
-            <item>
-             <widget class="QLabel" name="label">
-              <property name="text">
-               <string>Below is a list of all builders loaded by Asset Processor.  Builders are used by the asset processor to transform source assets into product assets. Builders are mostly defined in C++, and the source for them can be in Gems, the engine, or your project.  If a builder is missing from the list, make sure the owning gem is enabled and the component is activated. &lt;a href=&quot;https://www.o3de.org/docs/user-guide/assets/pipeline/asset-builders/&quot;&gt;Click here&lt;/a&gt; for documentation on creating a builder.</string>
+           <widget class="QLabel" name="buildersPageDescription">
+            <property name="sizePolicy">
+             <sizepolicy hsizetype="Expanding" vsizetype="Preferred">
+              <horstretch>0</horstretch>
+              <verstretch>0</verstretch>
+             </sizepolicy>
+            </property>
+            <property name="maximumSize">
+             <size>
+              <width>16777215</width>
+              <height>50</height>
+             </size>
+            </property>
+            <property name="text">
+             <string>Below is a list of all builders loaded by Asset Processor.  Builders are used by the asset processor to transform source assets into product assets. Builders are mostly defined in C++, and the source for them can be in Gems, the engine, or your project.  If a builder is missing from the list, make sure the owning gem is enabled and the component is activated. &lt;a href=&quot;https://www.o3de.org/docs/user-guide/assets/pipeline/asset-builders/&quot;&gt;Click here&lt;/a&gt; for documentation on creating a builder.</string>
+            </property>
+            <property name="alignment">
+             <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
+            </property>
+            <property name="wordWrap">
+             <bool>true</bool>
+            </property>
+            <property name="openExternalLinks">
+             <bool>true</bool>
+            </property>
+            <property name="textInteractionFlags">
+             <set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse</set>
+            </property>
+           </widget>
+          </item>
+          <item>
+           <widget class="QSplitter" name="buildersPageSplitter">
+            <property name="sizePolicy">
+             <sizepolicy hsizetype="Expanding" vsizetype="Preferred">
+              <horstretch>0</horstretch>
+              <verstretch>0</verstretch>
+             </sizepolicy>
+            </property>
+            <property name="orientation">
+             <enum>Qt::Horizontal</enum>
+            </property>
+            <widget class="QListView" name="builderList">
+             <property name="sizePolicy">
+              <sizepolicy hsizetype="Expanding" vsizetype="Expanding">
+               <horstretch>2</horstretch>
+               <verstretch>0</verstretch>
+              </sizepolicy>
+             </property>
+             <property name="alternatingRowColors">
+              <bool>true</bool>
+             </property>
+            </widget>
+            <widget class="QWidget" name="builderInfo" native="true">
+             <property name="sizePolicy">
+              <sizepolicy hsizetype="Expanding" vsizetype="Expanding">
+               <horstretch>3</horstretch>
+               <verstretch>0</verstretch>
+              </sizepolicy>
+             </property>
+             <layout class="QVBoxLayout" name="builderInfoLayout" stretch="0,0">
+              <property name="leftMargin">
+               <number>0</number>
               </property>
-              <property name="wordWrap">
-               <bool>true</bool>
+              <property name="topMargin">
+               <number>0</number>
               </property>
-              <property name="openExternalLinks">
-               <bool>true</bool>
+              <property name="rightMargin">
+               <number>0</number>
               </property>
-              <property name="textInteractionFlags">
-               <set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse</set>
+              <property name="bottomMargin">
+               <number>0</number>
               </property>
-             </widget>
-            </item>
-            <item>
-             <layout class="QHBoxLayout" name="horizontalLayout_7">
               <item>
-               <widget class="QListView" name="builderList">
-                <property name="alternatingRowColors">
-                 <bool>true</bool>
-                </property>
-               </widget>
+               <layout class="QGridLayout" name="builderInfoHeader" columnstretch="1,4">
+                <item row="3" column="0">
+                 <widget class="QLabel" name="builderInfoHeaderTitleVersionNumber">
+                  <property name="font">
+                   <font>
+                    <weight>75</weight>
+                    <italic>false</italic>
+                    <bold>true</bold>
+                   </font>
+                  </property>
+                  <property name="styleSheet">
+                   <string notr="true">font: bold;</string>
+                  </property>
+                  <property name="text">
+                   <string>Version Number</string>
+                  </property>
+                  <property name="wordWrap">
+                   <bool>true</bool>
+                  </property>
+                 </widget>
+                </item>
+                <item row="0" column="1">
+                 <widget class="QLabel" name="builderInfoHeaderValueName">
+                  <property name="text">
+                   <string/>
+                  </property>
+                  <property name="wordWrap">
+                   <bool>true</bool>
+                  </property>
+                 </widget>
+                </item>
+                <item row="0" column="0">
+                 <widget class="QLabel" name="builderInfoHeaderTitleName">
+                  <property name="font">
+                   <font>
+                    <weight>75</weight>
+                    <italic>false</italic>
+                    <bold>true</bold>
+                   </font>
+                  </property>
+                  <property name="styleSheet">
+                   <string notr="true">font: bold;</string>
+                  </property>
+                  <property name="text">
+                   <string>Name</string>
+                  </property>
+                  <property name="wordWrap">
+                   <bool>true</bool>
+                  </property>
+                 </widget>
+                </item>
+                <item row="1" column="1">
+                 <widget class="QLabel" name="builderInfoHeaderValueType">
+                  <property name="text">
+                   <string/>
+                  </property>
+                  <property name="wordWrap">
+                   <bool>true</bool>
+                  </property>
+                 </widget>
+                </item>
+                <item row="2" column="0">
+                 <widget class="QLabel" name="builderInfoHeaderTitleFingerprint">
+                  <property name="font">
+                   <font>
+                    <weight>75</weight>
+                    <italic>false</italic>
+                    <bold>true</bold>
+                   </font>
+                  </property>
+                  <property name="styleSheet">
+                   <string notr="true">font: bold;</string>
+                  </property>
+                  <property name="text">
+                   <string>Fingerprint</string>
+                  </property>
+                  <property name="wordWrap">
+                   <bool>true</bool>
+                  </property>
+                 </widget>
+                </item>
+                <item row="1" column="0">
+                 <widget class="QLabel" name="builderInfoHeaderTitleType">
+                  <property name="font">
+                   <font>
+                    <weight>75</weight>
+                    <italic>false</italic>
+                    <bold>true</bold>
+                   </font>
+                  </property>
+                  <property name="styleSheet">
+                   <string notr="true">font: bold;</string>
+                  </property>
+                  <property name="text">
+                   <string>Type</string>
+                  </property>
+                  <property name="wordWrap">
+                   <bool>true</bool>
+                  </property>
+                 </widget>
+                </item>
+                <item row="4" column="0">
+                 <widget class="QLabel" name="builderInfoHeaderTitleBusId">
+                  <property name="font">
+                   <font>
+                    <weight>75</weight>
+                    <italic>false</italic>
+                    <bold>true</bold>
+                   </font>
+                  </property>
+                  <property name="styleSheet">
+                   <string notr="true">font: bold;</string>
+                  </property>
+                  <property name="text">
+                   <string>BusID</string>
+                  </property>
+                  <property name="wordWrap">
+                   <bool>true</bool>
+                  </property>
+                 </widget>
+                </item>
+                <item row="2" column="1">
+                 <widget class="QLabel" name="builderInfoHeaderValueFingerprint">
+                  <property name="text">
+                   <string/>
+                  </property>
+                  <property name="wordWrap">
+                   <bool>true</bool>
+                  </property>
+                 </widget>
+                </item>
+                <item row="3" column="1">
+                 <widget class="QLabel" name="builderInfoHeaderValueVersionNumber">
+                  <property name="text">
+                   <string/>
+                  </property>
+                  <property name="wordWrap">
+                   <bool>true</bool>
+                  </property>
+                 </widget>
+                </item>
+                <item row="4" column="1">
+                 <widget class="QLabel" name="builderInfoHeaderValueBusId">
+                  <property name="text">
+                   <string/>
+                  </property>
+                  <property name="wordWrap">
+                   <bool>true</bool>
+                  </property>
+                 </widget>
+                </item>
+               </layout>
               </item>
               <item>
-               <widget class="QPlainTextEdit" name="builderDetails">
-                <property name="inputMethodHints">
-                 <set>Qt::ImhMultiLine|Qt::ImhNoEditMenu</set>
-                </property>
-                <property name="horizontalScrollBarPolicy">
-                 <enum>Qt::ScrollBarAlwaysOff</enum>
-                </property>
-                <property name="undoRedoEnabled">
-                 <bool>false</bool>
-                </property>
-                <property name="readOnly">
-                 <bool>true</bool>
-                </property>
-                <property name="backgroundVisible">
-                 <bool>false</bool>
+               <widget class="QTabWidget" name="builderInfoTabWidget">
+                <property name="currentIndex">
+                 <number>0</number>
                 </property>
+                <widget class="QWidget" name="builderInfoPatternsTab">
+                 <attribute name="title">
+                  <string>Patterns</string>
+                 </attribute>
+                 <layout class="QVBoxLayout" name="verticalLayout_2">
+                  <item>
+                   <widget class="AzQtComponents::TableView" name="builderInfoPatternsTableView">
+                    <property name="indentation">
+                     <number>0</number>
+                    </property>
+                    <property name="itemsExpandable">
+                     <bool>false</bool>
+                    </property>
+                    <property name="sortingEnabled">
+                     <bool>true</bool>
+                    </property>
+                    <property name="expandsOnDoubleClick">
+                     <bool>false</bool>
+                    </property>
+                    <attribute name="headerShowSortIndicator" stdset="0">
+                     <bool>false</bool>
+                    </attribute>
+                   </widget>
+                  </item>
+                 </layout>
+                </widget>
+                <widget class="QWidget" name="builderInfoDetailsTab">
+                 <attribute name="title">
+                  <string>Details</string>
+                 </attribute>
+                </widget>
+                <widget class="QWidget" name="builderInfoMetricsTab">
+                 <attribute name="title">
+                  <string>Metrics</string>
+                 </attribute>
+                 <layout class="QVBoxLayout" name="verticalLayout_7">
+                  <item>
+                   <widget class="AzQtComponents::TableView" name="builderInfoMetricsTreeView">
+                    <property name="sortingEnabled">
+                     <bool>true</bool>
+                    </property>
+                   </widget>
+                  </item>
+                 </layout>
+                </widget>
                </widget>
               </item>
              </layout>
-            </item>
-           </layout>
+            </widget>
+           </widget>
           </item>
          </layout>
         </widget>