From b8389a8aa0ad1cad08eca4f34daa491fc307603f Mon Sep 17 00:00:00 2001 From: dijunkun Date: Sat, 26 Sep 2026 02:04:18 +0800 Subject: [PATCH] [feat] add connection latency to network statistics --- .../src/gui/application/gui_application.cpp | 13 ++- .../assets/localization/localization_data.h | 1 + .../file_transfer/file_transfer_manager.cpp | 2 +- .../src/gui/runtime/connection_runtime.cpp | 1 + .../src/gui/runtime/net_traffic_stats_cache.h | 108 ++++++++++++++++++ .../src/gui/runtime/peer_event_handler.cpp | 3 +- apps/desktop/src/gui/runtime/remote_session.h | 3 +- apps/desktop/src/gui/ui/stream_window.slint | 49 ++++---- apps/desktop/src/gui/ui/ui_localization.h | 1 + deps/submodules/minirtc | 2 +- 10 files changed, 149 insertions(+), 34 deletions(-) create mode 100644 apps/desktop/src/gui/runtime/net_traffic_stats_cache.h diff --git a/apps/desktop/src/gui/application/gui_application.cpp b/apps/desktop/src/gui/application/gui_application.cpp index e92f05d9..308cc6a0 100644 --- a/apps/desktop/src/gui/application/gui_application.cpp +++ b/apps/desktop/src/gui/application/gui_application.cpp @@ -2803,7 +2803,7 @@ void GuiApplication::SyncStreamWindow() { ui_->display_model->set_vector(std::move(displays)); (*ui_->stream)->set_selected_display(props->selected_display_); - const auto& net = props->net_traffic_stats_; + const auto net = props->net_traffic_stats_.Load(); (*ui_->stream)->set_stats_encryption_enabled( status == ConnectionStatus::Connected && net.srtp_active); std::vector stats_rows; @@ -2925,6 +2925,17 @@ void GuiApplication::SyncStreamWindow() { static_cast(std::lround(latency->average_ms))) + " ms" : "—")); + const bool rtt_available = props->net_traffic_stats_button_pressed_ && + status == ConnectionStatus::Connected && + net.rtt_ms >= 0; + std::string connection_latency = "—"; + if (rtt_available) { + connection_latency = + net.rtt_ms < 1 + ? "<1 ms" + : std::to_string(static_cast(std::lround(net.rtt_ms))) + " ms"; + } + (*ui_->stream)->set_stats_connection_latency(UiText(connection_latency)); } void GuiApplication::SyncStreamVideoFrame() { diff --git a/apps/desktop/src/gui/assets/localization/localization_data.h b/apps/desktop/src/gui/assets/localization/localization_data.h index a14bdf2b..54d780bb 100644 --- a/apps/desktop/src/gui/assets/localization/localization_data.h +++ b/apps/desktop/src/gui/assets/localization/localization_data.h @@ -306,6 +306,7 @@ struct TranslationRow { X(default_desktop, u8"桌面", "Desktop", u8"Рабочий стол") \ X(resolution, u8"分辨率", "Res", u8"Разрешение") \ X(video_latency, u8"视频延时", "Video delay", u8"Задержка") \ + X(connection_latency, u8"连接延时", "Connection RTT", u8"Задержка соединения") \ X(connection_mode, u8"连接模式", "Mode", u8"Режим") \ X(connection_mode_direct, u8"直连", "Direct", u8"Прямой") \ X(connection_mode_relay, u8"中继", "Relay", u8"Релейный") \ diff --git a/apps/desktop/src/gui/features/file_transfer/file_transfer_manager.cpp b/apps/desktop/src/gui/features/file_transfer/file_transfer_manager.cpp index 0ec2daee..628f4ff2 100644 --- a/apps/desktop/src/gui/features/file_transfer/file_transfer_manager.cpp +++ b/apps/desktop/src/gui/features/file_transfer/file_transfer_manager.cpp @@ -298,7 +298,7 @@ void FileTransferManager::HandleAck(const char *data, size_t size) { uint32_t rate_bps = 0; if (props) { const uint32_t bitrate = - props->net_traffic_stats_.data_outbound_stats.bitrate; + props->net_traffic_stats_.DataOutboundBitrate(); if (bitrate > 0 && state->file_sending_.load()) { rate_bps = static_cast(bitrate * 0.99f); const uint32_t current_rate = state->file_send_rate_bps_.load(); diff --git a/apps/desktop/src/gui/runtime/connection_runtime.cpp b/apps/desktop/src/gui/runtime/connection_runtime.cpp index 6a493fc2..b042131d 100644 --- a/apps/desktop/src/gui/runtime/connection_runtime.cpp +++ b/apps/desktop/src/gui/runtime/connection_runtime.cpp @@ -417,6 +417,7 @@ void GuiRuntime::WaitForSessionCleanup() { void GuiRuntime::ResetRemoteSessionResources( std::shared_ptr props) { + props->net_traffic_stats_.Reset(); { std::lock_guard lock(props->video_frame_mutex_); props->video_latency_->Reset(); diff --git a/apps/desktop/src/gui/runtime/net_traffic_stats_cache.h b/apps/desktop/src/gui/runtime/net_traffic_stats_cache.h new file mode 100644 index 00000000..616f230d --- /dev/null +++ b/apps/desktop/src/gui/runtime/net_traffic_stats_cache.h @@ -0,0 +1,108 @@ +/* + * @Author: DI JUNKUN + * @Date: 2026-09-26 + * Copyright (c) 2026 by DI JUNKUN, All Rights Reserved. + */ + +#ifndef _NET_TRAFFIC_STATS_CACHE_H_ +#define _NET_TRAFFIC_STATS_CACHE_H_ + +#include + +#include "minirtc.h" + +namespace crossdesk::gui_detail { + +// Fields are independent observations and may come from different reports. +// Relaxed accesses keep each field safe without publishing any other state. +class NetTrafficStatsCache { + static_assert(std::atomic::is_always_lock_free && + std::atomic::is_always_lock_free && + std::atomic::is_always_lock_free && + std::atomic::is_always_lock_free, + "Network statistics require lock-free scalar atomics"); + + public: + void Store(const MiniRtcNetTrafficStats& stats) { + video_inbound_stats_.Store(stats.video_inbound_stats); + video_outbound_stats_.Store(stats.video_outbound_stats); + audio_inbound_stats_.Store(stats.audio_inbound_stats); + audio_outbound_stats_.Store(stats.audio_outbound_stats); + data_inbound_stats_.Store(stats.data_inbound_stats); + data_outbound_stats_.Store(stats.data_outbound_stats); + total_inbound_stats_.Store(stats.total_inbound_stats); + total_outbound_stats_.Store(stats.total_outbound_stats); + srtp_active_.store(stats.srtp_active, std::memory_order_relaxed); + rtt_ms_.store(stats.rtt_ms, std::memory_order_relaxed); + } + + MiniRtcNetTrafficStats Load() const { + MiniRtcNetTrafficStats stats{}; + stats.video_inbound_stats = video_inbound_stats_.Load(); + stats.video_outbound_stats = video_outbound_stats_.Load(); + stats.audio_inbound_stats = audio_inbound_stats_.Load(); + stats.audio_outbound_stats = audio_outbound_stats_.Load(); + stats.data_inbound_stats = data_inbound_stats_.Load(); + stats.data_outbound_stats = data_outbound_stats_.Load(); + stats.total_inbound_stats = total_inbound_stats_.Load(); + stats.total_outbound_stats = total_outbound_stats_.Load(); + stats.srtp_active = srtp_active_.load(std::memory_order_relaxed); + stats.rtt_ms = rtt_ms_.load(std::memory_order_relaxed); + return stats; + } + + uint32_t DataOutboundBitrate() const { + return data_outbound_stats_.bitrate.load(std::memory_order_relaxed); + } + + void Reset() { Store(MiniRtcNetTrafficStats{}); } + + private: + struct InboundStats { + void Store(const MiniRtcInboundStats& stats) { + bitrate.store(stats.bitrate, std::memory_order_relaxed); + rtp_packet_count.store(stats.rtp_packet_count, std::memory_order_relaxed); + loss_rate.store(stats.loss_rate, std::memory_order_relaxed); + } + + MiniRtcInboundStats Load() const { + return {bitrate.load(std::memory_order_relaxed), + rtp_packet_count.load(std::memory_order_relaxed), + loss_rate.load(std::memory_order_relaxed)}; + } + + std::atomic bitrate{0}; + std::atomic rtp_packet_count{0}; + std::atomic loss_rate{0}; + }; + + struct OutboundStats { + void Store(const MiniRtcOutboundStats& stats) { + bitrate.store(stats.bitrate, std::memory_order_relaxed); + rtp_packet_count.store(stats.rtp_packet_count, std::memory_order_relaxed); + } + + MiniRtcOutboundStats Load() const { + return {bitrate.load(std::memory_order_relaxed), + rtp_packet_count.load(std::memory_order_relaxed)}; + } + + std::atomic bitrate{0}; + std::atomic rtp_packet_count{0}; + }; + + InboundStats video_inbound_stats_; + OutboundStats video_outbound_stats_; + InboundStats audio_inbound_stats_; + OutboundStats audio_outbound_stats_; + InboundStats data_inbound_stats_; + OutboundStats data_outbound_stats_; + InboundStats total_inbound_stats_; + OutboundStats total_outbound_stats_; + std::atomic srtp_active_{false}; + std::atomic rtt_ms_{-1}; +}; + +} // namespace crossdesk::gui_detail + +#endif diff --git a/apps/desktop/src/gui/runtime/peer_event_handler.cpp b/apps/desktop/src/gui/runtime/peer_event_handler.cpp index 297ceaa3..8079380e 100644 --- a/apps/desktop/src/gui/runtime/peer_event_handler.cpp +++ b/apps/desktop/src/gui/runtime/peer_event_handler.cpp @@ -300,6 +300,7 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status, runtime->keyboard_.ReleaseRemotePressedKeys(remote_id, "connection_closed"); props->connection_established_ = false; + props->net_traffic_stats_.Reset(); props->enable_mouse_control_ = false; runtime->ResetRemoteServiceStatus(*props); { @@ -520,7 +521,7 @@ void PeerEventHandler::OnNetStatusReport( // only display client side net status if connected to itself if (!(runtime->peer_reserved_ && !strstr(client_id, "C-"))) { - props->net_traffic_stats_ = *net_traffic_stats; + props->net_traffic_stats_.Store(*net_traffic_stats); } } } // namespace crossdesk diff --git a/apps/desktop/src/gui/runtime/remote_session.h b/apps/desktop/src/gui/runtime/remote_session.h index 90651f6b..50108bc9 100644 --- a/apps/desktop/src/gui/runtime/remote_session.h +++ b/apps/desktop/src/gui/runtime/remote_session.h @@ -25,6 +25,7 @@ #include "cursor_presentation.h" #include "minirtc.h" #include "rendering/video_latency.h" +#include "runtime/net_traffic_stats_cache.h" namespace crossdesk { class PeerEventHandler; } @@ -157,7 +158,7 @@ struct RemoteSession { std::chrono::steady_clock::time_point last_time_; // UI-thread snapshot refreshed with FPS once per second. std::optional video_latency_snapshot_; - MiniRtcNetTrafficStats net_traffic_stats_{}; + NetTrafficStatsCache net_traffic_stats_; using QueuedFile = FileTransferState::QueuedFile; using FileTransferStatus = FileTransferState::FileTransferStatus; diff --git a/apps/desktop/src/gui/ui/stream_window.slint b/apps/desktop/src/gui/ui/stream_window.slint index 0d9b2d8b..2d18fa22 100644 --- a/apps/desktop/src/gui/ui/stream_window.slint +++ b/apps/desktop/src/gui/ui/stream_window.slint @@ -57,6 +57,7 @@ export global StreamStrings { in-out property stats-loss-rate: "Loss Rate"; in-out property stats-resolution: "Res"; in-out property stats-video-latency: "Video delay"; + in-out property stats-connection-latency: "Connection RTT"; in-out property stats-connection-mode: "Mode"; in-out property stats-encryption: "Encryption"; in-out property stats-encryption-enabled: "Enabled"; @@ -220,6 +221,7 @@ export component StreamWindow inherits Window { in property stats-fps: "0"; in property stats-resolution: ""; in property stats-video-latency: "—"; + in property stats-connection-latency: "—"; in property stats-connection-mode: ""; in property stats-encryption-enabled: false; in property file-transfer-visible: false; @@ -905,18 +907,22 @@ export component StreamWindow inherits Window { private property details-right-width: right-label-width-probe.preferred-width + 6px + max(resolution-width-probe.preferred-width, + connection-latency-width-probe.preferred-width, encryption-width-probe.preferred-width + (root.stats-encryption-enabled ? 14px : 0px)); - private property details-right-x: self.width / 2; + private property details-right-x: + min(self.width / 2, self.width - self.details-right-width - 6px); left-label-width-probe := Text { visible: false; - text: "FPS\n" + StreamStrings.stats-connection-mode; + text: "FPS\n" + StreamStrings.stats-video-latency + + "\n" + StreamStrings.stats-connection-mode; font-size: ImGuiFontStyle.small; } right-label-width-probe := Text { visible: false; - text: StreamStrings.stats-resolution + "\n" + StreamStrings.stats-encryption; + text: StreamStrings.stats-resolution + "\n" + StreamStrings.stats-connection-latency + + "\n" + StreamStrings.stats-encryption; font-size: ImGuiFontStyle.small; } resolution-width-probe := Text { @@ -925,6 +931,12 @@ export component StreamWindow inherits Window { font-size: ImGuiFontStyle.body; font-weight: 600; } + connection-latency-width-probe := Text { + visible: false; + text: root.stats-connection-latency; + font-size: ImGuiFontStyle.body; + font-weight: 600; + } encryption-width-probe := Text { visible: false; text: root.stats-encryption-enabled @@ -1028,38 +1040,17 @@ export component StreamWindow inherits Window { } } - HorizontalLayout { - x: 6px; - y: root.stats-header-height + root.stats-rows.length * stats-table.row-height + 44px; - width: parent.width - 12px; - height: 21px; - spacing: 6px; - Text { - text: StreamStrings.stats-video-latency; - color: #667085; - font-size: ImGuiFontStyle.small; - vertical-alignment: center; - } - Text { - text: root.stats-video-latency; - color: #253247; - font-size: ImGuiFontStyle.body; - font-weight: 600; - vertical-alignment: center; - horizontal-stretch: 1; - overflow: elide; - } - } - for metric[index] in [ { label: "FPS", value: root.stats-fps }, { label: StreamStrings.stats-resolution, value: root.stats-resolution }, + { label: StreamStrings.stats-video-latency, value: root.stats-video-latency }, + { label: StreamStrings.stats-connection-latency, value: root.stats-connection-latency }, { label: StreamStrings.stats-connection-mode, value: root.stats-connection-mode }, { label: StreamStrings.stats-encryption, value: root.stats-encryption-enabled ? StreamStrings.stats-encryption-enabled : StreamStrings.stats-encryption-disabled }, ]: Rectangle { private property encryption-icon-width: - index == 3 && root.stats-encryption-enabled ? 14px : 0px; + index == 5 && root.stats-encryption-enabled ? 14px : 0px; x: mod(index, 2) == 1 ? stats-table.details-right-x : 6px; y: root.stats-header-height + root.stats-rows.length * stats-table.row-height + 2px + floor(index / 2) * 21px; @@ -1083,14 +1074,14 @@ export component StreamWindow inherits Window { height: parent.height; width: parent.width - self.x - parent.encryption-icon-width; text: metric.value; - color: index == 3 && root.stats-encryption-enabled ? #18794e : #253247; + color: index == 5 && root.stats-encryption-enabled ? #18794e : #253247; font-size: ImGuiFontStyle.body; font-weight: 600; horizontal-alignment: left; vertical-alignment: center; overflow: elide; } - if index == 3 && root.stats-encryption-enabled: Text { + if index == 5 && root.stats-encryption-enabled: Text { x: metric-value.x + min(metric-value.preferred-width, metric-value.width) + 4px; y: metric-value.y + (metric-value.height - self.height) / 2; width: 10px; diff --git a/apps/desktop/src/gui/ui/ui_localization.h b/apps/desktop/src/gui/ui/ui_localization.h index bf24b407..f84534ef 100644 --- a/apps/desktop/src/gui/ui/ui_localization.h +++ b/apps/desktop/src/gui/ui/ui_localization.h @@ -176,6 +176,7 @@ inline int ApplyStreamWindowStrings( strings.set_stats_loss_rate(Text(localization::loss_rate[language])); strings.set_stats_resolution(Text(localization::resolution[language])); strings.set_stats_video_latency(Text(localization::video_latency[language])); + strings.set_stats_connection_latency(Text(localization::connection_latency[language])); strings.set_stats_connection_mode( Text(localization::connection_mode[language])); strings.set_stats_encryption( diff --git a/deps/submodules/minirtc b/deps/submodules/minirtc index a25eff65..77cabf43 160000 --- a/deps/submodules/minirtc +++ b/deps/submodules/minirtc @@ -1 +1 @@ -Subproject commit a25eff65158c56c7ca5be5ef1df2b1e5d77e7dbf +Subproject commit 77cabf43cbf686691a72b6ff518ede4d345897bf