Skip to content

Commit c77369b

Browse files
committed
chore: added latency tooltips on workspaces
1 parent 56003ed commit c77369b

File tree

4 files changed

+160
-34
lines changed

4 files changed

+160
-34
lines changed

App/ViewModels/AgentViewModel.cs

Lines changed: 107 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,3 @@
1-
using System;
2-
using System.Collections.Generic;
3-
using System.Collections.ObjectModel;
4-
using System.ComponentModel;
5-
using System.Linq;
6-
using System.Threading;
7-
using System.Threading.Tasks;
8-
using Windows.ApplicationModel.DataTransfer;
91
using Coder.Desktop.App.Services;
102
using Coder.Desktop.App.Utils;
113
using Coder.Desktop.CoderSdk;
@@ -18,15 +10,24 @@
1810
using Microsoft.UI.Xaml;
1911
using Microsoft.UI.Xaml.Controls;
2012
using Microsoft.UI.Xaml.Controls.Primitives;
13+
using System;
14+
using System.Collections.Generic;
15+
using System.Collections.ObjectModel;
16+
using System.ComponentModel;
17+
using System.Linq;
18+
using System.Text;
19+
using System.Threading;
20+
using System.Threading.Tasks;
21+
using System.Xml.Linq;
22+
using Windows.ApplicationModel.DataTransfer;
2123

2224
namespace Coder.Desktop.App.ViewModels;
2325

