Skip to content

Commit c091ef4

Browse files
committed
Efficient frame coalescing and ack for viewer delivery
Introduce ClientSendGrain to queue and coalesce screen frames per viewer, ensuring only the latest frame is delivered after explicit client ack. Update client/server protocol to support frame acknowledgments. Add integration tests for coalescing logic. Improve logging and refactor message delivery for reliability and backpressure.
1 parent fcdb896 commit c091ef4

File tree

12 files changed

+433
-38
lines changed

12 files changed

+433
-38
lines changed

src/RemoteViewer.Client/RemoteViewer.Client.csproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@
77
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
88
</PropertyGroup>
99

10+
<ItemGroup>
11+
<InternalsVisibleTo Include="RemoteViewer.IntegrationTests" />
12+
</ItemGroup>
13+
1014
<ItemGroup>
1115
<AvaloniaResource Include="Assets\**" />
1216
</ItemGroup>

src/RemoteViewer.Client/Services/HubClient/Connection.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -463,6 +463,10 @@ async void IConnectionImpl.OnMessageReceived(string senderClientId, string messa
463463
{
464464
var message = ProtocolSerializer.Deserialize<FrameMessage>(data);
465465
((IViewerServiceImpl)this.ViewerService!).HandleFrame(message.DisplayId, message.FrameNumber, message.Codec, message.Regions);
466+
467+
if (this.Owner.Options.SuppressAutoFrameAck is false)
468+
await this.Owner.SendAckFrameAsync();
469+
466470
break;
467471
}
468472

src/RemoteViewer.Client/Services/HubClient/ConnectionHubClient.cs

Lines changed: 34 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,15 @@ public ConnectionHubClient(
2323
{
2424
this._logger = logger;
2525
this._serviceProvider = serviceProvider;
26+
this.Options = options.Value;
2627

2728
this._connection = new HubConnectionBuilder()
28-
.WithUrl($"{options.Value.BaseUrl}/connection", httpOptions =>
29+
.WithUrl($"{this.Options.BaseUrl}/connection", httpOptions =>
2930
{
3031
httpOptions.Headers.Add("X-Client-Version", ThisAssembly.AssemblyInformationalVersion);
3132
httpOptions.Headers.Add("X-Display-Name", this.DisplayName);
3233
})
34+
3335
.WithAutomaticReconnect()
3436
.AddMessagePackProtocol(Witness.GeneratedTypeShapeProvider)
3537
.Build();
@@ -174,6 +176,8 @@ private void CloseAllConnections()
174176
this._connections.Clear();
175177
}
176178

179+
public ConnectionHubClientOptions Options { get; }
180+
177181
public string? ClientId { get; private set; }
178182
public string? Username { get; private set; }
179183
public string? Password { get; private set; }
@@ -260,9 +264,9 @@ public async Task ConnectToHub()
260264

261265
return error;
262266
}
263-
catch (Exception ex) when (!this.IsConnected)
267+
catch (Exception ex)
264268
{
265-
this._logger.LogWarning(ex, "Failed to connect to device - hub disconnected");
269+
this._logger.LogWarning(ex, "Failed to connect to device");
266270
return null;
267271
}
268272
}
@@ -278,9 +282,9 @@ public async Task GenerateNewPassword()
278282
await this._connection.InvokeAsync("GenerateNewPassword");
279283
this._logger.LogInformation("New password generated");
280284
}
281-
catch (Exception ex) when (!this.IsConnected)
285+
catch (Exception ex)
282286
{
283-
this._logger.LogWarning(ex, "Failed to generate new password - hub disconnected");
287+
this._logger.LogWarning(ex, "Failed to generate new password");
284288
}
285289
}
286290

@@ -297,9 +301,9 @@ public async Task SetDisplayName(string displayName)
297301
await this._connection.InvokeAsync("SetDisplayName", displayName);
298302
this._logger.LogInformation("Display name set successfully");
299303
}
300-
catch (Exception ex) when (!this.IsConnected)
304+
catch (Exception ex)
301305
{
302-
this._logger.LogWarning(ex, "Failed to set display name - hub disconnected");
306+
this._logger.LogWarning(ex, "Failed to set display name");
303307
}
304308
}
305309

@@ -321,9 +325,9 @@ internal async Task SendMessageAsync(string connectionId, string messageType, Re
321325
await this._connection.SendAsync("SendMessage", connectionId, messageType, data, destination, targetClientIds);
322326
this._logger.LogDebug("Message sent successfully");
323327
}
324-
catch (Exception ex) when (!this.IsConnected)
328+
catch (Exception ex)
325329
{
326-
this._logger.LogWarning(ex, "Failed to send message - hub disconnected");
330+
this._logger.LogWarning(ex, "Failed to send message");
327331
}
328332
}
329333

@@ -338,9 +342,9 @@ internal async Task DisconnectAsync(string connectionId)
338342
await this._connection.InvokeAsync("Disconnect", connectionId);
339343
this._logger.LogInformation("Disconnected from connection: {ConnectionId}", connectionId);
340344
}
341-
catch (Exception ex) when (!this.IsConnected)
345+
catch (Exception ex)
342346
{
343-
this._logger.LogWarning(ex, "Failed to disconnect - hub disconnected");
347+
this._logger.LogWarning(ex, "Failed to disconnect");
344348
}
345349
}
346350

