diff --git a/Gems/Multiplayer/Code/CMakeLists.txt b/Gems/Multiplayer/Code/CMakeLists.txt index 49b404fb1c..909590a25d 100644 --- a/Gems/Multiplayer/Code/CMakeLists.txt +++ b/Gems/Multiplayer/Code/CMakeLists.txt @@ -179,6 +179,10 @@ if (PAL_TRAIT_BUILD_TESTS_SUPPORTED) ly_add_googletest( NAME Gem::Multiplayer.Tests ) + ly_add_googlebenchmark( + NAME Gem::Multiplayer.Benchmarks + TARGET Gem::Multiplayer.Tests + ) if (PAL_TRAIT_BUILD_HOST_TOOLS) ly_add_target( diff --git a/Gems/Multiplayer/Code/Include/Multiplayer/Components/NetBindComponent.h b/Gems/Multiplayer/Code/Include/Multiplayer/Components/NetBindComponent.h index f604102e37..265137a078 100644 --- a/Gems/Multiplayer/Code/Include/Multiplayer/Components/NetBindComponent.h +++ b/Gems/Multiplayer/Code/Include/Multiplayer/Components/NetBindComponent.h @@ -193,6 +193,7 @@ namespace Multiplayer friend class EntityReplicationManager; friend class HierarchyTests; + friend class HierarchyBenchmarkBase; }; bool NetworkRoleHasController(NetEntityRole networkRole); diff --git a/Gems/Multiplayer/Code/Include/Multiplayer/Components/NetworkHierarchyRootComponent.h b/Gems/Multiplayer/Code/Include/Multiplayer/Components/NetworkHierarchyRootComponent.h index 4c9f94c004..674dc6114d 100644 --- a/Gems/Multiplayer/Code/Include/Multiplayer/Components/NetworkHierarchyRootComponent.h +++ b/Gems/Multiplayer/Code/Include/Multiplayer/Components/NetworkHierarchyRootComponent.h @@ -96,5 +96,7 @@ namespace Multiplayer //! Set to false when deactivating or otherwise not to be included in hierarchy considerations. bool m_isHierarchyEnabled = true; + + friend class HierarchyBenchmarkBase; }; } diff --git a/Gems/Multiplayer/Code/Tests/CommonBenchmarkSetup.h b/Gems/Multiplayer/Code/Tests/CommonBenchmarkSetup.h new file mode 100644 index 0000000000..e0018e2f26 --- /dev/null +++ b/Gems/Multiplayer/Code/Tests/CommonBenchmarkSetup.h @@ -0,0 +1,640 @@ +/* + * 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 + +#ifdef HAVE_BENCHMARK +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Multiplayer +{ + class BenchmarkComponentApplicationRequests : public AZ::ComponentApplicationRequests + { + public: + void RegisterComponentDescriptor([[maybe_unused]] const AZ::ComponentDescriptor* descriptor) override {} + void UnregisterComponentDescriptor([[maybe_unused]] const AZ::ComponentDescriptor* descriptor) override {} + AZ::ComponentApplication* GetApplication() override { return {}; } + void RegisterEntityAddedEventHandler([[maybe_unused]] AZ::Event::Handler& handler) override {} + void RegisterEntityRemovedEventHandler([[maybe_unused]] AZ::Event::Handler& handler) override {} + void RegisterEntityActivatedEventHandler([[maybe_unused]] AZ::Event::Handler& handler) override {} + void RegisterEntityDeactivatedEventHandler([[maybe_unused]] AZ::Event::Handler& handler) override {} + void SignalEntityActivated([[maybe_unused]] AZ::Entity* entity) override {} + void SignalEntityDeactivated([[maybe_unused]] AZ::Entity* entity) override {} + bool RemoveEntity([[maybe_unused]] AZ::Entity* entity) override { return {}; } + bool DeleteEntity([[maybe_unused]] const AZ::EntityId& id) override { return {}; } + void EnumerateEntities([[maybe_unused]] const EntityCallback& callback) override {} + AZ::SerializeContext* GetSerializeContext() override { return {}; } + AZ::BehaviorContext* GetBehaviorContext() override { return {}; } + AZ::JsonRegistrationContext* GetJsonRegistrationContext() override { return {}; } + const char* GetAppRoot() const override { return {}; } + const char* GetEngineRoot() const override { return {}; } + const char* GetExecutableFolder() const override { return {}; } + void QueryApplicationType([[maybe_unused]] AZ::ApplicationTypeQuery& appType) const override {} + + AZStd::map m_entities; + + bool AddEntity(AZ::Entity* entity) override + { + m_entities[entity->GetId()] = entity; + return true; + } + + AZ::Entity* FindEntity(const AZ::EntityId& id) override + { + const auto iterator = m_entities.find(id); + if (iterator != m_entities.end()) + { + return iterator->second; + } + + return nullptr; + } + }; + + + class BenchmarkConnectionListener : public AzNetworking::IConnectionListener + { + public: + ConnectResult ValidateConnect([[maybe_unused]] const IpAddress& remoteAddress, [[maybe_unused]] const IPacketHeader& packetHeader, [[maybe_unused]] ISerializer& serializer) override + { + return {}; + } + + void OnConnect([[maybe_unused]] IConnection* connection) override + { + } + + PacketDispatchResult OnPacketReceived([[maybe_unused]] IConnection* connection, [[maybe_unused]] const IPacketHeader& packetHeader, [[maybe_unused]] ISerializer& serializer) override + { + return {}; + } + + void OnPacketLost([[maybe_unused]] IConnection* connection, [[maybe_unused]] PacketId packetId) override + { + } + + void OnDisconnect([[maybe_unused]] IConnection* connection, [[maybe_unused]] DisconnectReason reason, [[maybe_unused]] TerminationEndpoint endpoint) override + { + } + }; + + class BenchmarkTime : public AZ::ITime + { + public: + AZ::TimeMs GetElapsedTimeMs() const override + { + return {}; + } + }; + + class BenchmarkNetworkTime : public Multiplayer::INetworkTime + { + public: + bool IsTimeRewound() const override + { + return false; + } + + HostFrameId GetHostFrameId() const override + { + return {}; + } + + HostFrameId GetUnalteredHostFrameId() const override + { + return {}; + } + + void IncrementHostFrameId() override + { + } + + AZ::TimeMs GetHostTimeMs() const override + { + return {}; + } + + float GetHostBlendFactor() const override + { + return 0; + } + + AzNetworking::ConnectionId GetRewindingConnectionId() const override + { + return {}; + } + + void ForceSetTime([[maybe_unused]] HostFrameId frameId, [[maybe_unused]] AZ::TimeMs timeMs) override + { + } + + void SyncEntitiesToRewindState([[maybe_unused]] const AZ::Aabb& rewindVolume) override + { + } + + void ClearRewoundEntities() override + { + } + + void AlterTime([[maybe_unused]] HostFrameId frameId, [[maybe_unused]] AZ::TimeMs timeMs, [[maybe_unused]] float blendFactor, [[maybe_unused]] AzNetworking::ConnectionId rewindConnectionId) override + { + } + }; + + class BenchmarkMultiplayerConnection : public IConnection + { + public: + BenchmarkMultiplayerConnection(ConnectionId connectionId, const IpAddress& address, [[maybe_unused]] ConnectionRole connectionRole) + : IConnection(connectionId, address) + { + ; + } + + ~BenchmarkMultiplayerConnection() override = default; + + bool SendReliablePacket([[maybe_unused]] const IPacket& packet) override + { + return false; + } + + PacketId SendUnreliablePacket([[maybe_unused]] const IPacket& packet) override + { + return {}; + } + + bool WasPacketAcked([[maybe_unused]] PacketId packetId) const override + { + return false; + } + + ConnectionState GetConnectionState() const override + { + return {}; + } + + ConnectionRole GetConnectionRole() const override + { + return {}; + } + + bool Disconnect([[maybe_unused]] DisconnectReason reason, [[maybe_unused]] TerminationEndpoint endpoint) override + { + return false; + } + + void SetConnectionMtu([[maybe_unused]] uint32_t connectionMtu) override + { + } + + uint32_t GetConnectionMtu() const override + { + return 0; + } + }; + + class BenchmarkNetworkEntityManager : public Multiplayer::INetworkEntityManager + { + public: + BenchmarkNetworkEntityManager() : m_authorityTracker(*this) {} + + NetworkEntityTracker* GetNetworkEntityTracker() override { return &m_tracker; } + NetworkEntityAuthorityTracker* GetNetworkEntityAuthorityTracker() override { return &m_authorityTracker; } + MultiplayerComponentRegistry* GetMultiplayerComponentRegistry() override { return &m_multiplayerComponentRegistry; } + HostId GetHostId() const override { return {}; } + EntityList CreateEntitiesImmediate( + [[maybe_unused]] const PrefabEntityId& prefabEntryId, + [[maybe_unused]] NetEntityRole netEntityRole, + [[maybe_unused]] const AZ::Transform& transform, + [[maybe_unused]] AutoActivate autoActivate) override { + return {}; + } + EntityList CreateEntitiesImmediate( + [[maybe_unused]] const PrefabEntityId& prefabEntryId, + [[maybe_unused]] NetEntityId netEntityId, + [[maybe_unused]] NetEntityRole netEntityRole, + [[maybe_unused]] AutoActivate autoActivate, + [[maybe_unused]] const AZ::Transform& transform) override { + return {}; + } + void SetupNetEntity( + [[maybe_unused]] AZ::Entity* netEntity, + [[maybe_unused]] PrefabEntityId prefabEntityId, + [[maybe_unused]] NetEntityRole netEntityRole) override {} + uint32_t GetEntityCount() const override { return {}; } + void MarkForRemoval( + [[maybe_unused]] const ConstNetworkEntityHandle& entityHandle) override {} + bool IsMarkedForRemoval( + [[maybe_unused]] const ConstNetworkEntityHandle& entityHandle) const override { + return {}; + } + void ClearEntityFromRemovalList( + [[maybe_unused]] const ConstNetworkEntityHandle& entityHandle) override {} + void ClearAllEntities() override {} + void AddEntityMarkedDirtyHandler( + [[maybe_unused]] AZ::Event<>::Handler& entityMarkedDirtyHandle) override {} + void AddEntityNotifyChangesHandler( + [[maybe_unused]] AZ::Event<>::Handler& entityNotifyChangesHandle) override {} + void AddEntityExitDomainHandler( + [[maybe_unused]] EntityExitDomainEvent::Handler& entityExitDomainHandler) override {} + void AddControllersActivatedHandler( + [[maybe_unused]] ControllersActivatedEvent::Handler& controllersActivatedHandler) override {} + void AddControllersDeactivatedHandler( + [[maybe_unused]] ControllersDeactivatedEvent::Handler& controllersDeactivatedHandler) override {} + void NotifyEntitiesDirtied() override {} + void NotifyEntitiesChanged() override {} + void NotifyControllersActivated( + [[maybe_unused]] const ConstNetworkEntityHandle& entityHandle, + [[maybe_unused]] EntityIsMigrating entityIsMigrating) override {} + void NotifyControllersDeactivated( + [[maybe_unused]] const ConstNetworkEntityHandle& entityHandle, + [[maybe_unused]] EntityIsMigrating entityIsMigrating) override {} + void HandleLocalRpcMessage( + [[maybe_unused]] NetworkEntityRpcMessage& message) override {} + + mutable AZStd::map m_networkEntityMap; + + NetworkEntityHandle AddEntityToEntityMap(NetEntityId netEntityId, AZ::Entity* entity) override + { + m_networkEntityMap[netEntityId] = entity; + return NetworkEntityHandle(entity, netEntityId, &m_tracker); + } + + ConstNetworkEntityHandle GetEntity(NetEntityId netEntityId) const override + { + AZ::Entity* entity = m_networkEntityMap[netEntityId]; + return ConstNetworkEntityHandle(entity, &m_tracker); + } + + NetEntityId GetNetEntityIdById(const AZ::EntityId& entityId) const override + { + for (const auto& pair : m_networkEntityMap) + { + if (pair.second->GetId() == entityId) + { + return pair.first; + } + } + + return InvalidNetEntityId; + } + + [[nodiscard]] AZStd::unique_ptr RequestNetSpawnableInstantiation( + [[maybe_unused]] const AZ::Data::Asset& netSpawnable, + [[maybe_unused]] const AZ::Transform& transform) override + { + return {}; + } + + NetworkEntityTracker m_tracker; + NetworkEntityAuthorityTracker m_authorityTracker; + MultiplayerComponentRegistry m_multiplayerComponentRegistry; + }; + + class BenchmarkMultiplayer : public Multiplayer::IMultiplayer + { + public: + BenchmarkMultiplayer(BenchmarkNetworkEntityManager& manager) : m_manager(manager) {} + + MultiplayerAgentType GetAgentType() const override { return {}; } + void InitializeMultiplayer([[maybe_unused]] MultiplayerAgentType state) override {} + bool StartHosting([[maybe_unused]] uint16_t port, [[maybe_unused]] bool isDedicated) override { return {}; } + bool Connect([[maybe_unused]] AZStd::string remoteAddress, [[maybe_unused]] uint16_t port) override { return {}; } + void Terminate([[maybe_unused]] AzNetworking::DisconnectReason reason) override {} + void AddClientDisconnectedHandler([[maybe_unused]] ClientDisconnectedEvent::Handler& handler) override {} + void AddConnectionAcquiredHandler([[maybe_unused]] ConnectionAcquiredEvent::Handler& handler) override {} + void AddSessionInitHandler([[maybe_unused]] SessionInitEvent::Handler& handler) override {} + void AddSessionShutdownHandler([[maybe_unused]] SessionShutdownEvent::Handler& handler) override {} + void SendReadyForEntityUpdates([[maybe_unused]] bool readyForEntityUpdates) override {} + AZ::TimeMs GetCurrentHostTimeMs() const override { return {}; } + float GetCurrentBlendFactor() const override { return {}; } + INetworkTime* GetNetworkTime() override { return {}; } + INetworkEntityManager* GetNetworkEntityManager() override { return &m_manager; } + void SetFilterEntityManager([[maybe_unused]] IFilterEntityManager* entityFilter) override {} + IFilterEntityManager* GetFilterEntityManager() override { return {}; } + + BenchmarkNetworkEntityManager& m_manager; + }; + + class HierarchyBenchmarkBase + : public benchmark::Fixture + , public AllocatorsBase + { + public: + void SetUp(const benchmark::State&) override + { + internalSetUp(); + } + void SetUp(benchmark::State&) override + { + internalSetUp(); + } + + void TearDown(const benchmark::State&) override + { + internalTearDown(); + } + void TearDown(benchmark::State&) override + { + internalTearDown(); + } + + virtual void internalSetUp() + { + SetupAllocator(); + AZ::NameDictionary::Create(); + + m_ComponentApplicationRequests = AZStd::make_unique(); + AZ::Interface::Register(m_ComponentApplicationRequests.get()); + + // register components involved in testing + m_serializeContext = AZStd::make_unique(); + + m_transformDescriptor.reset(AzFramework::TransformComponent::CreateDescriptor()); + m_transformDescriptor->Reflect(m_serializeContext.get()); + + m_netBindDescriptor.reset(NetBindComponent::CreateDescriptor()); + m_netBindDescriptor->Reflect(m_serializeContext.get()); + + m_hierarchyRootDescriptor.reset(NetworkHierarchyRootComponent::CreateDescriptor()); + m_hierarchyRootDescriptor->Reflect(m_serializeContext.get()); + + m_hierarchyChildDescriptor.reset(NetworkHierarchyChildComponent::CreateDescriptor()); + m_hierarchyChildDescriptor->Reflect(m_serializeContext.get()); + + m_netTransformDescriptor.reset(NetworkTransformComponent::CreateDescriptor()); + m_netTransformDescriptor->Reflect(m_serializeContext.get()); + + m_NetworkEntityManager = AZStd::make_unique(); + + m_Multiplayer = AZStd::make_unique(*m_NetworkEntityManager); + AZ::Interface::Register(m_Multiplayer.get()); + + // Create space for replication stats + // Without Multiplayer::RegisterMultiplayerComponents() the stats go to invalid id, which is fine for unit tests + GetMultiplayer()->GetStats().ReserveComponentStats(Multiplayer::InvalidNetComponentId, 50, 0); + + m_Time = AZStd::make_unique(); + AZ::Interface::Register(m_Time.get()); + + m_NetworkTime = AZStd::make_unique(); + AZ::Interface::Register(m_NetworkTime.get()); + + EXPECT_NE(AZ::Interface::Get()->GetNetworkEntityManager(), nullptr); + + const IpAddress address("localhost", 1, ProtocolType::Udp); + m_Connection = AZStd::make_unique(ConnectionId{ 1 }, address, ConnectionRole::Connector); + m_ConnectionListener = AZStd::make_unique(); + + m_entityReplicationManager = AZStd::make_unique(*m_Connection, *m_ConnectionListener, EntityReplicationManager::Mode::LocalClientToRemoteServer); + + m_console.reset(aznew AZ::Console()); + AZ::Interface::Register(m_console.get()); + m_console->LinkDeferredFunctors(AZ::ConsoleFunctorBase::GetDeferredHead()); + + RegisterMultiplayerComponents(); + } + + virtual void internalTearDown() + { + AZ::Interface::Unregister(m_console.get()); + m_console.reset(); + + m_entityReplicationManager.reset(); + + m_Connection.reset(); + m_ConnectionListener.reset(); + + AZ::Interface::Unregister(m_NetworkTime.get()); + AZ::Interface::Unregister(m_Time.get()); + AZ::Interface::Unregister(m_Multiplayer.get()); + AZ::Interface::Unregister(m_ComponentApplicationRequests.get()); + + m_Time.reset(); + + m_NetworkEntityManager.reset(); + m_Multiplayer.reset(); + + m_transformDescriptor.reset(); + m_netTransformDescriptor.reset(); + m_hierarchyRootDescriptor.reset(); + m_hierarchyChildDescriptor.reset(); + m_netBindDescriptor.reset(); + m_serializeContext.reset(); + m_ComponentApplicationRequests.reset(); + + AZ::NameDictionary::Destroy(); + TeardownAllocator(); + } + + AZStd::unique_ptr m_console; + + AZStd::unique_ptr m_ComponentApplicationRequests; + AZStd::unique_ptr m_serializeContext; + AZStd::unique_ptr m_transformDescriptor; + AZStd::unique_ptr m_netBindDescriptor; + AZStd::unique_ptr m_hierarchyRootDescriptor; + AZStd::unique_ptr m_hierarchyChildDescriptor; + AZStd::unique_ptr m_netTransformDescriptor; + + AZStd::unique_ptr m_Multiplayer; + AZStd::unique_ptr m_NetworkEntityManager; + AZStd::unique_ptr m_Time; + AZStd::unique_ptr m_NetworkTime; + + AZStd::unique_ptr m_Connection; + AZStd::unique_ptr m_ConnectionListener; + + AZStd::unique_ptr m_entityReplicationManager; + + void SetupEntity(const AZStd::unique_ptr& entity, NetEntityId netId, NetEntityRole role) + { + const auto netBindComponent = entity->FindComponent(); + EXPECT_NE(netBindComponent, nullptr); + netBindComponent->PreInit(entity.get(), PrefabEntityId{ AZ::Name("test"), 1 }, netId, role); + entity->Init(); + } + + static void StopEntity(const AZStd::unique_ptr& entity) + { + const auto netBindComponent = entity->FindComponent(); + EXPECT_NE(netBindComponent, nullptr); + netBindComponent->StopEntity(); + } + + static void StopAndDeactivateEntity(AZStd::unique_ptr& entity) + { + if (entity) + { + StopEntity(entity); + entity->Deactivate(); + entity.reset(); + } + } + + void CreateEntityWithRootHierarchy(AZStd::unique_ptr& rootEntity) + { + rootEntity->CreateComponent(); + rootEntity->CreateComponent(); + rootEntity->CreateComponent(); + rootEntity->CreateComponent(); + } + + void CreateEntityWithChildHierarchy(AZStd::unique_ptr& childEntity) + { + childEntity->CreateComponent(); + childEntity->CreateComponent(); + childEntity->CreateComponent(); + childEntity->CreateComponent(); + } + + void SetParentIdOnNetworkTransform(const AZStd::unique_ptr& entity, NetEntityId netParentId) + { + /* Derived from NetworkTransformComponent.AutoComponent.xml */ + constexpr int totalBits = 6 /*NetworkTransformComponentInternal::AuthorityToClientDirtyEnum::Count*/; + constexpr int parentIdBit = 4 /*NetworkTransformComponentInternal::AuthorityToClientDirtyEnum::parentEntityId_DirtyFlag*/; + + ReplicationRecord currentRecord; + currentRecord.m_authorityToClient.AddBits(totalBits); + currentRecord.m_authorityToClient.SetBit(parentIdBit, true); + + constexpr uint32_t bufferSize = 100; + AZStd::array buffer = {}; + NetworkInputSerializer inSerializer(buffer.begin(), bufferSize); + inSerializer.Serialize(reinterpret_cast(netParentId), + "parentEntityId", /* Derived from NetworkTransformComponent.AutoComponent.xml */ + AZStd::numeric_limits::min(), AZStd::numeric_limits::max()); + + NetworkOutputSerializer outSerializer(buffer.begin(), bufferSize); + + ReplicationRecord notifyRecord = currentRecord; + entity->FindComponent()->SerializeStateDeltaMessage(currentRecord, outSerializer); + entity->FindComponent()->NotifyStateDeltaChanges(notifyRecord); + } + + template + void SetHierarchyRootFieldOnNetworkHierarchyChild(const AZStd::unique_ptr& entity, NetEntityId value) + { + /* Derived from NetworkHierarchyChildComponent.AutoComponent.xml */ + constexpr int totalBits = 1 /*NetworkHierarchyChildComponentInternal::AuthorityToClientDirtyEnum::Count*/; + constexpr int inHierarchyBit = 0 /*NetworkHierarchyChildComponentInternal::AuthorityToClientDirtyEnum::hierarchyRoot_DirtyFlag*/; + + ReplicationRecord currentRecord; + currentRecord.m_authorityToClient.AddBits(totalBits); + currentRecord.m_authorityToClient.SetBit(inHierarchyBit, true); + + constexpr uint32_t bufferSize = 100; + AZStd::array buffer = {}; + NetworkInputSerializer inSerializer(buffer.begin(), bufferSize); + inSerializer.Serialize(reinterpret_cast(value), + "hierarchyRoot", /* Derived from NetworkHierarchyChildComponent.AutoComponent.xml */ + AZStd::numeric_limits::min(), AZStd::numeric_limits::max()); + + NetworkOutputSerializer outSerializer(buffer.begin(), bufferSize); + + ReplicationRecord notifyRecord = currentRecord; + entity->FindComponent()->SerializeStateDeltaMessage(currentRecord, outSerializer); + entity->FindComponent()->NotifyStateDeltaChanges(notifyRecord); + } + + struct EntityInfo + { + enum class Role + { + Root, + Child, + None + }; + + EntityInfo(AZ::u64 entityId, const char* entityName, NetEntityId netId, Role role) + : m_entity(AZStd::make_unique(AZ::EntityId(entityId), entityName)) + , m_netId(netId) + , m_role(role) + { + } + + ~EntityInfo() + { + StopAndDeactivateEntity(m_entity); + } + + AZStd::unique_ptr m_entity; + NetEntityId m_netId; + AZStd::unique_ptr m_replicator; + Role m_role = Role::None; + }; + + void PopulateHierarchicalEntity(const EntityInfo& entityInfo) + { + entityInfo.m_entity->CreateComponent(); + entityInfo.m_entity->CreateComponent(); + entityInfo.m_entity->CreateComponent(); + switch (entityInfo.m_role) + { + case EntityInfo::Role::Root: + entityInfo.m_entity->CreateComponent(); + break; + case EntityInfo::Role::Child: + entityInfo.m_entity->CreateComponent(); + break; + case EntityInfo::Role::None: + break; + } + } + + void CreateParent(EntityInfo& parent) + { + PopulateHierarchicalEntity(parent); + + SetupEntity(parent.m_entity, parent.m_netId, NetEntityRole::Authority); + + // Create an entity replicator for the child entity + const NetworkEntityHandle childHandle(parent.m_entity.get(), m_NetworkEntityManager->GetNetworkEntityTracker()); + parent.m_replicator = AZStd::make_unique(*m_entityReplicationManager, m_Connection.get(), NetEntityRole::Client, childHandle); + parent.m_replicator->Initialize(childHandle); + + parent.m_entity->Activate(); + } + + void CreateChildForParent(EntityInfo& child, EntityInfo& parent) + { + PopulateHierarchicalEntity(child); + + SetupEntity(child.m_entity, child.m_netId, NetEntityRole::Authority); + + // we need a parent-id value to be present in NetworkTransformComponent (which is in client mode and doesn't have a controller) + SetParentIdOnNetworkTransform(child.m_entity, parent.m_netId); + + // Create an entity replicator for the child entity + const NetworkEntityHandle childHandle(child.m_entity.get(), m_NetworkEntityManager->GetNetworkEntityTracker()); + child.m_replicator = AZStd::make_unique(*m_entityReplicationManager, m_Connection.get(), NetEntityRole::Client, childHandle); + child.m_replicator->Initialize(childHandle); + + child.m_entity->Activate(); + } + + void ForceRebuildHierarchy(const AZStd::unique_ptr& rootEntity) + { + if (NetworkHierarchyRootComponent* root = rootEntity->FindComponent()) + { + root->RebuildHierarchy(); + } + } + }; +} + +#endif diff --git a/Gems/Multiplayer/Code/Tests/ServerHierarchyBenchmarks.cpp b/Gems/Multiplayer/Code/Tests/ServerHierarchyBenchmarks.cpp new file mode 100644 index 0000000000..f6f82ebe2b --- /dev/null +++ b/Gems/Multiplayer/Code/Tests/ServerHierarchyBenchmarks.cpp @@ -0,0 +1,119 @@ +/* + * 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 + * + */ + +#ifdef HAVE_BENCHMARK +#include +#include + +namespace Multiplayer +{ + /* + * Hierarchy of 16 entities: Parent -> Child_2 -> .. -> Child_16 + * By default the maximum size of a hierarchy is defined by bg_hierarchyEntityMaxLimit (16). + */ + class ServerDeepHierarchyBenchmark : public HierarchyBenchmarkBase + { + public: + const NetEntityId RootNetEntityId = NetEntityId{ 1 }; + const NetEntityId ChildNetEntityId = NetEntityId{ 2 }; + const NetEntityId ChildOfChildNetEntityId = NetEntityId{ 3 }; + + void internalSetUp() override + { + HierarchyBenchmarkBase::internalSetUp(); + + m_root = AZStd::make_unique((1), "root", RootNetEntityId, EntityInfo::Role::Root); + CreateParent(*m_root); + + m_children = AZStd::make_unique>>(); + + EntityInfo* parent = m_root.get(); + for (int i = 0; i < 15; ++i) + { + m_children->push_back(AZStd::make_shared((i + 2), "child", ChildNetEntityId, EntityInfo::Role::Child)); + CreateChildForParent(*m_children->back(), *parent); + + m_children->back()->m_entity->FindComponent()->SetParent(parent->m_entity->GetId()); + parent = m_children->back().get(); + } + } + + void internalTearDown() override + { + m_children.reset(); + m_root.reset(); + + HierarchyBenchmarkBase::internalTearDown(); + } + + AZStd::unique_ptr m_root; + AZStd::unique_ptr>> m_children; + }; + + BENCHMARK_DEFINE_F(ServerDeepHierarchyBenchmark, RebuildHierarchy)(benchmark::State& state) + { + for ([[maybe_unused]] auto value : state) + { + ForceRebuildHierarchy(m_root->m_entity); + } + } + + BENCHMARK_REGISTER_F(ServerDeepHierarchyBenchmark, RebuildHierarchy) + ->Unit(benchmark::kMicrosecond) + ; + + // Should be roughly twice the time of @RebuildHierarchy benchmark + BENCHMARK_DEFINE_F(ServerDeepHierarchyBenchmark, RebuildHierarchyRemoveAndAddLastChild)(benchmark::State& state) + { + const AZ::EntityId parentOfLastChild = m_children->at(m_children->size() - 2)->m_entity->GetId(); + + for ([[maybe_unused]] auto value : state) + { + m_children->back()->m_entity->FindComponent()->SetParent(AZ::EntityId()); + m_children->back()->m_entity->FindComponent()->SetParent(parentOfLastChild); + } + } + + BENCHMARK_REGISTER_F(ServerDeepHierarchyBenchmark, RebuildHierarchyRemoveAndAddLastChild) + ->Unit(benchmark::kMicrosecond) + ; + + // Should be roughly twice the time of @RebuildHierarchy benchmark + BENCHMARK_DEFINE_F(ServerDeepHierarchyBenchmark, RebuildHierarchyRemoveAndAddMiddleChild)(benchmark::State& state) + { + const AZ::EntityId parentOfMiddleChild = m_children->at(4)->m_entity->GetId(); + + for ([[maybe_unused]] auto value : state) + { + m_children->at(5)->m_entity->FindComponent()->SetParent(AZ::EntityId()); + m_children->at(5)->m_entity->FindComponent()->SetParent(parentOfMiddleChild); + } + } + + BENCHMARK_REGISTER_F(ServerDeepHierarchyBenchmark, RebuildHierarchyRemoveAndAddMiddleChild) + ->Unit(benchmark::kMicrosecond) + ; + + // Should be roughly twice the time of @RebuildHierarchy benchmark + BENCHMARK_DEFINE_F(ServerDeepHierarchyBenchmark, RebuildHierarchyRemoveAndAddFirstChild)(benchmark::State& state) + { + const AZ::EntityId rootId = m_children->front()->m_entity->GetId(); + + for ([[maybe_unused]] auto value : state) + { + m_children->at(1)->m_entity->FindComponent()->SetParent(AZ::EntityId()); + m_children->at(1)->m_entity->FindComponent()->SetParent(rootId); + } + } + + BENCHMARK_REGISTER_F(ServerDeepHierarchyBenchmark, RebuildHierarchyRemoveAndAddFirstChild) + ->Unit(benchmark::kMicrosecond) + ; +} + +#endif diff --git a/Gems/Multiplayer/Code/multiplayer_tests_files.cmake b/Gems/Multiplayer/Code/multiplayer_tests_files.cmake index 93fb807b66..2114143b68 100644 --- a/Gems/Multiplayer/Code/multiplayer_tests_files.cmake +++ b/Gems/Multiplayer/Code/multiplayer_tests_files.cmake @@ -8,7 +8,9 @@ set(FILES Tests/ClientHierarchyTests.cpp + Tests/ServerHierarchyBenchmarks.cpp Tests/CommonHierarchySetup.h + Tests/CommonBenchmarkSetup.h Tests/IMultiplayerConnectionMock.h Tests/Main.cpp Tests/MockInterfaces.h