Skip to content

Commit e977cbf

Browse files
authored
Optimize WebSocketClient JSON serialization (#369)
* Optimize WebSocketClient JSON serialization * Create a DefaultBufferSize constant in WebSocketClientJsonSerializer
1 parent be175ee commit e977cbf

8 files changed

Lines changed: 127 additions & 31 deletions

NetCord/Gateway/GatewayClient.cs

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -896,16 +896,18 @@ private protected override void OnConnected()
896896

897897
private ValueTask SendIdentifyAsync(ConnectionState connectionState, PresenceProperties? presence = null, CancellationToken cancellationToken = default)
898898
{
899-
var serializedPayload = new GatewayPayloadProperties<GatewayIdentifyProperties>(GatewayOpcode.Identify, new(Token.RawToken)
899+
GatewayPayloadProperties<GatewayIdentifyProperties> payload = new(GatewayOpcode.Identify, new(Token.RawToken)
900900
{
901901
ConnectionProperties = _connectionProperties,
902902
LargeThreshold = _largeThreshold,
903903
Shard = Shard,
904904
Presence = presence ?? _presence,
905905
Intents = _intents,
906-
}).Serialize(Serialization.Default.GatewayPayloadPropertiesGatewayIdentifyProperties);
906+
});
907+
907908
_latencyTimer.Start();
908-
return SendConnectionPayloadAsync(connectionState, serializedPayload, _internalTextPayloadProperties, cancellationToken);
909+
910+
return SendConnectionObjectAsync(connectionState, payload, Serialization.Default.GatewayPayloadPropertiesGatewayIdentifyProperties, _internalTextPayloadProperties, cancellationToken);
909911
}
910912