2426
public interface IAgentViewModelFactory
2527
{
2628
public AgentViewModel Create(IAgentExpanderHost expanderHost, Uuid id, string fullyQualifiedDomainName,
27-
string hostnameSuffix,
28-
AgentConnectionStatus connectionStatus, Uri dashboardBaseUrl, string? workspaceName);
29-
29+
string hostnameSuffix, AgentConnectionStatus connectionStatus, Uri dashboardBaseUrl,
30+
string? workspaceName, bool? didP2p, string? preferredDerp, TimeSpan? latency, TimeSpan? preferredDerpLatency, DateTime? lastHandshake);
3031
public AgentViewModel CreateDummy(IAgentExpanderHost expanderHost, Uuid id,
3132
string hostnameSuffix,
3233
AgentConnectionStatus connectionStatus, Uri dashboardBaseUrl, string workspaceName);
@@ -40,8 +41,11 @@ public class AgentViewModelFactory(
4041
{
4142
public AgentViewModel Create(IAgentExpanderHost expanderHost, Uuid id, string fullyQualifiedDomainName,
4243
string hostnameSuffix,
43-
AgentConnectionStatus connectionStatus, Uri dashboardBaseUrl, string? workspaceName)
44+
AgentConnectionStatus connectionStatus, Uri dashboardBaseUrl,
45+
string? workspaceName, bool? didP2p, string? preferredDerp, TimeSpan? latency, TimeSpan? preferredDerpLatency,
46+
DateTime? lastHandshake)
4447
{
48+
System.Diagnostics.Debug.WriteLine($"Creating agent: {didP2p} {preferredDerp} {latency} {lastHandshake}");
4549
return new AgentViewModel(childLogger, coderApiClientFactory, credentialManager, agentAppViewModelFactory,
4650
expanderHost, id)
4751
{
@@ -51,6 +55,11 @@ public AgentViewModel Create(IAgentExpanderHost expanderHost, Uuid id, string fu
5155
ConnectionStatus = connectionStatus,
5256
DashboardBaseUrl = dashboardBaseUrl,
5357
WorkspaceName = workspaceName,
58+
DidP2p = didP2p,
59+
PreferredDerp = preferredDerp,
60+
Latency = latency,
61+
PreferredDerpLatency = preferredDerpLatency,
62+
LastHandshake = lastHandshake,
5463
};
5564
}
5665

@@ -73,10 +82,10 @@ public AgentViewModel CreateDummy(IAgentExpanderHost expanderHost, Uuid id,
7382

7483
public enum AgentConnectionStatus
7584
{
76-
Green,
77-
Yellow,
78-
Red,
79-
Gray,
85+
Healthy,
86+
Unhealthy,
87+
NoRecentHandshake,
88+
Offline,
8089
}
8190

8291
public partial class AgentViewModel : ObservableObject, IModelUpdateable<AgentViewModel>
@@ -182,6 +191,75 @@ public string FullyQualifiedDomainName
182191
[NotifyPropertyChangedFor(nameof(ExpandAppsMessage))]
183192
public partial bool AppFetchErrored { get; set; } = false;
184193

194+
[ObservableProperty]
195+
[NotifyPropertyChangedFor(nameof(ConnectionTooltip))]
196+
public partial bool? DidP2p { get; set; } = false;
197+
198+
[ObservableProperty]
199+
[NotifyPropertyChangedFor(nameof(ConnectionTooltip))]
200+
public partial string? PreferredDerp { get; set; } = null;
201+
202+
[ObservableProperty]
203+
[NotifyPropertyChangedFor(nameof(ConnectionTooltip))]
204+
public partial TimeSpan? Latency { get; set; } = null;
205+
206+
[ObservableProperty]
207+
[NotifyPropertyChangedFor(nameof(ConnectionTooltip))]
208+
public partial TimeSpan? PreferredDerpLatency { get; set; } = null;
209+
210+
[ObservableProperty]
211+
[NotifyPropertyChangedFor(nameof(ConnectionTooltip))]
212+
public partial DateTime? LastHandshake { get; set; } = null;
213+
214+
public string ConnectionTooltip { get
215+
{
216+
var description = new StringBuilder();
217+
218+
if (DidP2p != null && DidP2p.Value && Latency != null)
219+
{
220+
description.Append($"""
221+
You're connected peer-to-peer.
222+
223+
You ↔ {Latency.Value.Milliseconds} ms ↔ {WorkspaceName}
224+
"""
225+
);
226+
}
227+
else if (PreferredDerpLatency != null)
228+
{
229+
description.Append($"""
230+
You're connected through a DERP relay.
231+
We'll switch over to peer-to-peer when available.
232+
233+
Total latency: {PreferredDerpLatency.Value.Milliseconds} ms
234+
"""
235+
);
236+
237+
if (PreferredDerp != null && Latency != null)
238+
{
239+
description.Append($"\nYou ↔ {PreferredDerp}: {PreferredDerpLatency.Value.Milliseconds} ms");
240+
241+
var derpToWorkspaceEstimatedLatency = Latency - PreferredDerpLatency;
242+
243+
// Guard against negative values if the two readings were taken at different times
244+
if (derpToWorkspaceEstimatedLatency > TimeSpan.Zero)
245+
{
246+
description.Append($"\n{PreferredDerp} ms ↔ {WorkspaceName}: {derpToWorkspaceEstimatedLatency.Value.Milliseconds} ms");
247+
}
248+
}
249+
}
250+
if (LastHandshake != null)
251+
description.Append($"\n\nLast handshake: {LastHandshake?.ToString() ?? "Unknown"}");
252+
253+
var tooltip = description.ToString().TrimEnd('\n', ' ');
254+
255+
if (string.IsNullOrEmpty(tooltip))
256+
return "No connection information available.";
257+
258+
return tooltip;
259+
}
260+
}
261+
262+
185263
// We only show 6 apps max, which fills the entire width of the tray
186264
// window.
187265
public IEnumerable<AgentAppViewModel> VisibleApps => Apps.Count > MaxAppsPerRow ? Apps.Take(MaxAppsPerRow) : Apps;
@@ -192,7 +270,7 @@ public string? ExpandAppsMessage
192270
{
193271
get
194272
{
195-
if (ConnectionStatus == AgentConnectionStatus.Gray)
273+
if (ConnectionStatus == AgentConnectionStatus.Offline)
196274
return "Your workspace is offline.";
197275
if (FetchingApps && Apps.Count == 0)
198276
// Don't show this message if we have any apps already. When
@@ -285,6 +363,16 @@ public bool TryApplyChanges(AgentViewModel model)
285363
DashboardBaseUrl = model.DashboardBaseUrl;
286364
if (WorkspaceName != model.WorkspaceName)
287365
WorkspaceName = model.WorkspaceName;
366+
if (DidP2p != model.DidP2p)
367+
DidP2p = model.DidP2p;
368+
if (PreferredDerp != model.PreferredDerp)
369+
PreferredDerp = model.PreferredDerp;
370+
if (Latency != model.Latency)
371+
Latency = model.Latency;
372+
if (PreferredDerpLatency != model.PreferredDerpLatency)
373+
PreferredDerpLatency = model.PreferredDerpLatency;
374+
if (LastHandshake != model.LastHandshake)
375+
LastHandshake = model.LastHandshake;
288376

289377
// Apps are not set externally.
290378

@@ -307,7 +395,7 @@ public void SetExpanded(bool expanded)
307395

308396
partial void OnConnectionStatusChanged(AgentConnectionStatus oldValue, AgentConnectionStatus newValue)
309397
{
310-
if (IsExpanded && newValue is not AgentConnectionStatus.Gray) FetchApps();
398+
if (IsExpanded && newValue is not AgentConnectionStatus.Offline) FetchApps();
311399
}
312400

313401
private void FetchApps()
@@ -316,7 +404,7 @@ private void FetchApps()
316404
FetchingApps = true;
317405

318406
// If the workspace is off, then there's no agent and there's no apps.
319-
if (ConnectionStatus == AgentConnectionStatus.Gray)
407+
if (ConnectionStatus == AgentConnectionStatus.Offline)
320408
{
321409
FetchingApps = false;
322410
Apps.Clear();

App/ViewModels/TrayWindowViewModel.cs

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using System.Collections.ObjectModel;
44
using System.ComponentModel;
55
using System.Linq;
6+
using System.Security.Principal;
67
using System.Threading.Tasks;
78
using Coder.Desktop.App.Models;
89
using Coder.Desktop.App.Services;
@@ -29,6 +30,7 @@ public partial class TrayWindowViewModel : ObservableObject, IAgentExpanderHost
2930
{
3031
private const int MaxAgents = 5;
3132
private const string DefaultDashboardUrl = "https://coder.com";
33+
private readonly TimeSpan HealthyPingThreshold = TimeSpan.FromMilliseconds(150);
3234

3335
private readonly IServiceProvider _services;
3436
private readonly IRpcController _rpcController;
@@ -222,12 +224,26 @@ private void UpdateFromRpcModel(RpcModel rpcModel)
222224
if (string.IsNullOrWhiteSpace(fqdn))
223225
continue;
224226

227+
225228
var lastHandshakeAgo = DateTime.UtcNow.Subtract(agent.LastHandshake.ToDateTime());
226-
var connectionStatus = lastHandshakeAgo < TimeSpan.FromMinutes(5)
227-
? AgentConnectionStatus.Green
228-
: AgentConnectionStatus.Yellow;
229+
230+
// For compatibility with older deployments, we assume that if the
231+
// last ping is null, the agent is healthy.
232+
var isLatencyAcceptable = agent.LastPing != null ? agent.LastPing.Latency.ToTimeSpan() < HealthyPingThreshold : true;
233+
var connectionStatus = AgentConnectionStatus.Healthy;
234+
if (lastHandshakeAgo > TimeSpan.FromMinutes(5))
235+
{
236+
connectionStatus = AgentConnectionStatus.NoRecentHandshake;
237+
}
238+
else if (!isLatencyAcceptable)
239+
{
240+
connectionStatus = AgentConnectionStatus.Unhealthy;
241+
}
242+
243+
229244
workspacesWithAgents.Add(agent.WorkspaceId);
230245
var workspace = rpcModel.Workspaces.FirstOrDefault(w => w.Id == agent.WorkspaceId);
246+
System.Diagnostics.Debug.WriteLine($"Agent {uuid} LastHandshakeAgo: {lastHandshakeAgo} ConnectionStatus: {connectionStatus} FQDN: {fqdn} Last ping: {agent.LastPing} Last handshake: {agent.LastHandshake}");
231247

232248
agents.Add(_agentViewModelFactory.Create(
233249
this,
@@ -236,7 +252,12 @@ private void UpdateFromRpcModel(RpcModel rpcModel)
236252
_hostnameSuffixGetter.GetCachedSuffix(),
237253
connectionStatus,
238254
credentialModel.CoderUrl,
239-
workspace?.Name));
255+
workspace?.Name,
256+
agent.LastPing?.DidP2P,
257+
agent.LastPing?.PreferredDerp,
258+
agent.LastPing?.Latency?.ToTimeSpan(),
259+
agent.LastPing?.PreferredDerpLatency?.ToTimeSpan(),
260+
agent.LastHandshake?.ToDateTime()));
240261
}
241262

242263
// For every stopped workspace that doesn't have any agents, add a
@@ -253,7 +274,7 @@ private void UpdateFromRpcModel(RpcModel rpcModel)
253274
// conflict with any agent IDs.
254275
uuid,
255276
_hostnameSuffixGetter.GetCachedSuffix(),
256-
AgentConnectionStatus.Gray,
277+
AgentConnectionStatus.Offline,
257278
credentialModel.CoderUrl,
258279
workspace.Name));
259280
}
@@ -268,7 +289,7 @@ private void UpdateFromRpcModel(RpcModel rpcModel)
268289

269290
if (Agents.Count < MaxAgents) ShowAllAgents = false;
270291

271-
var firstOnlineAgent = agents.FirstOrDefault(a => a.ConnectionStatus != AgentConnectionStatus.Gray);
292+
var firstOnlineAgent = agents.FirstOrDefault(a => a.ConnectionStatus != AgentConnectionStatus.Offline);
272293
if (firstOnlineAgent is null)
273294
_hasExpandedAgent = false;
274295
if (!_hasExpandedAgent && firstOnlineAgent is not null)
@@ -433,7 +454,7 @@ private static bool ShouldShowDummy(Workspace workspace)
433454
case Workspace.Types.Status.Stopping:
434455
case Workspace.Types.Status.Stopped:
435456
return true;
436-
// TODO: should we include and show a different color than Gray for workspaces that are
457+
// TODO: should we include and show a different color than Offline for workspaces that are
437458
// failed, canceled or deleting?
438459
default:
439460
return false;

App/Views/Pages/TrayWindowMainPage.xaml

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -137,22 +137,22 @@
137137
x:Key="StatusColor"
138138
SelectedKey="{x:Bind Path=ConnectionStatus, Mode=OneWay}">
139139

140-
<converters:StringToBrushSelectorItem>
140+
<converters:StringToBrushSelectorItem Key="Offline">
141141
<converters:StringToBrushSelectorItem.Value>
142142
<SolidColorBrush Color="#8e8e93" />
143143
</converters:StringToBrushSelectorItem.Value>
144144
</converters:StringToBrushSelectorItem>
145-
<converters:StringToBrushSelectorItem Key="Red">
145+
<converters:StringToBrushSelectorItem Key="NoRecentHandshake">
146146
<converters:StringToBrushSelectorItem.Value>
147147
<SolidColorBrush Color="#ff3b30" />
148148
</converters:StringToBrushSelectorItem.Value>
149149
</converters:StringToBrushSelectorItem>
150-
<converters:StringToBrushSelectorItem Key="Yellow">
150+
<converters:StringToBrushSelectorItem Key="Unhealthy">
151151
<converters:StringToBrushSelectorItem.Value>
152152
<SolidColorBrush Color="#ffcc01" />
153153
</converters:StringToBrushSelectorItem.Value>
154154
</converters:StringToBrushSelectorItem>
155-
<converters:StringToBrushSelectorItem Key="Green">
155+
<converters:StringToBrushSelectorItem Key="Healthy">
156156
<converters:StringToBrushSelectorItem.Value>
157157
<SolidColorBrush Color="#34c759" />
158158
</converters:StringToBrushSelectorItem.Value>
@@ -189,6 +189,7 @@
189189
HorizontalAlignment="Right"
190190
VerticalAlignment="Center"
191191
Height="14" Width="14"
192+
ToolTipService.ToolTip="{x:Bind ConnectionTooltip, Mode=OneWay}"
192193
Margin="0,1,0,0">
193194

194195
<Ellipse

Vpn.Proto/vpn.proto

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ option go_package = "github.com/coder/coder/v2/vpn";
33
option csharp_namespace = "Coder.Desktop.Vpn.Proto";
44

55
import "google/protobuf/timestamp.proto";
6+
import "google/protobuf/duration.proto";
67

78
package vpn;
89

@@ -48,10 +49,10 @@ message TunnelMessage {
4849
message ClientMessage {
4950
RPC rpc = 1;
5051
oneof msg {
51-
StartRequest start = 2;
52-
StopRequest stop = 3;
53-
StatusRequest status = 4;
54-
}
52+
StartRequest start = 2;
53+
StopRequest stop = 3;
54+
StatusRequest status = 4;
55+
}
5556
}
5657

5758
// ServiceMessage is a message from the service (to the client). Windows only.
@@ -131,6 +132,21 @@ message Agent {
131132
// last_handshake is the primary indicator of whether we are connected to a peer. Zero value or
132133
// anything longer than 5 minutes ago means there is a problem.
133134
google.protobuf.Timestamp last_handshake = 6;
135+
// If unset, a successful ping has not yet been made.
136+
optional LastPing last_ping = 7;
137+
}
138+
139+
message LastPing {
140+
// latency is the RTT of the ping to the agent.
141+
google.protobuf.Duration latency = 1;
142+
// did_p2p indicates whether the ping was sent P2P, or over DERP.
143+
bool did_p2p = 2;
144+
// preferred_derp is the human readable name of the preferred DERP region,
145+
// or the region used for the last ping, if it was sent over DERP.
146+
string preferred_derp = 3;
147+
// preferred_derp_latency is the last known latency to the preferred DERP
148+
// region. Unset if the region does not appear in the DERP map.
149+
optional google.protobuf.Duration preferred_derp_latency = 4;
134150
}
135151

136152
// NetworkSettingsRequest is based on

0 commit comments

Comments
 (0)