perf: Weaver automatically uses VarInt compression for all integers in [SyncVar]/[Command]/[Rpc]/SyncList/NetworkMessage/etc. (#3870)

* perf: NetworkReader/Writer: read/write collection size headers as VarInt for significant bandwidth reduction!

* Weaver: when defining multiple Readers/Writers of same type, choose the one with priority suffix

* perf: NetworkReader/Writer: define weaver preferred VarInt compression for integers

* [WeaverPriority] attribute and more tests

* remove unused

---------

Co-authored-by: mischa <info@noobtuts.com>
This commit is contained in:
mischa 2024-08-10 11:54:21 +02:00 committed by GitHub
parent d8c7c1e815
commit de7ac40af9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 122 additions and 15 deletions

View File

@ -92,4 +92,10 @@ public class ShowInInspectorAttribute : Attribute {}
/// </summary> /// </summary>
[AttributeUsage(AttributeTargets.Field)] [AttributeUsage(AttributeTargets.Field)]
public class ReadOnlyAttribute : PropertyAttribute {} public class ReadOnlyAttribute : PropertyAttribute {}
/// <summary>
/// When defining multiple Readers/Writers for the same type, indicate which one Weaver must use.
/// </summary>
[AttributeUsage(AttributeTargets.Method)]
public class WeaverPriorityAttribute : Attribute {}
} }

View File

@ -45,6 +45,14 @@ public static class NetworkReaderExtensions
public static ulong ReadULong(this NetworkReader reader) => reader.ReadBlittable<ulong>(); public static ulong ReadULong(this NetworkReader reader) => reader.ReadBlittable<ulong>();
public static ulong? ReadULongNullable(this NetworkReader reader) => reader.ReadBlittableNullable<ulong>(); public static ulong? ReadULongNullable(this NetworkReader reader) => reader.ReadBlittableNullable<ulong>();
// ReadInt/UInt/Long/ULong writes full bytes by default.
// define additional "VarInt" versions that Weaver will automatically prefer.
// 99% of the time [SyncVar] ints are small values, which makes this very much worth it.
[WeaverPriority] public static int ReadVarInt(this NetworkReader reader) => (int)Compression.DecompressVarInt(reader);
[WeaverPriority] public static uint ReadVarUInt(this NetworkReader reader) => (uint)Compression.DecompressVarUInt(reader);
[WeaverPriority] public static long ReadVarLong(this NetworkReader reader) => Compression.DecompressVarInt(reader);
[WeaverPriority] public static ulong ReadVarULong(this NetworkReader reader) => Compression.DecompressVarUInt(reader);
public static float ReadFloat(this NetworkReader reader) => reader.ReadBlittable<float>(); public static float ReadFloat(this NetworkReader reader) => reader.ReadBlittable<float>();
public static float? ReadFloatNullable(this NetworkReader reader) => reader.ReadBlittableNullable<float>(); public static float? ReadFloatNullable(this NetworkReader reader) => reader.ReadBlittableNullable<float>();

View File

@ -40,6 +40,14 @@ public static class NetworkWriterExtensions
public static void WriteULong(this NetworkWriter writer, ulong value) => writer.WriteBlittable(value); public static void WriteULong(this NetworkWriter writer, ulong value) => writer.WriteBlittable(value);
public static void WriteULongNullable(this NetworkWriter writer, ulong? value) => writer.WriteBlittableNullable(value); public static void WriteULongNullable(this NetworkWriter writer, ulong? value) => writer.WriteBlittableNullable(value);
// WriteInt/UInt/Long/ULong writes full bytes by default.
// define additional "VarInt" versions that Weaver will automatically prefer.
// 99% of the time [SyncVar] ints are small values, which makes this very much worth it.
[WeaverPriority] public static void WriteVarInt(this NetworkWriter writer, int value) => Compression.CompressVarInt(writer, value);
[WeaverPriority] public static void WriteVarUInt(this NetworkWriter writer, uint value) => Compression.CompressVarUInt(writer, value);
[WeaverPriority] public static void WriteVarLong(this NetworkWriter writer, long value) => Compression.CompressVarInt(writer, value);
[WeaverPriority] public static void WriteVarULong(this NetworkWriter writer, ulong value) => Compression.CompressVarUInt(writer, value);
public static void WriteFloat(this NetworkWriter writer, float value) => writer.WriteBlittable(value); public static void WriteFloat(this NetworkWriter writer, float value) => writer.WriteBlittable(value);
public static void WriteFloatNullable(this NetworkWriter writer, float? value) => writer.WriteBlittableNullable(value); public static void WriteFloatNullable(this NetworkWriter writer, float? value) => writer.WriteBlittableNullable(value);