911913
/// <summary>
@@ -948,16 +950,20 @@ private protected override ValueTask TryResumeAsync(ConnectionState connectionSt
948950

949951
private ValueTask TryResumeAsync(ConnectionState connectionState, string sessionId, int sequenceNumber, CancellationToken cancellationToken = default)
950952
{
951-
var serializedPayload = new GatewayPayloadProperties<GatewayResumeProperties>(GatewayOpcode.Resume, new(Token.RawToken, sessionId, sequenceNumber)).Serialize(Serialization.Default.GatewayPayloadPropertiesGatewayResumeProperties);
953+
GatewayPayloadProperties<GatewayResumeProperties> payload = new(GatewayOpcode.Resume, new(Token.RawToken, sessionId, sequenceNumber));
954+
952955
_latencyTimer.Start();
953-
return SendConnectionPayloadAsync(connectionState, serializedPayload, _internalTextPayloadProperties, cancellationToken);
956+
957+
return SendConnectionObjectAsync(connectionState, payload, Serialization.Default.GatewayPayloadPropertiesGatewayResumeProperties, _internalTextPayloadProperties, cancellationToken);
954958
}
955959

956960
private protected override ValueTask HeartbeatAsync(ConnectionState connectionState, CancellationToken cancellationToken = default)
957961
{
958-
var serializedPayload = new GatewayPayloadProperties<int>(GatewayOpcode.Heartbeat, SequenceNumber).Serialize(Serialization.Default.GatewayPayloadPropertiesInt32);
962+
GatewayPayloadProperties<int> payload = new(GatewayOpcode.Heartbeat, SequenceNumber);
963+
959964
_latencyTimer.Start();
960-
return SendConnectionPayloadAsync(connectionState, serializedPayload, _internalTextPayloadProperties, cancellationToken);
965+
966+
return SendConnectionObjectAsync(connectionState, payload, Serialization.Default.GatewayPayloadPropertiesInt32, _internalTextPayloadProperties, cancellationToken);
961967
}
962968

963969
private protected override ValueTask ProcessPayloadAsync(State state, ConnectionState connectionState, WebSocketMessageType messageType, ReadOnlySpan<byte> payload)
@@ -1037,7 +1043,8 @@ private async ValueTask HandlePayloadAsync(State state, ConnectionState connecti
10371043
public ValueTask UpdateVoiceStateAsync(VoiceStateProperties voiceState, WebSocketPayloadProperties? properties = null, CancellationToken cancellationToken = default)
10381044
{
10391045
GatewayPayloadProperties<VoiceStateProperties> payload = new(GatewayOpcode.VoiceStateUpdate, voiceState);
1040-
return SendPayloadAsync(payload.Serialize(Serialization.Default.GatewayPayloadPropertiesVoiceStateProperties), properties, cancellationToken);
1046+
1047+
return SendObjectAsync(payload, Serialization.Default.GatewayPayloadPropertiesVoiceStateProperties, properties, cancellationToken);
10411048
}
10421049

10431050
/// <summary>
@@ -1049,7 +1056,8 @@ public ValueTask UpdateVoiceStateAsync(VoiceStateProperties voiceState, WebSocke
10491056
public ValueTask UpdatePresenceAsync(PresenceProperties presence, WebSocketPayloadProperties? properties = null, CancellationToken cancellationToken = default)
10501057
{
10511058
GatewayPayloadProperties<PresenceProperties> payload = new(GatewayOpcode.PresenceUpdate, presence);
1052-
return SendPayloadAsync(payload.Serialize(Serialization.Default.GatewayPayloadPropertiesPresenceProperties), properties, cancellationToken);
1059+
1060+
return SendObjectAsync(payload, Serialization.Default.GatewayPayloadPropertiesPresenceProperties, properties, cancellationToken);
10531061
}
10541062

10551063
/// <summary>
@@ -1058,7 +1066,8 @@ public ValueTask UpdatePresenceAsync(PresenceProperties presence, WebSocketPaylo
10581066
public ValueTask RequestGuildUsersAsync(GuildUsersRequestProperties requestProperties, WebSocketPayloadProperties? properties = null, CancellationToken cancellationToken = default)
10591067
{
10601068
GatewayPayloadProperties<GuildUsersRequestProperties> payload = new(GatewayOpcode.RequestGuildUsers, requestProperties);
1061-
return SendPayloadAsync(payload.Serialize(Serialization.Default.GatewayPayloadPropertiesGuildUsersRequestProperties), properties, cancellationToken);
1069+
1070+
return SendObjectAsync(payload, Serialization.Default.GatewayPayloadPropertiesGuildUsersRequestProperties, properties, cancellationToken);
10621071
}
10631072

10641073
private async Task ProcessEventAsync(State state, ConnectionState connectionState, JsonGatewayPayload payload)
Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
using System.Text.Json;
21
using System.Text.Json.Serialization;
3-
using System.Text.Json.Serialization.Metadata;
42

53
namespace NetCord.Gateway;
64

@@ -11,6 +9,4 @@ internal class GatewayPayloadProperties<T>(GatewayOpcode opcode, T d)
119

1210
[JsonPropertyName("d")]
1311
public T D { get; set; } = d;
14-
15-
public byte[] Serialize(JsonTypeInfo<GatewayPayloadProperties<T>> jsonTypeInfo) => JsonSerializer.SerializeToUtf8Bytes(this, jsonTypeInfo);
1612
}

NetCord/Gateway/RentedArrayBufferWriter.cs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,20 @@ namespace NetCord.Gateway;
66

77
internal sealed class RentedArrayBufferWriter<T>(int minimumInitialCapacity) : IBufferWriter<T>, IDisposable
88
{
9+
public readonly struct RentedBufferOwner(T[] buffer, int writtenCount) : IDisposable
10+
{
11+
public ReadOnlyMemory<T> WrittenMemory => buffer.AsMemory(0, writtenCount);
12+
13+
public ReadOnlySpan<T> WrittenSpan => buffer.AsSpan(0, writtenCount);
14+
15+
public int WrittenCount => writtenCount;
16+
17+
public void Dispose()
18+
{
19+
ArrayPool<T>.Shared.Return(buffer);
20+
}
21+
}
22+
923
private int _index;
1024

1125
private T[] _buffer = ArrayPool<T>.Shared.Rent(minimumInitialCapacity);
@@ -23,6 +37,19 @@ public void Advance(int count)
2337
_index += count;
2438
}
2539

40+
public RentedBufferOwner ExtractBuffer()
41+
{
42+
var buffer = _buffer;
43+
44+
RentedBufferOwner rentedBufferOwner = new(buffer, _index);
45+
46+
_buffer = ArrayPool<T>.Shared.Rent(buffer.Length);
47+
48+
_index = 0;
49+
50+
return rentedBufferOwner;
51+
}
52+
2653
public void Clear()
2754
{
2855
_index = 0;
@@ -52,13 +79,19 @@ private void ResizeBuffer(int sizeHint)
5279

5380
var buffer = _buffer;
5481
int index = _index;
82+
5583
int sum = index + sizeHint;
84+
5685
if (buffer.Length < sum)
5786
{
5887
var pool = ArrayPool<T>.Shared;
88+
5989
var newBuffer = pool.Rent(sum);
90+
6091
buffer.AsSpan(0, index).CopyTo(newBuffer);
92+
6193
_buffer = newBuffer;
94+
6295
pool.Return(buffer);
6396
}
6497
}

NetCord/Gateway/Voice/VoiceClient.DaveSession.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -367,9 +367,9 @@ private async ValueTask SendMlsKeyPackageAsync(ConnectionState connectionState)
367367

368368
private ValueTask SendTransitionReadyAsync(ConnectionState connectionState, ushort transitionId)
369369
{
370-
VoicePayloadProperties<DaveTransitionReadyProperties> readyPayload = new(VoiceOpcode.DaveTransitionReady, new(transitionId));
370+
VoicePayloadProperties<DaveTransitionReadyProperties> payload = new(VoiceOpcode.DaveTransitionReady, new(transitionId));
371371

372-
return _client.SendConnectionPayloadAsync(connectionState, readyPayload.Serialize(Serialization.Default.VoicePayloadPropertiesDaveTransitionReadyProperties), _client._internalTextPayloadProperties);
372+
return _client.SendConnectionObjectAsync(connectionState, payload, Serialization.Default.VoicePayloadPropertiesDaveTransitionReadyProperties, _client._internalTextPayloadProperties);
373373
}
374374

375375
private ValueTask SendMlsCommitWelcomeAsync(ConnectionState connectionState, ReadOnlySpan<byte> commitWelcomeMessage)
@@ -397,9 +397,9 @@ static async ValueTask ContinueAsync(VoiceClient client, ConnectionState connect
397397

398398
private ValueTask SendMlsInvalidCommitWelcomeAsync(ConnectionState connectionState, ushort transitionId)
399399
{
400-
VoicePayloadProperties<DaveMlsInvalidCommitWelcomeProperties> invalidCommitWelcomePayload = new(VoiceOpcode.DaveMlsInvalidCommitWelcome, new(transitionId));
400+
VoicePayloadProperties<DaveMlsInvalidCommitWelcomeProperties> payload = new(VoiceOpcode.DaveMlsInvalidCommitWelcome, new(transitionId));
401401

402-
return _client.SendConnectionPayloadAsync(connectionState, invalidCommitWelcomePayload.Serialize(Serialization.Default.VoicePayloadPropertiesDaveMlsInvalidCommitWelcomeProperties), _client._internalTextPayloadProperties);
402+
return _client.SendConnectionObjectAsync(connectionState, payload, Serialization.Default.VoicePayloadPropertiesDaveMlsInvalidCommitWelcomeProperties, _client._internalTextPayloadProperties);
403403
}
404404

405405
[SkipLocalsInit]

NetCord/Gateway/Voice/VoiceClient.cs

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -129,9 +129,11 @@ public void Dispose()
129129

130130
private protected override ValueTask SendIdentifyAsync(ConnectionState connectionState, CancellationToken cancellationToken = default)
131131
{
132-
var serializedPayload = new VoicePayloadProperties<VoiceIdentifyProperties>(VoiceOpcode.Identify, new(GuildId, UserId, SessionId, Token, DaveSession.GetMaxSupportedProtocolVersion())).Serialize(Serialization.Default.VoicePayloadPropertiesVoiceIdentifyProperties);
132+
VoicePayloadProperties<VoiceIdentifyProperties> payload = new(VoiceOpcode.Identify, new(GuildId, UserId, SessionId, Token, DaveSession.GetMaxSupportedProtocolVersion()));
133+
133134
_latencyTimer.Start();
134-
return SendConnectionPayloadAsync(connectionState, serializedPayload, _internalTextPayloadProperties, cancellationToken);
135+
136+
return SendConnectionObjectAsync(connectionState, payload, Serialization.Default.VoicePayloadPropertiesVoiceIdentifyProperties, _internalTextPayloadProperties, cancellationToken);
135137
}
136138

137139
private VoiceState CreateState()
@@ -201,16 +203,27 @@ private protected override ValueTask TryResumeAsync(ConnectionState connectionSt
201203

202204
private ValueTask TryResumeAsync(ConnectionState connectionState, int sequenceNumber, CancellationToken cancellationToken = default)
203205
{
204-
var serializedPayload = new VoicePayloadProperties<VoiceResumeProperties>(VoiceOpcode.Resume, new(GuildId, SessionId, Token, sequenceNumber)).Serialize(Serialization.Default.VoicePayloadPropertiesVoiceResumeProperties);
206+
VoicePayloadProperties<VoiceResumeProperties> payload = new(VoiceOpcode.Resume, new(GuildId, SessionId, Token, sequenceNumber));
207+
205208
_latencyTimer.Start();
206-
return SendConnectionPayloadAsync(connectionState, serializedPayload, _internalTextPayloadProperties, cancellationToken);
209+
210+
return SendConnectionObjectAsync(connectionState, payload, Serialization.Default.VoicePayloadPropertiesVoiceResumeProperties, _internalTextPayloadProperties, cancellationToken);
207211
}
208212

209213
private protected override ValueTask HeartbeatAsync(ConnectionState connectionState, CancellationToken cancellationToken = default)
210214
{
211-
var serializedPayload = new VoicePayloadProperties<VoiceHeartbeatProperties>(VoiceOpcode.Heartbeat, new(Environment.TickCount, SequenceNumber)).Serialize(Serialization.Default.VoicePayloadPropertiesVoiceHeartbeatProperties);
215+
VoicePayloadProperties<VoiceHeartbeatProperties> payload = new(VoiceOpcode.Heartbeat, new(Environment.TickCount, SequenceNumber));
216+
212217
_latencyTimer.Start();
213-
return SendConnectionPayloadAsync(connectionState, serializedPayload, _internalTextPayloadProperties, cancellationToken);
218+
219+
return SendConnectionObjectAsync(connectionState, payload, Serialization.Default.VoicePayloadPropertiesVoiceHeartbeatProperties, _internalTextPayloadProperties, cancellationToken);
220+
}
221+
222+
private ValueTask SelectProtocolAsync(ConnectionState connectionState, ProtocolProperties protocolProperties, CancellationToken cancellationToken = default)
223+
{
224+
VoicePayloadProperties<ProtocolProperties> payload = new(VoiceOpcode.SelectProtocol, protocolProperties);
225+
226+
return SendConnectionObjectAsync(connectionState, payload, Serialization.Default.VoicePayloadPropertiesProtocolProperties, _internalTextPayloadProperties, cancellationToken);
214227
}
215228

216229
private protected override ValueTask ProcessPayloadAsync(State state, ConnectionState connectionState, WebSocketMessageType messageType, ReadOnlySpan<byte> payload)
@@ -370,8 +383,7 @@ private async ValueTask HandleJsonPayloadAsync(State state, ConnectionState conn
370383

371384
Log<object?>(LogLevel.Debug, null, null, static (s, e) => "Selecting a protocol.");
372385

373-
VoicePayloadProperties<ProtocolProperties> protocolPayload = new(VoiceOpcode.SelectProtocol, new("udp", new(externalIp, externalPort, encryptionName)));
374-
await SendConnectionPayloadAsync(connectionState, protocolPayload.Serialize(Serialization.Default.VoicePayloadPropertiesProtocolProperties), _internalTextPayloadProperties).ConfigureAwait(false);
386+
await SelectProtocolAsync(connectionState, new("udp", new(externalIp, externalPort, encryptionName))).ConfigureAwait(false);
375387

376388
await updateLatencyTask;
377389
}
@@ -732,7 +744,8 @@ private bool TryGetVoiceData(RtpPacket packet, IVoiceEncryption encryption, Dave
732744
public ValueTask EnterSpeakingStateAsync(SpeakingProperties speaking, WebSocketPayloadProperties? properties = null, CancellationToken cancellationToken = default)
733745
{
734746
VoicePayloadProperties<SpeakingProperties> payload = new(VoiceOpcode.Speaking, speaking);
735-
return SendPayloadAsync(payload.Serialize(Serialization.Default.VoicePayloadPropertiesSpeakingProperties), properties, cancellationToken);
747+
748+
return SendObjectAsync(payload, Serialization.Default.VoicePayloadPropertiesSpeakingProperties, properties, cancellationToken);
736749
}
737750

738751
/// <summary>
Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
using System.Text.Json;
21
using System.Text.Json.Serialization;
3-
using System.Text.Json.Serialization.Metadata;
42

53
namespace NetCord.Gateway.Voice;
64

@@ -11,6 +9,4 @@ internal class VoicePayloadProperties<T>(VoiceOpcode opcode, T d)
119

1210
[JsonPropertyName("d")]
1311
public T D { get; set; } = d;
14-
15-
public byte[] Serialize(JsonTypeInfo<VoicePayloadProperties<T>> jsonTypeInfo) => JsonSerializer.SerializeToUtf8Bytes(this, jsonTypeInfo);
1612
}

NetCord/Gateway/WebSocketClient.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
using System.Diagnostics;
44
using System.Diagnostics.CodeAnalysis;
55
using System.Runtime.CompilerServices;
6+
using System.Text.Json;
7+
using System.Text.Json.Serialization.Metadata;
68

79
using NetCord.Gateway.LatencyTimers;
810
using NetCord.Gateway.ReconnectStrategies;
@@ -610,6 +612,13 @@ private protected ValueTask AbortAndResumeAsync(State state, ConnectionState con
610612
return ResumeAsync(state);
611613
}
612614

615+
private protected async ValueTask SendObjectAsync<T>(T obj, JsonTypeInfo<T> jsonTypeInfo, WebSocketPayloadProperties? properties, CancellationToken cancellationToken = default)
616+
{
617+
using var output = WebSocketClientJsonSerializer.Serialize(obj, jsonTypeInfo);
618+
619+
await SendPayloadAsync(output.WrittenMemory, properties, cancellationToken).ConfigureAwait(false);
620+
}
621+
613622
public async ValueTask SendPayloadAsync(ReadOnlyMemory<byte> buffer, WebSocketPayloadProperties? properties = null, CancellationToken cancellationToken = default)
614623
{
615624
var payloadProperties = _defaultTextPayloadProperties.Compose(properties);
@@ -658,6 +667,13 @@ public async ValueTask SendPayloadAsync(ReadOnlyMemory<byte> buffer, WebSocketPa
658667
}
659668
}
660669

670+
private protected async ValueTask SendConnectionObjectAsync<T>(ConnectionState connectionState, T obj, JsonTypeInfo<T> jsonTypeInfo, InternalWebSocketPayloadProperties payloadProperties, CancellationToken cancellationToken = default)
671+
{
672+
using var output = WebSocketClientJsonSerializer.Serialize(obj, jsonTypeInfo);
673+
674+
await SendConnectionPayloadAsync(connectionState, output.WrittenMemory, payloadProperties, cancellationToken).ConfigureAwait(false);
675+
}
676+
661677
private protected async ValueTask SendConnectionPayloadAsync(ConnectionState connectionState, ReadOnlyMemory<byte> buffer, InternalWebSocketPayloadProperties payloadProperties, CancellationToken cancellationToken = default)
662678
{
663679
var exception = await TrySendConnectionPayloadAsync(connectionState, buffer, payloadProperties, cancellationToken).ConfigureAwait(false);
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
using System.Text.Json;
2+
using System.Text.Json.Serialization.Metadata;
3+
4+
namespace NetCord.Gateway;
5+
6+
internal static class WebSocketClientJsonSerializer
7+
{
8+
private readonly record struct State(Utf8JsonWriter Writer, RentedArrayBufferWriter<byte> Output);
9+
10+
private const int DefaultBufferSize = 1024;
11+
12+
[ThreadStatic]
13+
private static State? t_state;
14+
15+
public static RentedArrayBufferWriter<byte>.RentedBufferOwner Serialize<T>(T value, JsonTypeInfo<T> jsonTypeInfo)
16+
{
17+
if (t_state is (var writer, var output))
18+
{
19+
writer.Reset();
20+
output.Clear();
21+
}
22+
else
23+
{
24+
output = new(DefaultBufferSize);
25+
writer = new(output);
26+
t_state = new(writer, output);
27+
}
28+
29+
JsonSerializer.Serialize(writer, value, jsonTypeInfo);
30+
31+
return output.ExtractBuffer();
32+
}
33+
}

0 commit comments

Comments
 (0)