From c77369bb8ad9adb74b4a0a9e63979d29eef340a4 Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Mon, 23 Jun 2025 20:15:21 +0200 Subject: [PATCH 1/4] chore: added latency tooltips on workspaces --- App/ViewModels/AgentViewModel.cs | 126 ++++++++++++++++++++---- App/ViewModels/TrayWindowViewModel.cs | 35 +++++-- App/Views/Pages/TrayWindowMainPage.xaml | 9 +- Vpn.Proto/vpn.proto | 24 ++++- 4 files changed, 160 insertions(+), 34 deletions(-) diff --git a/App/ViewModels/AgentViewModel.cs b/App/ViewModels/AgentViewModel.cs index cd5907b..0b41fc1 100644 --- a/App/ViewModels/AgentViewModel.cs +++ b/App/ViewModels/AgentViewModel.cs @@ -1,11 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.ComponentModel; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Windows.ApplicationModel.DataTransfer; using Coder.Desktop.App.Services; using Coder.Desktop.App.Utils; using Coder.Desktop.CoderSdk; @@ -18,15 +10,24 @@ using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Controls.Primitives; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.Xml.Linq; +using Windows.ApplicationModel.DataTransfer; namespace Coder.Desktop.App.ViewModels; public interface IAgentViewModelFactory { public AgentViewModel Create(IAgentExpanderHost expanderHost, Uuid id, string fullyQualifiedDomainName, - string hostnameSuffix, - AgentConnectionStatus connectionStatus, Uri dashboardBaseUrl, string? workspaceName); - + string hostnameSuffix, AgentConnectionStatus connectionStatus, Uri dashboardBaseUrl, + string? workspaceName, bool? didP2p, string? preferredDerp, TimeSpan? latency, TimeSpan? preferredDerpLatency, DateTime? lastHandshake); public AgentViewModel CreateDummy(IAgentExpanderHost expanderHost, Uuid id, string hostnameSuffix, AgentConnectionStatus connectionStatus, Uri dashboardBaseUrl, string workspaceName); @@ -40,8 +41,11 @@ public class AgentViewModelFactory( { public AgentViewModel Create(IAgentExpanderHost expanderHost, Uuid id, string fullyQualifiedDomainName, string hostnameSuffix, - AgentConnectionStatus connectionStatus, Uri dashboardBaseUrl, string? workspaceName) + AgentConnectionStatus connectionStatus, Uri dashboardBaseUrl, + string? workspaceName, bool? didP2p, string? preferredDerp, TimeSpan? latency, TimeSpan? preferredDerpLatency, + DateTime? lastHandshake) { + System.Diagnostics.Debug.WriteLine($"Creating agent: {didP2p} {preferredDerp} {latency} {lastHandshake}"); return new AgentViewModel(childLogger, coderApiClientFactory, credentialManager, agentAppViewModelFactory, expanderHost, id) { @@ -51,6 +55,11 @@ public AgentViewModel Create(IAgentExpanderHost expanderHost, Uuid id, string fu ConnectionStatus = connectionStatus, DashboardBaseUrl = dashboardBaseUrl, WorkspaceName = workspaceName, + DidP2p = didP2p, + PreferredDerp = preferredDerp, + Latency = latency, + PreferredDerpLatency = preferredDerpLatency, + LastHandshake = lastHandshake, }; } @@ -73,10 +82,10 @@ public AgentViewModel CreateDummy(IAgentExpanderHost expanderHost, Uuid id, public enum AgentConnectionStatus { - Green, - Yellow, - Red, - Gray, + Healthy, + Unhealthy, + NoRecentHandshake, + Offline, } public partial class AgentViewModel : ObservableObject, IModelUpdateable @@ -182,6 +191,75 @@ public string FullyQualifiedDomainName [NotifyPropertyChangedFor(nameof(ExpandAppsMessage))] public partial bool AppFetchErrored { get; set; } = false; + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(ConnectionTooltip))] + public partial bool? DidP2p { get; set; } = false; + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(ConnectionTooltip))] + public partial string? PreferredDerp { get; set; } = null; + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(ConnectionTooltip))] + public partial TimeSpan? Latency { get; set; } = null; + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(ConnectionTooltip))] + public partial TimeSpan? PreferredDerpLatency { get; set; } = null; + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(ConnectionTooltip))] + public partial DateTime? LastHandshake { get; set; } = null; + + public string ConnectionTooltip { get + { + var description = new StringBuilder(); + + if (DidP2p != null && DidP2p.Value && Latency != null) + { + description.Append($""" + You're connected peer-to-peer. + + You ↔ {Latency.Value.Milliseconds} ms ↔ {WorkspaceName} + """ + ); + } + else if (PreferredDerpLatency != null) + { + description.Append($""" + You're connected through a DERP relay. + We'll switch over to peer-to-peer when available. + + Total latency: {PreferredDerpLatency.Value.Milliseconds} ms + """ + ); + + if (PreferredDerp != null && Latency != null) + { + description.Append($"\nYou ↔ {PreferredDerp}: {PreferredDerpLatency.Value.Milliseconds} ms"); + + var derpToWorkspaceEstimatedLatency = Latency - PreferredDerpLatency; + + // Guard against negative values if the two readings were taken at different times + if (derpToWorkspaceEstimatedLatency > TimeSpan.Zero) + { + description.Append($"\n{PreferredDerp} ms ↔ {WorkspaceName}: {derpToWorkspaceEstimatedLatency.Value.Milliseconds} ms"); + } + } + } + if (LastHandshake != null) + description.Append($"\n\nLast handshake: {LastHandshake?.ToString() ?? "Unknown"}"); + + var tooltip = description.ToString().TrimEnd('\n', ' '); + + if (string.IsNullOrEmpty(tooltip)) + return "No connection information available."; + + return tooltip; + } + } + + // We only show 6 apps max, which fills the entire width of the tray // window. public IEnumerable VisibleApps => Apps.Count > MaxAppsPerRow ? Apps.Take(MaxAppsPerRow) : Apps; @@ -192,7 +270,7 @@ public string? ExpandAppsMessage { get { - if (ConnectionStatus == AgentConnectionStatus.Gray) + if (ConnectionStatus == AgentConnectionStatus.Offline) return "Your workspace is offline."; if (FetchingApps && Apps.Count == 0) // Don't show this message if we have any apps already. When @@ -285,6 +363,16 @@ public bool TryApplyChanges(AgentViewModel model) DashboardBaseUrl = model.DashboardBaseUrl; if (WorkspaceName != model.WorkspaceName) WorkspaceName = model.WorkspaceName; + if (DidP2p != model.DidP2p) + DidP2p = model.DidP2p; + if (PreferredDerp != model.PreferredDerp) + PreferredDerp = model.PreferredDerp; + if (Latency != model.Latency) + Latency = model.Latency; + if (PreferredDerpLatency != model.PreferredDerpLatency) + PreferredDerpLatency = model.PreferredDerpLatency; + if (LastHandshake != model.LastHandshake) + LastHandshake = model.LastHandshake; // Apps are not set externally. @@ -307,7 +395,7 @@ public void SetExpanded(bool expanded) partial void OnConnectionStatusChanged(AgentConnectionStatus oldValue, AgentConnectionStatus newValue) { - if (IsExpanded && newValue is not AgentConnectionStatus.Gray) FetchApps(); + if (IsExpanded && newValue is not AgentConnectionStatus.Offline) FetchApps(); } private void FetchApps() @@ -316,7 +404,7 @@ private void FetchApps() FetchingApps = true; // If the workspace is off, then there's no agent and there's no apps. - if (ConnectionStatus == AgentConnectionStatus.Gray) + if (ConnectionStatus == AgentConnectionStatus.Offline) { FetchingApps = false; Apps.Clear(); diff --git a/App/ViewModels/TrayWindowViewModel.cs b/App/ViewModels/TrayWindowViewModel.cs index 8540453..b43eaa8 100644 --- a/App/ViewModels/TrayWindowViewModel.cs +++ b/App/ViewModels/TrayWindowViewModel.cs @@ -3,6 +3,7 @@ using System.Collections.ObjectModel; using System.ComponentModel; using System.Linq; +using System.Security.Principal; using System.Threading.Tasks; using Coder.Desktop.App.Models; using Coder.Desktop.App.Services; @@ -29,6 +30,7 @@ public partial class TrayWindowViewModel : ObservableObject, IAgentExpanderHost { private const int MaxAgents = 5; private const string DefaultDashboardUrl = "https://coder.com"; + private readonly TimeSpan HealthyPingThreshold = TimeSpan.FromMilliseconds(150); private readonly IServiceProvider _services; private readonly IRpcController _rpcController; @@ -222,12 +224,26 @@ private void UpdateFromRpcModel(RpcModel rpcModel) if (string.IsNullOrWhiteSpace(fqdn)) continue; + var lastHandshakeAgo = DateTime.UtcNow.Subtract(agent.LastHandshake.ToDateTime()); - var connectionStatus = lastHandshakeAgo < TimeSpan.FromMinutes(5) - ? AgentConnectionStatus.Green - : AgentConnectionStatus.Yellow; + + // For compatibility with older deployments, we assume that if the + // last ping is null, the agent is healthy. + var isLatencyAcceptable = agent.LastPing != null ? agent.LastPing.Latency.ToTimeSpan() < HealthyPingThreshold : true; + var connectionStatus = AgentConnectionStatus.Healthy; + if (lastHandshakeAgo > TimeSpan.FromMinutes(5)) + { + connectionStatus = AgentConnectionStatus.NoRecentHandshake; + } + else if (!isLatencyAcceptable) + { + connectionStatus = AgentConnectionStatus.Unhealthy; + } + + workspacesWithAgents.Add(agent.WorkspaceId); var workspace = rpcModel.Workspaces.FirstOrDefault(w => w.Id == agent.WorkspaceId); + System.Diagnostics.Debug.WriteLine($"Agent {uuid} LastHandshakeAgo: {lastHandshakeAgo} ConnectionStatus: {connectionStatus} FQDN: {fqdn} Last ping: {agent.LastPing} Last handshake: {agent.LastHandshake}"); agents.Add(_agentViewModelFactory.Create( this, @@ -236,7 +252,12 @@ private void UpdateFromRpcModel(RpcModel rpcModel) _hostnameSuffixGetter.GetCachedSuffix(), connectionStatus, credentialModel.CoderUrl, - workspace?.Name)); + workspace?.Name, + agent.LastPing?.DidP2P, + agent.LastPing?.PreferredDerp, + agent.LastPing?.Latency?.ToTimeSpan(), + agent.LastPing?.PreferredDerpLatency?.ToTimeSpan(), + agent.LastHandshake?.ToDateTime())); } // For every stopped workspace that doesn't have any agents, add a @@ -253,7 +274,7 @@ private void UpdateFromRpcModel(RpcModel rpcModel) // conflict with any agent IDs. uuid, _hostnameSuffixGetter.GetCachedSuffix(), - AgentConnectionStatus.Gray, + AgentConnectionStatus.Offline, credentialModel.CoderUrl, workspace.Name)); } @@ -268,7 +289,7 @@ private void UpdateFromRpcModel(RpcModel rpcModel) if (Agents.Count < MaxAgents) ShowAllAgents = false; - var firstOnlineAgent = agents.FirstOrDefault(a => a.ConnectionStatus != AgentConnectionStatus.Gray); + var firstOnlineAgent = agents.FirstOrDefault(a => a.ConnectionStatus != AgentConnectionStatus.Offline); if (firstOnlineAgent is null) _hasExpandedAgent = false; if (!_hasExpandedAgent && firstOnlineAgent is not null) @@ -433,7 +454,7 @@ private static bool ShouldShowDummy(Workspace workspace) case Workspace.Types.Status.Stopping: case Workspace.Types.Status.Stopped: return true; - // TODO: should we include and show a different color than Gray for workspaces that are + // TODO: should we include and show a different color than Offline for workspaces that are // failed, canceled or deleting? default: return false; diff --git a/App/Views/Pages/TrayWindowMainPage.xaml b/App/Views/Pages/TrayWindowMainPage.xaml index 9f27fb1..896327e 100644 --- a/App/Views/Pages/TrayWindowMainPage.xaml +++ b/App/Views/Pages/TrayWindowMainPage.xaml @@ -137,22 +137,22 @@ x:Key="StatusColor" SelectedKey="{x:Bind Path=ConnectionStatus, Mode=OneWay}"> - + - + - + - + @@ -189,6 +189,7 @@ HorizontalAlignment="Right" VerticalAlignment="Center" Height="14" Width="14" + ToolTipService.ToolTip="{x:Bind ConnectionTooltip, Mode=OneWay}" Margin="0,1,0,0"> Date: Tue, 24 Jun 2025 14:17:57 +0200 Subject: [PATCH 2/4] PR fixes --- App/ViewModels/AgentViewModel.cs | 33 +++++++++++++++++++-------- App/ViewModels/TrayWindowViewModel.cs | 6 ++--- 2 files changed, 26 insertions(+), 13 deletions(-) diff --git a/App/ViewModels/AgentViewModel.cs b/App/ViewModels/AgentViewModel.cs index 0b41fc1..c1ea0fc 100644 --- a/App/ViewModels/AgentViewModel.cs +++ b/App/ViewModels/AgentViewModel.cs @@ -45,7 +45,6 @@ public AgentViewModel Create(IAgentExpanderHost expanderHost, Uuid id, string fu string? workspaceName, bool? didP2p, string? preferredDerp, TimeSpan? latency, TimeSpan? preferredDerpLatency, DateTime? lastHandshake) { - System.Diagnostics.Debug.WriteLine($"Creating agent: {didP2p} {preferredDerp} {latency} {lastHandshake}"); return new AgentViewModel(childLogger, coderApiClientFactory, credentialManager, agentAppViewModelFactory, expanderHost, id) { @@ -88,6 +87,19 @@ public enum AgentConnectionStatus Offline, } +public static class AgentConnectionStatusExtensions +{ + public static string ToDisplayString(this AgentConnectionStatus status) => + status switch + { + AgentConnectionStatus.Healthy => "Healthy", + AgentConnectionStatus.Unhealthy => "High latency", + AgentConnectionStatus.NoRecentHandshake => "No recent handshake", + AgentConnectionStatus.Offline => "Offline", + _ => status.ToString() + }; +} + public partial class AgentViewModel : ObservableObject, IModelUpdateable { private const string DefaultDashboardUrl = "https://coder.com"; @@ -169,6 +181,7 @@ public string FullyQualifiedDomainName [ObservableProperty] [NotifyPropertyChangedFor(nameof(ShowExpandAppsMessage))] [NotifyPropertyChangedFor(nameof(ExpandAppsMessage))] + [NotifyPropertyChangedFor(nameof(ConnectionTooltip))] public required partial AgentConnectionStatus ConnectionStatus { get; set; } [ObservableProperty] @@ -214,11 +227,12 @@ public string FullyQualifiedDomainName public string ConnectionTooltip { get { var description = new StringBuilder(); + var highLatencyWarning = ConnectionStatus == AgentConnectionStatus.Unhealthy ? $"({AgentConnectionStatus.Unhealthy.ToDisplayString()})" : ""; if (DidP2p != null && DidP2p.Value && Latency != null) { description.Append($""" - You're connected peer-to-peer. + You're connected peer-to-peer. {highLatencyWarning} You ↔ {Latency.Value.Milliseconds} ms ↔ {WorkspaceName} """ @@ -227,7 +241,7 @@ public string ConnectionTooltip { get else if (PreferredDerpLatency != null) { description.Append($""" - You're connected through a DERP relay. + You're connected through a DERP relay. {highLatencyWarning} We'll switch over to peer-to-peer when available. Total latency: {PreferredDerpLatency.Value.Milliseconds} ms @@ -247,15 +261,14 @@ public string ConnectionTooltip { get } } } + else + { + description.Append(ConnectionStatus.ToDisplayString()); + } if (LastHandshake != null) - description.Append($"\n\nLast handshake: {LastHandshake?.ToString() ?? "Unknown"}"); - - var tooltip = description.ToString().TrimEnd('\n', ' '); - - if (string.IsNullOrEmpty(tooltip)) - return "No connection information available."; + description.Append($"\n\nLast handshake: {LastHandshake?.ToString()}"); - return tooltip; + return description.ToString().TrimEnd('\n', ' '); ; } } diff --git a/App/ViewModels/TrayWindowViewModel.cs b/App/ViewModels/TrayWindowViewModel.cs index b43eaa8..3b1e0be 100644 --- a/App/ViewModels/TrayWindowViewModel.cs +++ b/App/ViewModels/TrayWindowViewModel.cs @@ -224,12 +224,13 @@ private void UpdateFromRpcModel(RpcModel rpcModel) if (string.IsNullOrWhiteSpace(fqdn)) continue; - +#pragma warning disable CS8602 // Protobuf will always set this value, so we can safely dereference them. var lastHandshakeAgo = DateTime.UtcNow.Subtract(agent.LastHandshake.ToDateTime()); +#pragma warning restore CS8602 // For compatibility with older deployments, we assume that if the // last ping is null, the agent is healthy. - var isLatencyAcceptable = agent.LastPing != null ? agent.LastPing.Latency.ToTimeSpan() < HealthyPingThreshold : true; + var isLatencyAcceptable = agent.LastPing == null || agent.LastPing.Latency.ToTimeSpan() < HealthyPingThreshold; var connectionStatus = AgentConnectionStatus.Healthy; if (lastHandshakeAgo > TimeSpan.FromMinutes(5)) { @@ -243,7 +244,6 @@ private void UpdateFromRpcModel(RpcModel rpcModel) workspacesWithAgents.Add(agent.WorkspaceId); var workspace = rpcModel.Workspaces.FirstOrDefault(w => w.Id == agent.WorkspaceId); - System.Diagnostics.Debug.WriteLine($"Agent {uuid} LastHandshakeAgo: {lastHandshakeAgo} ConnectionStatus: {connectionStatus} FQDN: {fqdn} Last ping: {agent.LastPing} Last handshake: {agent.LastHandshake}"); agents.Add(_agentViewModelFactory.Create( this, From d1b2c7d59b7e770d334f0ea449ad13bb8468980f Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Tue, 24 Jun 2025 14:46:36 +0200 Subject: [PATCH 3/4] added connecting state and improved the logic on tooltip generation --- App/ViewModels/AgentViewModel.cs | 8 +++++-- App/ViewModels/TrayWindowViewModel.cs | 30 ++++++++++++++----------- App/Views/Pages/TrayWindowMainPage.xaml | 5 +++++ 3 files changed, 28 insertions(+), 15 deletions(-) diff --git a/App/ViewModels/AgentViewModel.cs b/App/ViewModels/AgentViewModel.cs index c1ea0fc..0e50b06 100644 --- a/App/ViewModels/AgentViewModel.cs +++ b/App/ViewModels/AgentViewModel.cs @@ -82,9 +82,10 @@ public AgentViewModel CreateDummy(IAgentExpanderHost expanderHost, Uuid id, public enum AgentConnectionStatus { Healthy, + Connecting, Unhealthy, NoRecentHandshake, - Offline, + Offline } public static class AgentConnectionStatusExtensions @@ -93,6 +94,7 @@ public static string ToDisplayString(this AgentConnectionStatus status) => status switch { AgentConnectionStatus.Healthy => "Healthy", + AgentConnectionStatus.Connecting => "Connecting", AgentConnectionStatus.Unhealthy => "High latency", AgentConnectionStatus.NoRecentHandshake => "No recent handshake", AgentConnectionStatus.Offline => "Offline", @@ -224,7 +226,9 @@ public string FullyQualifiedDomainName [NotifyPropertyChangedFor(nameof(ConnectionTooltip))] public partial DateTime? LastHandshake { get; set; } = null; - public string ConnectionTooltip { get + public string ConnectionTooltip + { + get { var description = new StringBuilder(); var highLatencyWarning = ConnectionStatus == AgentConnectionStatus.Unhealthy ? $"({AgentConnectionStatus.Unhealthy.ToDisplayString()})" : ""; diff --git a/App/ViewModels/TrayWindowViewModel.cs b/App/ViewModels/TrayWindowViewModel.cs index 3b1e0be..f57947d 100644 --- a/App/ViewModels/TrayWindowViewModel.cs +++ b/App/ViewModels/TrayWindowViewModel.cs @@ -224,24 +224,28 @@ private void UpdateFromRpcModel(RpcModel rpcModel) if (string.IsNullOrWhiteSpace(fqdn)) continue; -#pragma warning disable CS8602 // Protobuf will always set this value, so we can safely dereference them. - var lastHandshakeAgo = DateTime.UtcNow.Subtract(agent.LastHandshake.ToDateTime()); -#pragma warning restore CS8602 - - // For compatibility with older deployments, we assume that if the - // last ping is null, the agent is healthy. - var isLatencyAcceptable = agent.LastPing == null || agent.LastPing.Latency.ToTimeSpan() < HealthyPingThreshold; var connectionStatus = AgentConnectionStatus.Healthy; - if (lastHandshakeAgo > TimeSpan.FromMinutes(5)) + + if (agent.LastHandshake != null && agent.LastHandshake.ToDateTime() != default && agent.LastHandshake.ToDateTime() < DateTime.UtcNow) { - connectionStatus = AgentConnectionStatus.NoRecentHandshake; + // For compatibility with older deployments, we assume that if the + // last ping is null, the agent is healthy. + var isLatencyAcceptable = agent.LastPing == null || agent.LastPing.Latency.ToTimeSpan() < HealthyPingThreshold; + + var lastHandshakeAgo = DateTime.UtcNow.Subtract(agent.LastHandshake.ToDateTime()); + + if (lastHandshakeAgo > TimeSpan.FromMinutes(5)) + connectionStatus = AgentConnectionStatus.NoRecentHandshake; + else if (!isLatencyAcceptable) + connectionStatus = AgentConnectionStatus.Unhealthy; } - else if (!isLatencyAcceptable) + else { - connectionStatus = AgentConnectionStatus.Unhealthy; + // If the last handshake is not correct (null, default or in the future), + // we assume the agent is connecting (yellow status icon). + connectionStatus = AgentConnectionStatus.Connecting; } - workspacesWithAgents.Add(agent.WorkspaceId); var workspace = rpcModel.Workspaces.FirstOrDefault(w => w.Id == agent.WorkspaceId); @@ -257,7 +261,7 @@ private void UpdateFromRpcModel(RpcModel rpcModel) agent.LastPing?.PreferredDerp, agent.LastPing?.Latency?.ToTimeSpan(), agent.LastPing?.PreferredDerpLatency?.ToTimeSpan(), - agent.LastHandshake?.ToDateTime())); + agent.LastHandshake != null && agent.LastHandshake.ToDateTime() != default ? agent.LastHandshake?.ToDateTime() : null)); } // For every stopped workspace that doesn't have any agents, add a diff --git a/App/Views/Pages/TrayWindowMainPage.xaml b/App/Views/Pages/TrayWindowMainPage.xaml index 17d2eef..3cc21de 100644 --- a/App/Views/Pages/TrayWindowMainPage.xaml +++ b/App/Views/Pages/TrayWindowMainPage.xaml @@ -152,6 +152,11 @@ + + + + + From 8fbbff0531065a2c469e323e953d4cac002a5668 Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Tue, 24 Jun 2025 14:50:23 +0200 Subject: [PATCH 4/4] PR review --- App/ViewModels/AgentViewModel.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/App/ViewModels/AgentViewModel.cs b/App/ViewModels/AgentViewModel.cs index 0e50b06..0cf2651 100644 --- a/App/ViewModels/AgentViewModel.cs +++ b/App/ViewModels/AgentViewModel.cs @@ -242,17 +242,17 @@ public string ConnectionTooltip """ ); } - else if (PreferredDerpLatency != null) + else if (Latency != null) { description.Append($""" You're connected through a DERP relay. {highLatencyWarning} We'll switch over to peer-to-peer when available. - Total latency: {PreferredDerpLatency.Value.Milliseconds} ms + Total latency: {Latency.Value.Milliseconds} ms """ ); - if (PreferredDerp != null && Latency != null) + if (PreferredDerpLatency != null) { description.Append($"\nYou ↔ {PreferredDerp}: {PreferredDerpLatency.Value.Milliseconds} ms"); pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy