diff --git a/Gems/Multiplayer/Code/Source/Debug/MultiplayerDebugByteReporter.cpp b/Gems/Multiplayer/Code/Source/Debug/MultiplayerDebugByteReporter.cpp new file mode 100644 index 0000000000..382bc70d9b --- /dev/null +++ b/Gems/Multiplayer/Code/Source/Debug/MultiplayerDebugByteReporter.cpp @@ -0,0 +1,222 @@ +/* +* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or +* its licensors. +* +* For complete copyright and license terms please see the LICENSE at the root of this +* distribution (the "License"). All use of this software is governed by the License, +* or, if provided, by the license below or the license accompanying this file. Do not +* remove or modify any license notices. This file is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* +*/ +#include "MultiplayerDebugByteReporter.h" + +#include +#include +#include + +namespace MultiplayerDiagnostics +{ + void MultiplayerDebugByteReporter::ReportBytes(size_t byteSize) + { + m_count++; + m_totalBytes += byteSize; + m_totalBytesThisSecond += byteSize; + m_minBytes = AZStd::min(m_minBytes, byteSize); + m_maxBytes = AZStd::max(m_maxBytes, byteSize); + } + + void MultiplayerDebugByteReporter::AggregateBytes(size_t byteSize) + { + m_aggregateBytes += byteSize; + } + + void MultiplayerDebugByteReporter::ReportAggregateBytes() + { + ReportBytes(m_aggregateBytes); + m_aggregateBytes = 0; + } + + float MultiplayerDebugByteReporter::GetAverageBytes() const + { + if (m_count == 0) + { + AZ_Warning("MultiplayerDebugByteReporter", m_totalBytes == 0, "Attempted to average bytes with a zero count."); + return 0.0f; + } + + return (1.0f * m_totalBytes) / m_count; + } + + size_t MultiplayerDebugByteReporter::GetMaxBytes() const + { + return m_maxBytes; + } + + size_t MultiplayerDebugByteReporter::GetMinBytes() const + { + return m_minBytes; + } + + size_t MultiplayerDebugByteReporter::GetTotalBytes() const + { + return m_totalBytes; + } + + float MultiplayerDebugByteReporter::GetKbitsPerSecond() + { + auto now = AZStd::chrono::monotonic_clock::now(); + + // Check the amount of time elapsed and update totals if necessary. + // Time here is measured in whole seconds from the epoch, providing synchronization in + // reporting intervals across all byte reporters. + AZStd::chrono::seconds nowSeconds = AZStd::chrono::duration_cast(now.time_since_epoch()); + AZStd::chrono::seconds secondsSinceLastUpdate = nowSeconds - + AZStd::chrono::duration_cast(m_lastUpdateTime.time_since_epoch()); + if (secondsSinceLastUpdate.count()) + { + // normalize over elapsed milliseconds + const int k_millisecondsPerSecond = 1000; + auto msSinceLastUpdate = AZStd::chrono::duration_cast(now - m_lastUpdateTime); + m_totalBytesLastSecond = k_millisecondsPerSecond * (1.f * m_totalBytesThisSecond / msSinceLastUpdate.count()); + m_totalBytesThisSecond = 0; + m_lastUpdateTime = now; + } + + const float k_bitsPerByte = 8.0f; + const int k_bitsPerKilobit = 1024; + return k_bitsPerByte * m_totalBytesLastSecond / k_bitsPerKilobit; + } + + void MultiplayerDebugByteReporter::Combine(const MultiplayerDebugByteReporter& other) + { + m_count += other.m_count; + m_totalBytes += other.m_totalBytes; + m_totalBytesThisSecond += other.m_totalBytesThisSecond; + m_minBytes = AZStd::GetMin(m_minBytes, other.m_minBytes); + m_maxBytes = AZStd::GetMax(m_maxBytes, other.m_maxBytes); + } + + void MultiplayerDebugByteReporter::Reset() + { + m_count = 0; + m_totalBytes = 0; + m_totalBytesThisSecond = 0; + m_totalBytesLastSecond = 0; + m_minBytes = std::numeric_limits::max(); + m_maxBytes = 0; + m_aggregateBytes = 0; + } + + void ComponentReporter::ReportField(const char* fieldName, size_t byteSize) + { + MultiplayerDebugByteReporter::AggregateBytes(byteSize); + m_fieldReports[fieldName].ReportBytes(byteSize); + } + + void ComponentReporter::ReportFragmentEnd() + { + MultiplayerDebugByteReporter::ReportAggregateBytes(); + m_componentDirtyBytes.ReportAggregateBytes(); + } + + AZStd::vector ComponentReporter::GetFieldReports() + { + AZStd::vector copy; + for (auto field = m_fieldReports.begin(); field != m_fieldReports.end(); ++field) + { + copy.emplace_back(field->first, &field->second); + } + + auto sortByFrequency = [](const Report& a, const Report& b) + { + return a.second->GetTotalCount() > b.second->GetTotalCount(); + }; + + AZStd::sort(copy.begin(), copy.end(), sortByFrequency); + + return copy; + } + + void ComponentReporter::Combine(const ComponentReporter& other) + { + MultiplayerDebugByteReporter::Combine(other); + + for (const auto& fieldIter : other.m_fieldReports) + { + m_fieldReports[fieldIter.first].Combine(fieldIter.second); + } + + m_componentDirtyBytes.Combine(other.m_componentDirtyBytes); + } + + void EntityReporter::ReportField(AZ::u32 index, const char* componentName, + const char* fieldName, size_t byteSize) + { + if (m_currentComponentReport == nullptr) + { + std::stringstream component; + component << "[" << std::setw(2) << std::setfill('0') << static_cast(index) << "]" << " " << componentName; + m_currentComponentReport = &m_componentReports[component.str().c_str()]; + } + + m_currentComponentReport->ReportField(fieldName, byteSize); + MultiplayerDebugByteReporter::AggregateBytes(byteSize); + } + + void EntityReporter::ReportDirtyBits(AZ::u32 index, const char* componentName, size_t byteSize) + { + const char* const prefix = "MB::"; + if (strncmp(prefix, componentName, 4) == 0) + { + componentName += strlen(prefix); + } + + if (m_currentComponentReport == nullptr) + { + std::stringstream component; + component << "[" << std::setw(2) << std::setfill('0') << static_cast(index) << "]" << " " << componentName; + m_currentComponentReport = &m_componentReports[component.str().c_str()]; + } + + m_currentComponentReport->ReportDirtyBits(byteSize); + m_gdeDirtyBytes.AggregateBytes(byteSize); + } + + void EntityReporter::ReportFragmentEnd() + { + if (m_currentComponentReport) + { + m_currentComponentReport->ReportFragmentEnd(); + m_currentComponentReport = nullptr; + } + + m_gdeDirtyBytes.ReportAggregateBytes(); + MultiplayerDebugByteReporter::ReportAggregateBytes(); + } + + void EntityReporter::Combine(const EntityReporter& other) + { + MultiplayerDebugByteReporter::Combine(other); + + for (const auto& componentIter : other.m_componentReports) + { + m_componentReports[componentIter.first].Combine(componentIter.second); + } + + m_gdeDirtyBytes.Combine(other.m_gdeDirtyBytes); + } + + void EntityReporter::Reset() + { + MultiplayerDebugByteReporter::Reset(); + + m_componentReports.clear(); + m_gdeDirtyBytes.Reset(); + } + + AZStd::map& EntityReporter::GetComponentReports() + { + return m_componentReports; + } +} diff --git a/Gems/Multiplayer/Code/Source/Debug/MultiplayerDebugByteReporter.h b/Gems/Multiplayer/Code/Source/Debug/MultiplayerDebugByteReporter.h new file mode 100644 index 0000000000..7e6a04569d --- /dev/null +++ b/Gems/Multiplayer/Code/Source/Debug/MultiplayerDebugByteReporter.h @@ -0,0 +1,96 @@ +/* +* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or +* its licensors. +* +* For complete copyright and license terms please see the LICENSE at the root of this +* distribution (the "License"). All use of this software is governed by the License, +* or, if provided, by the license below or the license accompanying this file. Do not +* remove or modify any license notices. This file is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* +*/ +#pragma once + +#include +#include +#include +#include + +namespace MultiplayerDiagnostics +{ + class MultiplayerDebugByteReporter + { + public: + MultiplayerDebugByteReporter() { MultiplayerDebugByteReporter::Reset(); } + virtual ~MultiplayerDebugByteReporter() = default; + + void ReportBytes(size_t byteSize); + void AggregateBytes(size_t byteSize); + void ReportAggregateBytes(); + + float GetAverageBytes() const; + size_t GetMaxBytes() const; + size_t GetMinBytes() const; + size_t GetTotalBytes() const; + float GetKbitsPerSecond(); + + void Combine(const MultiplayerDebugByteReporter& other); + virtual void Reset(); + + size_t GetTotalCount() const { return m_count; } + + private: + size_t m_count; + size_t m_totalBytes; + size_t m_totalBytesThisSecond; + float m_totalBytesLastSecond; + size_t m_minBytes; + size_t m_maxBytes; + size_t m_aggregateBytes; + + AZStd::chrono::monotonic_clock::time_point m_lastUpdateTime; + }; + + class ComponentReporter : public MultiplayerDebugByteReporter + { + public: + ComponentReporter() = default; + + void ReportField(const char* fieldName, size_t byteSize); + void ReportDirtyBits(size_t byteSize) { m_componentDirtyBytes.AggregateBytes(byteSize); } + void ReportFragmentEnd(); + + using Report = AZStd::pair; + AZStd::vector GetFieldReports(); + AZStd::size_t GetTotalDirtyBits() const { return m_componentDirtyBytes.GetTotalBytes(); } + float GetAvgDirtyBits() const { return m_componentDirtyBytes.GetAverageBytes(); } + + void Combine(const ComponentReporter& other); + + private: + AZStd::map m_fieldReports; + MultiplayerDebugByteReporter m_componentDirtyBytes; + }; + + class EntityReporter : public MultiplayerDebugByteReporter + { + public: + EntityReporter() = default; + + void ReportField(AZ::u32 index, const char* componentName, const char* fieldName, size_t byteSize); + void ReportDirtyBits(AZ::u32 index, const char* componentName, size_t byteSize); + void ReportFragmentEnd(); + + void Combine(const EntityReporter& other); + void Reset() override; + + AZStd::map& GetComponentReports(); + AZStd::size_t GetTotalDirtyBits() const { return m_gdeDirtyBytes.GetTotalBytes(); } + float GetAvgDirtyBits() const { return m_gdeDirtyBytes.GetAverageBytes(); } + + private: + ComponentReporter* m_currentComponentReport = nullptr; + AZStd::map m_componentReports; + MultiplayerDebugByteReporter m_gdeDirtyBytes; + }; +} diff --git a/Gems/Multiplayer/Code/Source/Debug/MultiplayerDebugPerEntityReporter.cpp b/Gems/Multiplayer/Code/Source/Debug/MultiplayerDebugPerEntityReporter.cpp new file mode 100644 index 0000000000..e60b800372 --- /dev/null +++ b/Gems/Multiplayer/Code/Source/Debug/MultiplayerDebugPerEntityReporter.cpp @@ -0,0 +1,385 @@ +/* +* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or +* its licensors. +* +* For complete copyright and license terms please see the LICENSE at the root of this +* distribution (the "License"). All use of this software is governed by the License, +* or, if provided, by the license below or the license accompanying this file. Do not +* remove or modify any license notices. This file is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* +*/ +#include "MultiplayerDebugPerEntityReporter.h" +#include +#include +#include + +#if defined(IMGUI_ENABLED) +#include +#endif + +namespace MultiplayerDiagnostics +{ +#if defined(IMGUI_ENABLED) + static const ImVec4 k_ImGuiTomato = ImVec4(1.0f, 0.4f, 0.3f, 1.0f); + static const ImVec4 k_ImGuiKhaki = ImVec4(0.9f, 0.8f, 0.5f, 1.0f); + static const ImVec4 k_ImGuiCyan = ImVec4(0.5f, 1.0f, 1.0f, 1.0f); + static const ImVec4 k_ImGuiDusk = ImVec4(0.7f, 0.7f, 1.0f, 1.0f); + static const ImVec4 k_ImGuiWhite = ImVec4(1.0f, 1.0f, 1.0f, 1.0f); + + // -------------------------------------------------------------------------------------------- + template + bool ReplicatedStateTreeNode(const AZStd::string& name, Reporter& report, const ImVec4& color, int depth = 0) + { + const int defaultPadAmount = 55; + const int depthReduction = 3; + ImGui::PushStyleColor(ImGuiCol_Text, color); + + const bool expanded = ImGui::TreeNode(name.c_str(), + "%-*s %7.2f kbps %7.2f B Avg. %4zu B Max %10zu B Payload", + defaultPadAmount - depthReduction * depth, + name.c_str(), + report.GetKbitsPerSecond(), + report.GetAverageBytes(), + report.GetMaxBytes(), + report.GetTotalBytes()); + ImGui::PopStyleColor(); + return expanded; + } + + // -------------------------------------------------------------------------------------------- + void DisplayReplicatedStateReport(AZStd::map& componentReports, float kbpsWarn, float maxWarn) + { + for (auto& componentPair : componentReports) + { + ImGui::Separator(); + ComponentReporter& componentReport = componentPair.second; + + if (ReplicatedStateTreeNode(componentPair.first, componentReport, k_ImGuiCyan, 1)) + { + ImGui::Separator(); + ImGui::Columns(6, "replicated_field_columns"); + ImGui::NextColumn(); + ImGui::Text("kbps"); + ImGui::NextColumn(); + ImGui::Text("Avg. Bytes"); + ImGui::NextColumn(); + ImGui::Text("Min Bytes"); + ImGui::NextColumn(); + ImGui::Text("Max Bytes"); + ImGui::NextColumn(); + ImGui::Text("Total Bytes"); + ImGui::NextColumn(); + + auto fieldReports = componentReport.GetFieldReports(); + for (auto& fieldPair : fieldReports) + { + MultiplayerDebugByteReporter& fieldReport = *fieldPair.second; + const float kbitsLastSecond = fieldReport.GetKbitsPerSecond(); + + const ImVec4* textColor = &k_ImGuiWhite; + if (fieldReport.GetMaxBytes() > maxWarn) + { + textColor = &k_ImGuiKhaki; + } + + if (kbitsLastSecond > kbpsWarn) + { + textColor = &k_ImGuiTomato; + } + + ImGui::PushStyleColor(ImGuiCol_Text, *textColor); + + ImGui::Text("%s", fieldPair.first.c_str()); + ImGui::NextColumn(); + ImGui::Text("%.2f", kbitsLastSecond); + ImGui::NextColumn(); + ImGui::Text("%.2f", fieldReport.GetAverageBytes()); + ImGui::NextColumn(); + ImGui::Text("%zu", fieldReport.GetMinBytes()); + ImGui::NextColumn(); + ImGui::Text("%zu", fieldReport.GetMaxBytes()); + ImGui::NextColumn(); + ImGui::Text("%zu", fieldReport.GetTotalBytes()); + ImGui::NextColumn(); + + ImGui::PopStyleColor(); + } + + ImGui::Columns(1); + ImGui::TreePop(); + } + } + } +#endif + + MultiplayerDebugPerEntityReporter::MultiplayerDebugPerEntityReporter () + { + GridMate::Debug::CarrierDrillerBus::Handler::BusConnect(); + } + + // -------------------------------------------------------------------------------------------- + MultiplayerDebugPerEntityReporter::~MultiplayerDebugPerEntityReporter() + { + GridMate::Debug::ReplicaDrillerBus::Handler::BusDisconnect(); + GridMate::Debug::CarrierDrillerBus::Handler::BusDisconnect(); + } + + void MultiplayerDebugPerEntityReporter::OnReceiveReplicaBegin(GridMate::Replica*, const void*, size_t) + { + m_currentReceivingEntityReport.Reset(); + } + + void MultiplayerDebugPerEntityReporter::OnReceiveReplicaEnd(GridMate::Replica* replica) + { + m_receivingEntityReports[replica->GetDebugName()].Combine(m_currentReceivingEntityReport); + } + + void MultiplayerDebugPerEntityReporter::OnReceiveReplicaChunkEnd(GridMate::ReplicaChunkBase* chunk, AZ::u32 chunkIndex) + { + AZ_UNUSED(chunk); + AZ_UNUSED(chunkIndex); + m_currentReceivingEntityReport.ReportFragmentEnd(); + } + + void MultiplayerDebugPerEntityReporter::OnReceiveDataSet(GridMate::ReplicaChunkBase* chunk, AZ::u32 chunkIndex, + GridMate::DataSetBase* dataSet, GridMate::PeerId, GridMate::PeerId, const void*, size_t len) + { + m_currentReceivingEntityReport.ReportField(chunkIndex, chunk->GetDescriptor()->GetChunkName(), chunk->GetDescriptor()->GetDataSetName(chunk, dataSet), len); + } + + void MultiplayerDebugPerEntityReporter::OnReceiveRpc (GridMate::ReplicaChunkBase* chunk, + AZ::u32 chunkIndex, + GridMate::Internal::RpcRequest* rpc, + GridMate::PeerId from, + GridMate::PeerId to, + const void* data, + size_t len) + { + AZ_UNUSED( from ); + AZ_UNUSED( to ); + AZ_UNUSED( data ); + + m_currentReceivingEntityReport.ReportField(chunkIndex, chunk->GetDescriptor()->GetChunkName(), chunk->GetDescriptor()->GetRpcName(chunk, rpc->m_rpc), len); + } + + void MultiplayerDebugPerEntityReporter::OnSendReplicaBegin (GridMate::Replica*) + { + m_currentSendingEntityReport.Reset(); + } + + void MultiplayerDebugPerEntityReporter::OnSendReplicaEnd (GridMate::Replica* replica, const void*, size_t) + { + m_sendingEntityReports[replica->GetDebugName()].Combine(m_currentSendingEntityReport); + } + + void MultiplayerDebugPerEntityReporter::OnSendReplicaChunkEnd (GridMate::ReplicaChunkBase* chunk, + AZ::u32 chunkIndex, + const void*, + size_t) + { + AZ_UNUSED(chunk); + AZ_UNUSED(chunkIndex); + m_currentSendingEntityReport.ReportFragmentEnd(); + } + + void MultiplayerDebugPerEntityReporter::OnSendDataSet (GridMate::ReplicaChunkBase* chunk, + AZ::u32 chunkIndex, + GridMate::DataSetBase* dataSet, + GridMate::PeerId, + GridMate::PeerId, + const void*, + size_t len) + { + m_currentSendingEntityReport.ReportField(chunkIndex, chunk->GetDescriptor()->GetChunkName(), chunk->GetDescriptor()->GetDataSetName(chunk, dataSet), len); + } + + void MultiplayerDebugPerEntityReporter::OnSendRpc (GridMate::ReplicaChunkBase* chunk, + AZ::u32 chunkIndex, + GridMate::Internal::RpcRequest* rpc, + GridMate::PeerId, + GridMate::PeerId, + const void*, + size_t len) + { + m_currentSendingEntityReport.ReportField(chunkIndex, chunk->GetDescriptor()->GetChunkName(), chunk->GetDescriptor()->GetRpcName(chunk, rpc->m_rpc), len); + } + + void MultiplayerDebugPerEntityReporter::OnIncomingConnection (GridMate::Carrier*, GridMate::ConnectionID) + { + } + + void MultiplayerDebugPerEntityReporter::OnFailedToConnect (GridMate::Carrier*, + GridMate::ConnectionID, + GridMate::CarrierDisconnectReason) + { + m_lastSecondStats.clear(); + } + + void MultiplayerDebugPerEntityReporter::OnConnectionEstablished (GridMate::Carrier*, GridMate::ConnectionID) + { + } + + void MultiplayerDebugPerEntityReporter::OnDisconnect (GridMate::Carrier*, + GridMate::ConnectionID, + GridMate::CarrierDisconnectReason) + { + /* + * CarrierDrillerBus doesn't provide enough information to correctly keep track of network traffic for all peers. + * This is a work around until that is fixed to at least not over report the bandwidth amount. + */ + m_lastSecondStats.clear(); + } + + void MultiplayerDebugPerEntityReporter::OnDriverError (GridMate::Carrier*, + GridMate::ConnectionID, + const GridMate::DriverError&) + { + m_lastSecondStats.clear(); + } + + void MultiplayerDebugPerEntityReporter::OnSecurityError (GridMate::Carrier*, + GridMate::ConnectionID, + const GridMate::SecurityError&) + { + m_lastSecondStats.clear(); + } + + void MultiplayerDebugPerEntityReporter::OnUpdateStatistics (const GridMate::string& address, + const GridMate::TrafficControl::Statistics&, + const GridMate::TrafficControl::Statistics&, + const GridMate::TrafficControl::Statistics& effectiveLastSecond, + const GridMate::TrafficControl::Statistics&) + { + m_lastSecondStats[address] = effectiveLastSecond; + } + + void MultiplayerDebugPerEntityReporter::OnConnectionStateChanged (GridMate::Carrier*, + GridMate::ConnectionID, + GridMate::Carrier::ConnectionStates) + { + m_lastSecondStats.clear(); + } + + void MultiplayerDebugPerEntityReporter::UpdateTrafficStatistics() + { +#if defined(IMGUI_ENABLED) + AZ::u32 dataReceived = 0, dataSent = 0; + + for (auto& perConnection : m_lastSecondStats) + { + dataReceived += perConnection.second.m_dataReceived; + dataSent += perConnection.second.m_dataSend; + } + + if (dataReceived !=0 || dataSent != 0) + { + ImGui::Text("Total bandwidth: Sent %u kbps Received %u kbps.", dataSent * 8 / 1000, dataReceived * 8 / 1000); + } + else + { + ImGui::Text("Total bandwidth: Sent -- kbps Received -- kbps."); + } +#endif + } + + // -------------------------------------------------------------------------------------------- + void MultiplayerDebugPerEntityReporter::OnImGuiUpdate() + { +#if defined(IMGUI_ENABLED) + if (ImGui::BeginMainMenuBar()) + { + if (ImGui::BeginMenu("GridMate")) + { + if (m_showServerReportWindow) + { + if (ImGui::MenuItem("Hide Multiplayer Analytics Window")) + { + m_showServerReportWindow = false; + } + } + else if (ImGui::MenuItem("Show Multiplayer Analytics Window")) + { + m_showServerReportWindow = true; + } + + ImGui::End(); + } + + ImGui::EndMainMenuBar(); + } + + if (m_showServerReportWindow) + { + if (ImGui::Begin("Multiplayer Analytics", &m_showServerReportWindow)) + { + // General carrier stats + UpdateTrafficStatistics(); + + if (ImGui::Checkbox("Analyze network traffic", &m_isTrackingMessages)) + { + if (m_isTrackingMessages) + { + GridMate::Debug::ReplicaDrillerBus::Handler::BusConnect(); + } + else + { + GridMate::Debug::ReplicaDrillerBus::Handler::BusDisconnect(); + + m_currentReceivingEntityReport.Reset(); + m_receivingEntityReports.clear(); + + m_currentSendingEntityReport.Reset(); + m_sendingEntityReports.clear(); + } + } + + if (m_isTrackingMessages) + { + ImGui::Separator(); + + static ImGuiTextFilter filter; + filter.Draw(); + + if (ImGui::CollapsingHeader("Received replicas per type")) + { + for (auto& entityPair : m_receivingEntityReports) + { + if (!filter.PassFilter(entityPair.first.c_str())) + { + continue; + } + + ImGui::Separator(); + if (ReplicatedStateTreeNode(entityPair.first, entityPair.second, k_ImGuiDusk)) + { + DisplayReplicatedStateReport(entityPair.second.GetComponentReports(), m_replicatedStateKbpsWarn, m_replicatedStateMaxSizeWarn); + ImGui::TreePop(); + } + } + } + + if (ImGui::CollapsingHeader("Sent replicas per type")) + { + for (auto& entityPair : m_sendingEntityReports) + { + if (!filter.PassFilter(entityPair.first.c_str())) + { + continue; + } + + ImGui::Separator(); + if (ReplicatedStateTreeNode(entityPair.first, entityPair.second, k_ImGuiDusk)) + { + DisplayReplicatedStateReport(entityPair.second.GetComponentReports(), m_replicatedStateKbpsWarn, m_replicatedStateMaxSizeWarn); + ImGui::TreePop(); + } + } + } + } + } + ImGui::End(); + } +#endif + } +} diff --git a/Gems/Multiplayer/Code/Source/Debug/MultiplayerDebugPerEntityReporter.h b/Gems/Multiplayer/Code/Source/Debug/MultiplayerDebugPerEntityReporter.h new file mode 100644 index 0000000000..96cfadb14d --- /dev/null +++ b/Gems/Multiplayer/Code/Source/Debug/MultiplayerDebugPerEntityReporter.h @@ -0,0 +1,90 @@ +/* +* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or +* its licensors. +* +* For complete copyright and license terms please see the LICENSE at the root of this +* distribution (the "License"). All use of this software is governed by the License, +* or, if provided, by the license below or the license accompanying this file. Do not +* remove or modify any license notices. This file is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* +*/ +#pragma once +#include +#include "MultiplayerDebugByteReporter.h" +#include + +namespace MultiplayerDiagnostics +{ + /** + * \brief GridMate network live analysis tool via ImGui. + */ + class MultiplayerDebugPerEntityReporter + : public GridMate::Debug::ReplicaDrillerBus::Handler + , public GridMate::Debug::CarrierDrillerBus::Handler + { + public: + MultiplayerDebugPerEntityReporter(); + virtual ~MultiplayerDebugPerEntityReporter(); + + // main update loop + void OnImGuiUpdate(); + + // ReplicaDrillerBus - receive + + void OnReceiveReplicaBegin(GridMate::Replica* replica, const void* data, size_t len) override; + void OnReceiveReplicaEnd(GridMate::Replica* replica) override; + void OnReceiveReplicaChunkEnd(GridMate::ReplicaChunkBase* chunk, AZ::u32 chunkIndex) override; + void OnReceiveDataSet(GridMate::ReplicaChunkBase* chunk, AZ::u32 chunkIndex, GridMate::DataSetBase* dataSet, GridMate::PeerId from, GridMate::PeerId to, const void* data, size_t len) override; + void OnReceiveRpc(GridMate::ReplicaChunkBase* chunk, AZ::u32 chunkIndex, GridMate::Internal::RpcRequest* rpc, GridMate::PeerId from, GridMate::PeerId to, const void* data, size_t len) override; + + // ReplicaDrillerBus - sending + + void OnSendReplicaBegin(GridMate::Replica* replica) override; + void OnSendReplicaEnd(GridMate::Replica* replica, const void* data, size_t len) override; + void OnSendReplicaChunkEnd(GridMate::ReplicaChunkBase* chunk, AZ::u32 chunkIndex, const void* data, size_t len) override; + void OnSendDataSet(GridMate::ReplicaChunkBase* chunk, AZ::u32 chunkIndex, GridMate::DataSetBase* dataSet, GridMate::PeerId from, GridMate::PeerId to, const void* data, size_t len) override; + void OnSendRpc(GridMate::ReplicaChunkBase* chunk, AZ::u32 chunkIndex, GridMate::Internal::RpcRequest* rpc, GridMate::PeerId from, GridMate::PeerId to, const void* data, size_t len) override; + + // CarrierDrillerBus + void OnIncomingConnection (GridMate::Carrier* carrier, GridMate::ConnectionID id) override; + void OnFailedToConnect (GridMate::Carrier* carrier, + GridMate::ConnectionID id, + GridMate::CarrierDisconnectReason reason) override; + void OnConnectionEstablished (GridMate::Carrier* carrier, GridMate::ConnectionID id) override; + void OnDisconnect (GridMate::Carrier* carrier, + GridMate::ConnectionID id, + GridMate::CarrierDisconnectReason reason) override; + void OnDriverError (GridMate::Carrier* carrier, + GridMate::ConnectionID id, + const GridMate::DriverError& error) override; + void OnSecurityError (GridMate::Carrier* carrier, + GridMate::ConnectionID id, + const GridMate::SecurityError& error) override; + void OnUpdateStatistics (const GridMate::string& address, + const GridMate::TrafficControl::Statistics& lastSecond, + const GridMate::TrafficControl::Statistics& lifeTime, + const GridMate::TrafficControl::Statistics& effectiveLastSecond, + const GridMate::TrafficControl::Statistics& effectiveLifeTime) override; + void OnConnectionStateChanged (GridMate::Carrier* carrier, + GridMate::ConnectionID id, + GridMate::Carrier::ConnectionStates newState) override; + + private: + + bool m_showServerReportWindow = false; + bool m_isTrackingMessages = false; + + AZStd::map m_sendingEntityReports{}; + EntityReporter m_currentSendingEntityReport; + + AZStd::map m_receivingEntityReports{}; + EntityReporter m_currentReceivingEntityReport; + + float m_replicatedStateKbpsWarn = 10.f; + float m_replicatedStateMaxSizeWarn = 30.f; + + void UpdateTrafficStatistics(); + AZStd::map m_lastSecondStats; + }; +} diff --git a/Gems/Multiplayer/Code/Source/Debug/MultiplayerDebugSystemComponent.cpp b/Gems/Multiplayer/Code/Source/Debug/MultiplayerDebugSystemComponent.cpp index 1611a3213d..b97fa053b2 100644 --- a/Gems/Multiplayer/Code/Source/Debug/MultiplayerDebugSystemComponent.cpp +++ b/Gems/Multiplayer/Code/Source/Debug/MultiplayerDebugSystemComponent.cpp @@ -53,6 +53,11 @@ namespace Multiplayer #endif } + void MultiplayerDebugSystemComponent::OnImGuiInitialize() + { + m_reporter = AZStd::make_unique(); + } + #ifdef IMGUI_ENABLED void MultiplayerDebugSystemComponent::OnImGuiMainMenuUpdate() { @@ -318,6 +323,12 @@ namespace Multiplayer ImGui::End(); } } + + + if (m_reporter) + { + m_reporter->OnImGuiUpdate(); + } } #endif } diff --git a/Gems/Multiplayer/Code/Source/Debug/MultiplayerDebugSystemComponent.h b/Gems/Multiplayer/Code/Source/Debug/MultiplayerDebugSystemComponent.h index 77daaf9b7d..3dbbd06d6f 100644 --- a/Gems/Multiplayer/Code/Source/Debug/MultiplayerDebugSystemComponent.h +++ b/Gems/Multiplayer/Code/Source/Debug/MultiplayerDebugSystemComponent.h @@ -9,6 +9,7 @@ #pragma once #include +#include #ifdef IMGUI_ENABLED # include @@ -42,6 +43,7 @@ namespace Multiplayer #ifdef IMGUI_ENABLED //! ImGui::ImGuiUpdateListenerBus overrides //! @{ + void OnImGuiInitialize() override; void OnImGuiMainMenuUpdate() override; void OnImGuiUpdate() override; //! @} @@ -49,5 +51,7 @@ namespace Multiplayer private: bool m_displayNetworkingStats = false; bool m_displayMultiplayerStats = false; + + AZStd::unique_ptr m_reporter; }; } diff --git a/Gems/Multiplayer/Code/multiplayer_debug_files.cmake b/Gems/Multiplayer/Code/multiplayer_debug_files.cmake index 62bd632e7e..37a1c91640 100644 --- a/Gems/Multiplayer/Code/multiplayer_debug_files.cmake +++ b/Gems/Multiplayer/Code/multiplayer_debug_files.cmake @@ -7,6 +7,10 @@ # set(FILES + Source/Debug/MultiplayerDebugByteReporter.cpp + Source/Debug/MultiplayerDebugByteReporter.h + Source/Debug/MultiplayerDebugPerEntityReporter.cpp + Source/Debug/MultiplayerDebugPerEntityReporter.h Source/Debug/MultiplayerDebugModule.cpp Source/Debug/MultiplayerDebugModule.h Source/Debug/MultiplayerDebugSystemComponent.cpp