@@ -354,9 +358,9 @@ internal async Task SetConnectionPropertiesAsync(string connectionId, Connection
354358
this._logger.LogDebug("Setting connection properties - ConnectionId: {ConnectionId}", connectionId);
355359
await this._connection.InvokeAsync("SetConnectionProperties", connectionId, properties);
356360
}
357-
catch (Exception ex) when (!this.IsConnected)
361+
catch (Exception ex)
358362
{
359-
this._logger.LogWarning(ex, "Failed to set connection properties - hub disconnected");
363+
this._logger.LogWarning(ex, "Failed to set connection properties");
360364
}
361365
}
362366

@@ -370,12 +374,27 @@ internal async Task SetConnectionPropertiesAsync(string connectionId, Connection
370374
this._logger.LogDebug("Generating IPC auth token for connection: {ConnectionId}", connectionId);
371375
return await this._connection.InvokeAsync<string?>("GenerateIpcAuthToken", connectionId);
372376
}
373-
catch (Exception ex) when (!this.IsConnected)
377+
catch (Exception ex)
374378
{
375-
this._logger.LogWarning(ex, "Failed to generate IPC auth token - hub disconnected");
379+
this._logger.LogWarning(ex, "Failed to generate IPC auth token");
376380
return null;
377381
}
378382
}
383+
384+
internal async Task SendAckFrameAsync()
385+
{
386+
if (!this.IsConnected || this.IsReconnecting)
387+
return;
388+
389+
try
390+
{
391+
await this._connection.SendAsync("AckFrame");
392+
}
393+
catch (Exception ex)
394+
{
395+
this._logger.LogWarning(ex, "Failed to send frame ack");
396+
}
397+
}
379398
}
380399

381400
#region EventArgs Classes
Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
using Microsoft.AspNetCore.Http.Connections;
1+
namespace RemoteViewer.Client.Services.HubClient;
22

3-
namespace RemoteViewer.Client.Services.HubClient;
43

54
public class ConnectionHubClientOptions
65
{
@@ -9,4 +8,6 @@ public class ConnectionHubClientOptions
98
#else
109
public string BaseUrl { get; set; } = "https://rdp.xemio.net";
1110
#endif
11+
12+
public bool SuppressAutoFrameAck { get; set; }
1213
}

src/RemoteViewer.Server/Hubs/ConnectionHub.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,11 @@ public async Task SendMessage(string connectionId, string messageType, byte[] da
7979
await clientsService.SendMessage(this.Context.ConnectionId, connectionId, messageType, data, destination, targetClientIds);
8080
}
8181

82+
public Task AckFrame()
83+
{
84+
return clientsService.AckFrame(this.Context.ConnectionId);
85+
}
86+
8287
public async Task SetConnectionProperties(string connectionId, ConnectionProperties properties)
8388
{
8489
await clientsService.SetConnectionProperties(this.Context.ConnectionId, connectionId, properties);

src/RemoteViewer.Server/Orleans/Grains/ClientGrain.cs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ public sealed partial class ClientGrain(ILogger<ClientGrain> logger, IHubContext
3535

3636
private string _displayName = string.Empty;
3737

38+
private IClientSendGrain? _sendGrain;
39+
3840
private IConnectionGrain? _presenterConnectionGrain;
3941
private readonly List<IConnectionGrain> _viewerConnectionGrains = [];
4042

@@ -67,6 +69,8 @@ public async Task Initialize(string? displayName)
6769
this.LogUsernameCollision(attempts);
6870
}
6971

72+
this._sendGrain = this.GrainFactory.GetGrain<IClientSendGrain>(this.GetPrimaryKeyString());
73+
7074
this.LogClientInitialized(this._clientId, this._usernameGrain.GetPrimaryKeyString());
7175

7276
await hubContext.Clients
@@ -93,6 +97,7 @@ public async Task Deactivate()
9397
await connection.Internal_RemoveClient(this.AsReference<IClientGrain>());
9498
}
9599

100+
await this._sendGrain.Disconnect();
96101
await this._usernameGrain.ReleaseAsync(this.GetPrimaryKeyString());
97102

98103
this.DeactivateOnIdle();
@@ -208,14 +213,15 @@ private static string FormatUsername(string username)
208213
}
209214
return sb.ToString();
210215
}
211-
[MemberNotNull(nameof(_clientId), nameof(_usernameGrain), nameof(_password))]
216+
[MemberNotNull(nameof(_clientId), nameof(_usernameGrain), nameof(_sendGrain), nameof(_password))]
212217
private void EnsureInitialized()
213218
{
214-
if (this._clientId is null || this._usernameGrain is null || this._password is null)
219+
if (this._clientId is null || this._usernameGrain is null || this._sendGrain is null || this._password is null)
215220
{
216221
throw new InvalidOperationException(
217222
$"ClientGrain not initialized: clientId={(this._clientId is null ? "null" : "set")}, " +
218223
$"usernameGrain={(this._usernameGrain is null ? "null" : "set")}, " +
224+
$"sendGrain={(this._sendGrain is null ? "null" : "set")}, " +
219225
$"password={(this._password is null ? "null" : "set")}");
220226
}
221227
}

0 commit comments

Comments
 (0)