View File

@ -33,12 +33,24 @@ public Readers(AssemblyDefinition assembly, WeaverTypes weaverTypes, TypeDefinit
internal void Register(TypeReference dataType, MethodReference methodReference) internal void Register(TypeReference dataType, MethodReference methodReference)
{ {
if (readFuncs.ContainsKey(dataType)) // sometimes we define multiple read methods for the same type.
// for example:
// ReadInt() // alwasy writes 4 bytes: should be available to the user for binary protocols etc.
// ReadVarInt() // varint compression: we may want Weaver to always use this for minimal bandwidth
// give the user a way to define the weaver prefered one if two exists:
// "[WeaverPriority]" attribute is automatically detected and prefered.
MethodDefinition methodDefinition = methodReference.Resolve();
bool priority = methodDefinition.HasCustomAttribute<WeaverPriorityAttribute>();
// if (priority) Log.Warning($"Weaver: Registering priority Read<{dataType.FullName}> with {methodReference.FullName}.", methodReference);
// Weaver sometimes calls Register for <T> multiple times because we resolve assemblies multiple times.
// if the function name is the same: always use the latest one.
// if the function name differes: use the priority one.
if (readFuncs.TryGetValue(dataType, out MethodReference existingMethod) && // if it was already defined
existingMethod.FullName != methodReference.FullName && // and this one is a different name
!priority) // and it's not the priority one
{ {
// TODO enable this again later. return; // then skip
// Reader has some obsolete functions that were renamed.
// Don't want weaver warnings for all of them.
//Log.Warning($"Registering a Read method for {dataType.FullName} when one already exists", methodReference);
} }
// we need to import type when we Initialize Readers so import here in case it is used anywhere else // we need to import type when we Initialize Readers so import here in case it is used anywhere else

View File

@ -33,12 +33,24 @@ public Writers(AssemblyDefinition assembly, WeaverTypes weaverTypes, TypeDefinit
public void Register(TypeReference dataType, MethodReference methodReference) public void Register(TypeReference dataType, MethodReference methodReference)
{ {
if (writeFuncs.ContainsKey(dataType)) // sometimes we define multiple write methods for the same type.
// for example:
// WriteInt() // alwasy writes 4 bytes: should be available to the user for binary protocols etc.
// WriteVarInt() // varint compression: we may want Weaver to always use this for minimal bandwidth
// give the user a way to define the weaver prefered one if two exists:
// "[WeaverPriority]" attribute is automatically detected and prefered.
MethodDefinition methodDefinition = methodReference.Resolve();
bool priority = methodDefinition.HasCustomAttribute<WeaverPriorityAttribute>();
// if (priority) Log.Warning($"Weaver: Registering priority Write<{dataType.FullName}> with {methodReference.FullName}.", methodReference);
// Weaver sometimes calls Register for <T> multiple times because we resolve assemblies multiple times.
// if the function name is the same: always use the latest one.
// if the function name differes: use the priority one.
if (writeFuncs.TryGetValue(dataType, out MethodReference existingMethod) && // if it was already defined
existingMethod.FullName != methodReference.FullName && // and this one is a different name
!priority) // and it's not the priority one
{ {
// TODO enable this again later. return; // then skip
// Writer has some obsolete functions that were renamed.
// Don't want weaver warnings for all of them.
//Log.Warning($"Registering a Write method for {dataType.FullName} when one already exists", methodReference);
} }
// we need to import type when we Initialize Writers so import here in case it is used anywhere else // we need to import type when we Initialize Writers so import here in case it is used anywhere else

View File

@ -6,24 +6,85 @@ namespace Mirror.Tests.NetworkReaderWriter
{ {
public class NetworkWriterCollectionTest public class NetworkWriterCollectionTest
{ {
// we defined WriteInt and WriteVarInt. make sure it's using the priority one.
[Test] [Test]
public void HasWriteFunctionForInt() public void UsesPriorityWriteFunctionForInt()
{ {
Assert.That(Writer<int>.write, Is.Not.Null, "int write function was not found"); Assert.That(Writer<int>.write, Is.Not.Null, "int write function was not found");
Action<NetworkWriter, int> action = NetworkWriterExtensions.WriteInt; Action<NetworkWriter, int> action = NetworkWriterExtensions.WriteVarInt;
Assert.That(Writer<int>.write, Is.EqualTo(action), "int write function was incorrect value"); Assert.That(Writer<int>.write, Is.EqualTo(action), "int write function was incorrect value");
} }
// we defined ReadInt and ReadVarInt. make sure it's using the priority one.
[Test] [Test]
public void HasReadFunctionForInt() public void UsesPriorityReadFunctionForInt()
{ {
Assert.That(Reader<int>.read, Is.Not.Null, "int read function was not found"); Assert.That(Reader<int>.read, Is.Not.Null, "int read function was not found");
Func<NetworkReader, int> action = NetworkReaderExtensions.ReadInt; Func<NetworkReader, int> action = NetworkReaderExtensions.ReadVarInt;
Assert.That(Reader<int>.read, Is.EqualTo(action), "int read function was incorrect value"); Assert.That(Reader<int>.read, Is.EqualTo(action), "int read function was incorrect value");
} }
// we defined WriteInt and WriteVarUInt. make sure it's using the priority one.
[Test]
public void UsesPriorityWriteFunctionForUInt()
{
Assert.That(Writer<uint>.write, Is.Not.Null, "uint write function was not found");
Action<NetworkWriter, uint> action = NetworkWriterExtensions.WriteVarUInt;
Assert.That(Writer<uint>.write, Is.EqualTo(action), "uint write function was incorrect value");
}
// we defined ReadInt and ReadVarInt. make sure it's using the priority one.
[Test]
public void UsesPriorityReadFunctionForUInt()
{
Assert.That(Reader<uint>.read, Is.Not.Null, "uint read function was not found");
Func<NetworkReader, uint> action = NetworkReaderExtensions.ReadVarUInt;
Assert.That(Reader<uint>.read, Is.EqualTo(action), "uint read function was incorrect value");
}
// we defined WriteInt and WriteVarInt. make sure it's using the priority one.
[Test]
public void UsesPriorityWriteFunctionForLong()
{
Assert.That(Writer<long>.write, Is.Not.Null, "long write function was not found");
Action<NetworkWriter, long> action = NetworkWriterExtensions.WriteVarLong;
Assert.That(Writer<long>.write, Is.EqualTo(action), "long write function was incorrect value");
}
// we defined ReadLong and ReadVarLong. make sure it's using the priority one.
[Test]
public void UsesPriorityReadFunctionForLong()
{
Assert.That(Reader<long>.read, Is.Not.Null, "long read function was not found");
Func<NetworkReader, long> action = NetworkReaderExtensions.ReadVarLong;
Assert.That(Reader<long>.read, Is.EqualTo(action), "long read function was incorrect value");
}
// we defined WriteLong and WriteVarULong. make sure it's using the priority one.
[Test]
public void UsesPriorityWriteFunctionForULong()
{
Assert.That(Writer<ulong>.write, Is.Not.Null, "ulong write function was not found");
Action<NetworkWriter, ulong> action = NetworkWriterExtensions.WriteVarULong;
Assert.That(Writer<ulong>.write, Is.EqualTo(action), "ulong write function was incorrect value");
}
// we defined ReadLong and ReadVarLong. make sure it's using the priority one.
[Test]
public void UsesPriorityReadFunctionForULong()
{
Assert.That(Reader<ulong>.read, Is.Not.Null, "ulong read function was not found");
Func<NetworkReader, ulong> action = NetworkReaderExtensions.ReadVarULong;
Assert.That(Reader<ulong>.read, Is.EqualTo(action), "ulong read function was incorrect value");
}
[Test] [Test]
public void HasWriteNetworkBehaviourFunction() public void HasWriteNetworkBehaviourFunction()
{ {

View File

@ -1405,7 +1405,7 @@ public void TestArrayThrowsIfLengthIsWrong(int badLength)
void WriteBadArray() void WriteBadArray()
{ {
// Reader/Writer encode null as count=0 and [] as count=1 (+1 offset) // Reader/Writer encode null as count=0 and [] as count=1 (+1 offset)
writer.WriteUInt((uint)(badLength+1)); writer.WriteVarUInt((uint)(badLength+1)); // Reader/Writer encode size headers as VarInt
int[] array = new int[testArraySize] { 1, 2, 3, 4 }; int[] array = new int[testArraySize] { 1, 2, 3, 4 };
for (int i = 0; i < array.Length; i++) for (int i = 0; i < array.Length; i++)
writer.Write(array[i]); writer.Write(array[i]);