From 4aab0f6189869bb2cb166ef0ddadb129ea20017c Mon Sep 17 00:00:00 2001 From: Petr Ohlidal Date: Fri, 16 Jun 2023 10:58:41 +0200 Subject: [PATCH] Added net performance graphs to MPClientList UI. This is a spinoff from https://github.com/RigsOfRods/rigs-of-rods/pull/3055 Requires updated rorserver. New features: - Added send timestamp to `RoRnet::Header` - all packets now track their time shift between remote client and local client. Note that the existing truck time shift tracking made by Ulteq is unchanged for now. - Added queue timestamp to `RoRnet::Header` - all packets now track the delay between creating the packet and transmitting it to server. - Added recv queue timestamp to `NetRecvPacket` - all packets now track local delay between receiving packet and processing it. - Added 3 new graphs to the MPClientList UI: 1. timeshift (should be stable), 2. local recv lag, 3. remote send lag. Motivation: - to smoothen out character movement the same way truck movement is smoothed. That requires tracking the timeshift, and Ulteq's truck smoothing code is very much tied to `Actor` and `ActorManager`. - To clean up network timers - there are 3 separate timers (`Character`, `Actor`, `ActorManager` - the pattern is not clear). - To better understand and tweak network traffic. - I like diagnostic views. Time spent: 6.5hours, not counting rorserver. --- source/main/CMakeLists.txt | 1 + .../gui/panels/GUI_MultiplayerClientList.cpp | 45 ++++++++- .../gui/panels/GUI_MultiplayerClientList.h | 3 + source/main/network/NetStats.cpp | 98 +++++++++++++++++++ source/main/network/NetStats.h | 63 ++++++++++++ source/main/network/Network.cpp | 52 +++++++--- source/main/network/Network.h | 32 ++++-- source/main/network/RoRnet.h | 25 +++-- source/main/physics/ActorManager.cpp | 6 +- 9 files changed, 293 insertions(+), 32 deletions(-) create mode 100644 source/main/network/NetStats.cpp create mode 100644 source/main/network/NetStats.h diff --git a/source/main/CMakeLists.txt b/source/main/CMakeLists.txt index 0a5cb219fe..8508193ac8 100644 --- a/source/main/CMakeLists.txt +++ b/source/main/CMakeLists.txt @@ -141,6 +141,7 @@ set(SOURCE_FILES gui/panels/GUI_VehicleDescription.{h,cpp} gui/panels/GUI_VehicleButtons.{h,cpp} network/DiscordRpc.{h,cpp} + network/NetStats.{h,cpp} network/Network.{h,cpp} network/OutGauge.{h,cpp} network/RoRnet.h diff --git a/source/main/gui/panels/GUI_MultiplayerClientList.cpp b/source/main/gui/panels/GUI_MultiplayerClientList.cpp index 6cd1d9fa48..7ab2bc844c 100644 --- a/source/main/gui/panels/GUI_MultiplayerClientList.cpp +++ b/source/main/gui/panels/GUI_MultiplayerClientList.cpp @@ -65,7 +65,7 @@ void MpClientList::Draw() ImGuiWindowFlags flags = ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoScrollbar; - const float content_width = 200.f; + const float content_width = 300.f; ImGui::SetNextWindowContentWidth(content_width); ImGui::SetNextWindowPos(ImVec2( ImGui::GetIO().DisplaySize.x - (content_width + (2*ImGui::GetStyle().WindowPadding.x) + theme.screen_edge_padding.x), @@ -85,6 +85,8 @@ void MpClientList::Draw() const RoRnet::UserInfo& local_user = m_users[0]; // See `UpdateClients()` for (RoRnet::UserInfo const& user: m_users) { + ImGui::PushID(user.uniqueid); + // Icon sizes: flag(16x11), auth(16x16), up(16x16), down(16x16) bool hovered = false; Ogre::TexturePtr flag_tex; @@ -113,8 +115,18 @@ void MpClientList::Draw() default:; } } + + // net graphs + NetClientStats stats; + App::GetNetwork()->GetUserStats(user.uniqueid, /*out*/stats); + + hovered |= this->DrawPlotSmall("##Time", "", stats.client_time_offset, 40.f); + // Always invoke to keep usernames aligned hovered |= this->DrawIcon(down_tex, ImVec2(8.f, ImGui::GetTextLineHeight())); + hovered |= this->DrawPlotSmall("##Send", "", stats.remote_queue_delay, 25.f); + hovered |= this->DrawPlotSmall("##Recv", "", stats.local_queue_delay, 25.f); + // Always invoke to keep usernames aligned hovered |= this->DrawIcon(up_tex, ImVec2(8.f, ImGui::GetTextLineHeight())); // Auth icon @@ -214,10 +226,18 @@ void MpClientList::Draw() case 2: ImGui::Text("%s", _LC("MultiplayerClientList", "No Trucks loaded")); break; default:; // never happens } + + // Stats + ImGui::Separator(); + this->DrawPlotBig("Time shift (ms)", stats.client_time_offset); + this->DrawPlotBig("Send lag (ms)", stats.remote_queue_delay); + this->DrawPlotBig("Recv lag (ms)", stats.local_queue_delay); } ImGui::EndTooltip(); } + + ImGui::PopID(); //user.uniqueid } if (App::GetNetwork()->GetNetQuality() != 0) @@ -266,3 +286,26 @@ void MpClientList::CacheIcons() m_icons_cached = true; } + +bool MpClientList::DrawPlotSmall(const char* label, const char* overlay_text, NetGraphData& graphdata, float width) +{ + graphdata.ImPlotLines(label, overlay_text, ImVec2(width, ImGui::GetTextLineHeight())); + ImGui::SameLine(); + return ImGui::IsItemHovered(); +} + +void MpClientList::DrawPlotBig(const char* label, NetGraphData& graphdata) +{ + const float PLOT_WIDTH = 125.f; + + // The plot widget + const ImVec2 cursor_start = ImGui::GetCursorPos(); + graphdata.ImPlotLines(label, nullptr, ImVec2(PLOT_WIDTH, 35.f)); + const ImVec2 cursor_end = ImGui::GetCursorPos(); + + // The plot scale text + const ImVec2 scale_pos = cursor_start + ImVec2(PLOT_WIDTH + 5.f, ImGui::GetTextLineHeight() + 2.f); + ImGui::SetCursorPos(scale_pos); + ImGui::TextDisabled("%5.2f - %5.2f", graphdata.plotline_smoothmin, graphdata.plotline_smoothmax); + ImGui::SetCursorPos(cursor_end); +} diff --git a/source/main/gui/panels/GUI_MultiplayerClientList.h b/source/main/gui/panels/GUI_MultiplayerClientList.h index 34ed98490d..09354c022b 100644 --- a/source/main/gui/panels/GUI_MultiplayerClientList.h +++ b/source/main/gui/panels/GUI_MultiplayerClientList.h @@ -28,6 +28,7 @@ #pragma once #include "RoRnet.h" +#include "Network.h" #include #include @@ -43,6 +44,8 @@ class MpClientList private: bool DrawIcon(Ogre::TexturePtr tex, ImVec2 reference_box); // Returns true if hovered + bool DrawPlotSmall(const char* label, const char* overlay_text, NetGraphData& graphdata, float width); + void DrawPlotBig(const char* label, NetGraphData& graphdata); void CacheIcons(); std::vector m_users; // only updated on demand to reduce mutex locking and vector allocating overhead. diff --git a/source/main/network/NetStats.cpp b/source/main/network/NetStats.cpp new file mode 100644 index 0000000000..0ef9474dbe --- /dev/null +++ b/source/main/network/NetStats.cpp @@ -0,0 +1,98 @@ +/* + This source file is part of Rigs of Rods + Copyright 2005-2012 Pierre-Michel Ricordel + Copyright 2007-2012 Thomas Fischer + Copyright 2013-2023 Petr Ohlidal + + For more information, see http://www.rigsofrods.org/ + + Rigs of Rods is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License version 3, as + published by the Free Software Foundation. + + Rigs of Rods is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Rigs of Rods. If not, see . +*/ + +#ifdef USE_SOCKETW + +#include "Network.h" + +#include "Application.h" +#include "ChatSystem.h" +#include "Console.h" +#include "ErrorUtils.h" +#include "GameContext.h" +#include "GUIManager.h" +#include "GUI_TopMenubar.h" +#include "Language.h" +#include "RoRVersion.h" +#include "ScriptEngine.h" +#include "Utils.h" + +#include +#include + +#include +#include +#include + +using namespace RoR; + +NetGraphData::NetGraphData(size_t size) +{ + plotline.resize(size, RoRnet::NetTime32_t(0)); +} + +void NetGraphData::AddSample(RoRnet::NetTime32_t sample) +{ + plotline.erase(plotline.begin()); + plotline.push_back(sample); + const auto minmax = std::minmax_element(plotline.begin(), plotline.end()); + plotline_min = *minmax.first; + plotline_max = *minmax.second; + + // smooth scale + const float SMOOTH_UP = 0.4f; + const float SMOOTH_DOWN = 0.05f; + + const float fmin = static_cast(*minmax.first); + if (fmin < plotline_smoothmin) + plotline_smoothmin = fmin * SMOOTH_UP + plotline_smoothmin * (1.f - SMOOTH_UP); + else + plotline_smoothmin = fmin * SMOOTH_DOWN + plotline_smoothmin * (1.f - SMOOTH_DOWN); + + const float fmax = static_cast(*minmax.second); + if (fmax > plotline_smoothmax) + plotline_smoothmax = fmax * SMOOTH_UP + plotline_smoothmax * (1.f - SMOOTH_UP); + else + plotline_smoothmax = fmax * SMOOTH_DOWN + plotline_smoothmax * (1.f - SMOOTH_DOWN); +} + +// static +float NetGraphData::ImPlotGetSample(void* data, int idx) +{ + NetGraphPlotline* plotline = static_cast(data); + return static_cast(plotline->at(idx)); +} + +void NetGraphData::ImPlotLines(const char* label, const char* overlay_text, ImVec2 size) +{ + ImGui::PlotLines( + label, + NetGraphData::ImPlotGetSample, + static_cast(&plotline), + static_cast(plotline.size()), + /*values_offset:*/0, + overlay_text, + plotline_smoothmin, + plotline_smoothmax, + size); +} + +#endif // USE_SOCKETW diff --git a/source/main/network/NetStats.h b/source/main/network/NetStats.h new file mode 100644 index 0000000000..82c6819d30 --- /dev/null +++ b/source/main/network/NetStats.h @@ -0,0 +1,63 @@ +/* + This source file is part of Rigs of Rods + Copyright 2005-2012 Pierre-Michel Ricordel + Copyright 2007-2012 Thomas Fischer + Copyright 2013-2023 Petr Ohlidal + + For more information, see http://www.rigsofrods.org/ + + Rigs of Rods is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License version 3, as + published by the Free Software Foundation. + + Rigs of Rods is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Rigs of Rods. If not, see . +*/ + +#pragma once + +#ifdef USE_SOCKETW + +#include "Application.h" +#include "OgreImGui.h" +#include "RoRnet.h" + +namespace RoR { + +/// @addtogroup Network +/// @{ + +typedef std::vector NetGraphPlotline; + +struct NetGraphData +{ + NetGraphPlotline plotline; + RoRnet::NetTime32_t plotline_max = 0; + RoRnet::NetTime32_t plotline_min = 0; + float plotline_smoothmax = 0.f; + float plotline_smoothmin = 0.f; + + NetGraphData(size_t size); + + void AddSample(RoRnet::NetTime32_t sample); + static float ImPlotGetSample(void* data, int idx); + void ImPlotLines(const char* label, const char* overlay_text, ImVec2 size); +}; + +struct NetClientStats +{ + NetGraphData remote_queue_delay = NetGraphData(20); //!< How long it takes remote client to transmit outgoing packet. + NetGraphData local_queue_delay = NetGraphData(20); //!< How long it takes us to process received packet. + NetGraphData client_time_offset = NetGraphData(40); //!< Difference between client timers; we monitor it's stability. +}; + +/// @} //addtogroup Network + +} // namespace RoR + +#endif // USE_SOCKETW diff --git a/source/main/network/Network.cpp b/source/main/network/Network.cpp index 25d603bfa8..42e3ec01f1 100644 --- a/source/main/network/Network.cpp +++ b/source/main/network/Network.cpp @@ -2,7 +2,7 @@ This source file is part of Rigs of Rods Copyright 2005-2012 Pierre-Michel Ricordel Copyright 2007-2012 Thomas Fischer - Copyright 2013-2016 Petr Ohlidal + Copyright 2013-2023 Petr Ohlidal For more information, see http://www.rigsofrods.org/ @@ -167,7 +167,8 @@ void Network::QueueStreamData(RoRnet::Header &header, char *buffer, size_t buffe memcpy(packet.buffer, buffer, std::min(buffer_len, size_t(RORNET_MAX_MESSAGE_LENGTH))); std::lock_guard lock(m_recv_packetqueue_mutex); - m_recv_packet_buffer.push_back(packet); + packet.recv_queue_time = m_recv_packet_timer.getMilliseconds(); + m_recv_packet_buffer.push_back(packet); } int Network::ReceiveMessage(RoRnet::Header *head, char* content, int bufferlen) @@ -228,6 +229,7 @@ void Network::SendThread() break; } packet = m_send_packet_buffer.front(); + packet.GetHeader()->source_send_time = m_send_packet_timer.getMilliseconds(); m_send_packet_buffer.pop_front(); } SendMessageRaw(packet.buffer, packet.size); @@ -552,6 +554,8 @@ bool Network::ConnectThread() m_shutdown = false; LOG("[RoR|Networking] Connect(): Creating Send/Recv threads"); + m_send_packet_timer.reset(); + m_recv_packet_timer.reset(); m_send_thread = std::thread(&Network::SendThread, this); m_recv_thread = std::thread(&Network::RecvThread, this); PushNetMessage(MSG_NET_CONNECT_SUCCESS, ""); @@ -614,18 +618,15 @@ void Network::AddPacket(int streamid, int type, int len, const char *content) } NetSendPacket packet; - memset(&packet, 0, sizeof(NetSendPacket)); - - char *buffer = (char*)(packet.buffer); - - RoRnet::Header *head = (RoRnet::Header *)buffer; - head->command = type; - head->source = m_uid; - head->size = len; - head->streamid = streamid; + + // fill in the header + packet.GetHeader()->command = type; + packet.GetHeader()->source = m_uid; + packet.GetHeader()->size = len; + packet.GetHeader()->streamid = streamid; // then copy the contents - char *bufferContent = (char *)(buffer + sizeof(RoRnet::Header)); + char *bufferContent = (char *)(packet.buffer + sizeof(RoRnet::Header)); memcpy(bufferContent, content, len); // record the packet size @@ -633,6 +634,7 @@ void Network::AddPacket(int streamid, int type, int len, const char *content) { // Lock scope std::lock_guard lock(m_send_packetqueue_mutex); + if (type == MSG2_STREAM_DATA_DISCARDABLE) { if (m_send_packet_buffer.size() > m_packet_buffer_size) @@ -651,6 +653,7 @@ void Network::AddPacket(int streamid, int type, int len, const char *content) } } //DebugPacket("send", head, buffer); + packet.GetHeader()->source_queue_time = m_send_packet_timer.getMilliseconds(); m_send_packet_buffer.push_back(packet); } @@ -674,6 +677,19 @@ std::vector Network::GetIncomingStreamData() std::lock_guard lock(m_recv_packetqueue_mutex); std::vector buf_copy = m_recv_packet_buffer; m_recv_packet_buffer.clear(); + + // update stats (graphs) + // TODO: this should be done _after_ the packets get pruned in `HandleActorStreamData()` + // - that logic should be moved here so that characters benefit too. + for (const NetRecvPacket& packet: buf_copy) + { + const RoRnet::NetTime32_t cur_time = m_recv_packet_timer.getMilliseconds(); + NetClientStats& stats = m_recv_client_stats[packet.header.source]; + stats.remote_queue_delay.AddSample(packet.header.source_send_time - packet.header.source_queue_time); + stats.local_queue_delay.AddSample(cur_time - packet.recv_queue_time); + stats.client_time_offset.AddSample(cur_time - packet.header.source_queue_time); + } + return buf_copy; } @@ -770,6 +786,18 @@ bool Network::FindUserInfo(std::string const& username, RoRnet::UserInfo &result return false; } +bool Network::GetUserStats(int uid, NetClientStats& result) +{ + std::lock_guard lock(m_recv_packetqueue_mutex); + const auto it = m_recv_client_stats.find(uid); + const bool found = it != m_recv_client_stats.end(); + if (found) + { + result = it->second; + } + return found; +} + void Network::BroadcastChatMsg(const char* msg) { AddPacket(m_stream_id, RoRnet::MSG2_UTF8_CHAT, (int)std::strlen(msg), msg); diff --git a/source/main/network/Network.h b/source/main/network/Network.h index 70c8eed361..356321208c 100644 --- a/source/main/network/Network.h +++ b/source/main/network/Network.h @@ -2,7 +2,7 @@ This source file is part of Rigs of Rods Copyright 2005-2012 Pierre-Michel Ricordel Copyright 2007-2012 Thomas Fischer - Copyright 2013-2016 Petr Ohlidal + Copyright 2013-2023 Petr Ohlidal For more information, see http://www.rigsofrods.org/ @@ -24,6 +24,7 @@ #ifdef USE_SOCKETW #include "Application.h" +#include "NetStats.h" #include "RoRnet.h" #include @@ -80,13 +81,16 @@ struct NetCharacterMsgAttach struct NetSendPacket { - char buffer[RORNET_MAX_MESSAGE_LENGTH]; - int size; + char buffer[RORNET_MAX_MESSAGE_LENGTH] = {}; + int size = 0; + + RoRnet::Header* const GetHeader() { return reinterpret_cast(buffer); } }; struct NetRecvPacket { RoRnet::Header header; + RoRnet::NetTime32_t recv_queue_time; char buffer[RORNET_MAX_MESSAGE_LENGTH]; }; @@ -119,6 +123,7 @@ class Network bool GetDisconnectedUserInfo(int uid, RoRnet::UserInfo &result); bool GetAnyUserInfo(int uid, RoRnet::UserInfo &result); //!< Also considers local client bool FindUserInfo(std::string const& username, RoRnet::UserInfo &result); + bool GetUserStats(int uid, NetClientStats& result); Ogre::ColourValue GetPlayerColor(int color_num); void BroadcastChatMsg(const char* msg); @@ -168,13 +173,22 @@ class Network std::mutex m_users_mutex; std::mutex m_userdata_mutex; - std::mutex m_recv_packetqueue_mutex; - std::mutex m_send_packetqueue_mutex; - - std::condition_variable m_send_packet_available_cv; - + + /// @name Threadsafe send packet queue + /// { + std::mutex m_send_packetqueue_mutex; + std::condition_variable m_send_packet_available_cv; + std::deque m_send_packet_buffer; + Ogre::Timer m_send_packet_timer; //!< In sync with `m_recv_packet_timer` + /// } + + /// @name Threadsafe recv packet queue + stats + /// { + std::mutex m_recv_packetqueue_mutex; std::vector m_recv_packet_buffer; - std::deque m_send_packet_buffer; + Ogre::Timer m_recv_packet_timer; //!< In sync with `m_send_packet_timer` + std::unordered_map m_recv_client_stats; + /// } }; /// @} //addtogroup Network diff --git a/source/main/network/RoRnet.h b/source/main/network/RoRnet.h index be9467a570..f998cb53f3 100644 --- a/source/main/network/RoRnet.h +++ b/source/main/network/RoRnet.h @@ -1,8 +1,9 @@ /* This file is part of Rigs of Rods - Copyright 2007 Pierre-Michel Ricordel - Copyright 2014+ Petr Ohlidal & contributors. + Copyright 2007 Pierre-Michel Ricordel + Copyright 2014-2017 Ulteq + Copyright 2020-2023 Petr Ohlidal Rigs of Rods is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -34,6 +35,8 @@ namespace RoRnet { #define RORNET_VERSION "RoRnet_2.44" +typedef uint32_t NetTime32_t; //!< Milliseconds + enum MessageType { MSG2_HELLO = 1025, //!< client sends its version as first message @@ -67,7 +70,10 @@ enum MessageType MSG2_STREAM_DATA_DISCARDABLE, //!< stream data that is allowed to be discarded // Legacy values (RoRnet_2.38 and earlier) - MSG2_WRONG_VER_LEGACY = 1003 //!< Wrong version + MSG2_WRONG_VER_LEGACY = 1003, //!< Wrong version + + // Special values + MSG2_INVALID = 0 //!< Not to be transmitted }; enum UserAuth @@ -131,10 +137,13 @@ enum Lightmask struct Header //!< Common header for every packet { - uint32_t command; //!< the command of this packet: MSG2_* - int32_t source; //!< source of this command: 0 = server - uint32_t streamid; //!< streamid for this command - uint32_t size; //!< size of the attached data block + uint32_t command; //!< the command of this packet: MSG2_* + int32_t source; //!< client who sent this command: 0 = server + NetTime32_t source_queue_time; //!< client time when queuing packet for sending + NetTime32_t source_send_time; //!< client time when actually sending the packet + uint32_t streamid; //!< streamid for this command + uint32_t size; //!< size of the attached data block + }; struct StreamRegister //!< Sent from the client to server and vice versa, to broadcast a new stream @@ -158,7 +167,7 @@ struct ActorStreamRegister //!< Must preserve mem. layout of RoRnet::Str // RoRnet::StreamRegister: Data buffer (128B) int32_t bufferSize; //!< initial stream status int32_t time; //!< initial time stamp - char skin[60]; //!< skin + char skin[60]; //!< skin char sectionconfig[60]; //!< section configuration }; diff --git a/source/main/physics/ActorManager.cpp b/source/main/physics/ActorManager.cpp index 7ebba6da20..ba6af3e89a 100644 --- a/source/main/physics/ActorManager.cpp +++ b/source/main/physics/ActorManager.cpp @@ -352,8 +352,10 @@ void ActorManager::HandleActorStreamData(std::vector packet_ // Compress data stream by eliminating all but the last update from every consecutive group of stream data updates auto it = std::unique(packet_buffer.rbegin(), packet_buffer.rend(), [](const RoR::NetRecvPacket& a, const RoR::NetRecvPacket& b) - { return !memcmp(&a.header, &b.header, sizeof(RoRnet::Header)) && - a.header.command == RoRnet::MSG2_STREAM_DATA; }); + { return a.header.command == RoRnet::MSG2_STREAM_DATA + && a.header.source == b.header.source + && a.header.streamid == b.header.streamid; + }); packet_buffer.erase(packet_buffer.begin(), it.base()); for (auto& packet : packet_buffer) {