]> git.deb.at Git - rhonda/impostor.git/commitdiff
Solved GameOptionsData TODO (#64)
authorJonas Kamsker <11245306+JKamsker@users.noreply.github.com>
Wed, 21 Oct 2020 22:08:14 +0000 (00:08 +0200)
committerGitHub <noreply@github.com>
Wed, 21 Oct 2020 22:08:14 +0000 (00:08 +0200)
* Ignoring .Vs folder

* GameOptionsData deserialization requires x16 less time & no extra alloc

* Super-micro optimisation (variable localization)

* JoinGame probably faster

* Added Tests

* Renamed swap -> advance

* Removed submodule Hazel

* Removed launchsettings

Co-authored-by: Weirdo <git.jonas@kamsker.at>
12 files changed:
.gitignore
src/Impostor.Api/Extensions/SpanReaderExtensions.cs [new file with mode: 0644]
src/Impostor.Api/Impostor.Api.csproj
src/Impostor.Api/Innersloth/GameOptionsData.cs
src/Impostor.Api/Net/Messages/C2S/Message01JoinGameC2S.cs
src/Impostor.Api/Net/Messages/C2S/Message10AlterGameC2S.cs
src/Impostor.Hazel/MessageReader.cs
src/Impostor.Hazel/MessageWriter.cs
src/Impostor.Plugins.Debugger/Impostor.Plugins.Debugger.csproj
src/Impostor.Server/GameData/Objects/Components/InnerVoteBanSystem.cs
src/Impostor.Tests/GameCodeTests.cs
src/Impostor.Tests/Hazel/MessageWriterTests.cs [new file with mode: 0644]

index 95e2d57dc6b8d0aaa922c15ba8944e428989033f..3878f3eb6c7386e0702a240179c81dfee4fc6e55 100644 (file)
@@ -2,3 +2,4 @@
 /build
 /.vs
 /src/Impostor.Plugins.Debugger/Properties/launchSettings.json
+
diff --git a/src/Impostor.Api/Extensions/SpanReaderExtensions.cs b/src/Impostor.Api/Extensions/SpanReaderExtensions.cs
new file mode 100644 (file)
index 0000000..97365ac
--- /dev/null
@@ -0,0 +1,70 @@
+using System;
+using System.Buffers.Binary;
+using System.Runtime.CompilerServices;
+
+namespace Impostor.Api.Extensions
+{
+    /// <summary>
+    /// Priovides a StreamReader-like api throught extensions
+    /// </summary>
+    public static class SpanReaderExtensions
+    {
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        public static byte ReadByte(this ref ReadOnlySpan<byte> input)
+        {
+            var original = Advance<byte>(ref input);
+            return original[0];
+        }
+
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        public static int ReadInt32(this ref ReadOnlySpan<byte> input)
+        {
+            var original = Advance<int>(ref input);
+            return BinaryPrimitives.ReadInt32LittleEndian(original);
+        }
+
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        public static uint ReadUInt32(this ref ReadOnlySpan<byte> input)
+        {
+            var original = Advance<uint>(ref input);
+            return BinaryPrimitives.ReadUInt32LittleEndian(original);
+        }
+
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        public static float ReadSingle(this ref ReadOnlySpan<byte> input)
+        {
+            var original = Advance<float>(ref input);
+
+            // BitConverter.Int32BitsToSingle
+            // Doesn't exist in net 2.0 for some reason
+            return Int32BitsToSingle(BinaryPrimitives.ReadInt32LittleEndian(original));
+        }
+
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        public static bool ReadBoolean(this ref ReadOnlySpan<byte> input)
+        {
+            return input.ReadByte() != 0;
+        }
+
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        private static unsafe float Int32BitsToSingle(int value)
+        {
+            return *((float*)&value);
+        }
+
+        /// <summary>
+        /// Advances the position of <see cref="input"/> by the size of <see cref="T"/>.
+        /// </summary>
+        /// <typeparam name="T">Type that will be read.</typeparam>
+        /// <param name="input">input "stream"/span.</param>
+        /// <returns>The original input</returns>
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        private static unsafe ReadOnlySpan<byte> Advance<T>(ref ReadOnlySpan<byte> input)
+            where T : unmanaged
+        {
+            var original = input;
+            input = input.Slice(sizeof(T));
+            return original;
+        }
+    }
+}
\ No newline at end of file
index 9f65ba6d8de4067aa05c9afcf8fd79c738b6bdf7..071adf1894d2584a4f67cb628b9d466d0e2ce1aa 100644 (file)
@@ -7,6 +7,14 @@
         <LangVersion>9</LangVersion>
     </PropertyGroup>
 
+    <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
+      <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
+    </PropertyGroup>
+
+    <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
+      <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
+    </PropertyGroup>
+
     <ItemGroup>
       <PackageReference Include="Microsoft.Bcl.HashCode" Version="1.1.0" />
       <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="5.0.0-rc.1.20451.14" />
index cd26653d37905a200fe6a6cc67e740d7b8fecf81..e420e53f64fa2796bf417057c8871a8dc7412713 100644 (file)
@@ -1,7 +1,9 @@
-using System;
-using System.IO;
+using Impostor.Api.Extensions;
 using Impostor.Api.Innersloth.Data;
 
+using System;
+using System.IO;
+
 namespace Impostor.Api.Innersloth
 {
     public class GameOptionsData
@@ -29,78 +31,78 @@ namespace Impostor.Api.Innersloth
         public bool ConfirmImpostor { get; set; }
         public bool VisualTasks { get; set; }
         public bool IsDefaults { get; set; }
-        
+
         public void Serialize(BinaryWriter writer, byte version)
         {
-            writer.Write((byte) version);
-            writer.Write((byte) MaxPlayers);
-            writer.Write((uint) Keywords);
-            writer.Write((byte) MapId);
-            writer.Write((float) PlayerSpeedMod);
-            writer.Write((float) CrewLightMod);
-            writer.Write((float) ImpostorLightMod);
-            writer.Write((float) KillCooldown);
-            writer.Write((byte) NumCommonTasks);
-            writer.Write((byte) NumLongTasks);
-            writer.Write((byte) NumShortTasks);
-            writer.Write((int) NumEmergencyMeetings);
-            writer.Write((byte) NumImpostors);
-            writer.Write((byte) KillDistance);
-            writer.Write((uint) DiscussionTime);
-            writer.Write((uint) VotingTime);
-            writer.Write((bool) IsDefaults);
+            writer.Write((byte)version);
+            writer.Write((byte)MaxPlayers);
+            writer.Write((uint)Keywords);
+            writer.Write((byte)MapId);
+            writer.Write((float)PlayerSpeedMod);
+            writer.Write((float)CrewLightMod);
+            writer.Write((float)ImpostorLightMod);
+            writer.Write((float)KillCooldown);
+            writer.Write((byte)NumCommonTasks);
+            writer.Write((byte)NumLongTasks);
+            writer.Write((byte)NumShortTasks);
+            writer.Write((int)NumEmergencyMeetings);
+            writer.Write((byte)NumImpostors);
+            writer.Write((byte)KillDistance);
+            writer.Write((uint)DiscussionTime);
+            writer.Write((uint)VotingTime);
+            writer.Write((bool)IsDefaults);
             if (version > 1)
             {
-                writer.Write((byte) EmergencyCooldown);
+                writer.Write((byte)EmergencyCooldown);
             }
 
             if (version > 2)
             {
-                writer.Write((bool) ConfirmImpostor);
-                writer.Write((bool) VisualTasks);
+                writer.Write((bool)ConfirmImpostor);
+                writer.Write((bool)VisualTasks);
             }
         }
 
-        public static GameOptionsData Deserialize(ReadOnlyMemory<byte> bytes)
+        public static GameOptionsData Deserialize(ReadOnlyMemory<byte> memory)
         {
-            // TODO: Remove memory allocation.
+            var bytes = memory.Span;
 
-            using (var stream = new MemoryStream(bytes.ToArray()))
-            using (var reader = new BinaryReader(stream))
-            {
-                var result = new GameOptionsData();
+            var result = new GameOptionsData();
+            result.Version = bytes.ReadByte();
+            result.MaxPlayers = bytes.ReadByte();
+            result.Keywords = (GameKeywords)bytes.ReadUInt32();
+            result.MapId = bytes.ReadByte();
+            result.PlayerSpeedMod = bytes.ReadSingle();
+
+            result.CrewLightMod = bytes.ReadSingle();
+            result.ImpostorLightMod = bytes.ReadSingle();
+            result.KillCooldown = bytes.ReadSingle();
+
+            result.NumCommonTasks = bytes.ReadByte();
+            result.NumLongTasks = bytes.ReadByte();
+            result.NumShortTasks = bytes.ReadByte();
 
-                result.Version = reader.ReadByte();
-                result.MaxPlayers = reader.ReadByte();
-                result.Keywords = (GameKeywords) reader.ReadUInt32();
-                result.MapId = reader.ReadByte();
-                result.PlayerSpeedMod = reader.ReadSingle();
-                result.CrewLightMod = reader.ReadSingle();
-                result.ImpostorLightMod = reader.ReadSingle();
-                result.KillCooldown = reader.ReadSingle();
-                result.NumCommonTasks = reader.ReadByte();
-                result.NumLongTasks = reader.ReadByte();
-                result.NumShortTasks = reader.ReadByte();
-                result.NumEmergencyMeetings = reader.ReadInt32();
-                result.NumImpostors = reader.ReadByte();
-                result.KillDistance = reader.ReadByte();
-                result.DiscussionTime = reader.ReadInt32();
-                result.VotingTime = reader.ReadInt32();
-                result.IsDefaults = reader.ReadBoolean();
+            result.NumEmergencyMeetings = bytes.ReadInt32();
 
-                if (result.Version > 1)
-                {
-                    result.EmergencyCooldown = reader.ReadByte();
-                }
+            result.NumImpostors = bytes.ReadByte();
+            result.KillDistance = bytes.ReadByte();
+            result.DiscussionTime = bytes.ReadInt32();
+            result.VotingTime = bytes.ReadInt32();
 
-                if (result.Version > 2)
-                {
-                    result.ConfirmImpostor = reader.ReadBoolean();
-                    result.VisualTasks = reader.ReadBoolean();
-                }
-                
-                return result;
+            result.IsDefaults = bytes.ReadBoolean();
+
+            if (result.Version > 1)
+            {
+                result.EmergencyCooldown = bytes.ReadByte();
+            }
+
+            if (result.Version > 2)
+            {
+                result.ConfirmImpostor = bytes.ReadBoolean();
+                result.VisualTasks = bytes.ReadBoolean();
             }
+
+            return result;
         }
     }
 }
\ No newline at end of file
index 7e6baf32aece622ffce5cad7da49be6036e35558..c713790e39d0cdebcf9c6866f6ffe30a724da614 100644 (file)
@@ -1,4 +1,7 @@
-namespace Impostor.Api.Net.Messages.C2S
+using System;
+using Impostor.Api.Extensions;
+
+namespace Impostor.Api.Net.Messages.C2S
 {
     public static class Message01JoinGameC2S
     {
@@ -9,8 +12,10 @@
 
         public static void Deserialize(IMessageReader reader, out int gameCode, out byte unknown)
         {
-            gameCode = reader.ReadInt32();
-            unknown = reader.ReadByte();
+            var slice = reader.ReadBytes(sizeof(Int32) + sizeof(byte)).Span;
+
+            gameCode = slice.ReadInt32();
+            unknown = slice.ReadByte();
         }
     }
 }
\ No newline at end of file
index 3a92e5803d25164e37159904641c1f0a0496644f..e9ad8e8af1b1a4f50925f49fa85b28ce61217ba6 100644 (file)
@@ -1,4 +1,5 @@
-using Impostor.Api.Innersloth.Data;
+using Impostor.Api.Extensions;
+using Impostor.Api.Innersloth.Data;
 
 namespace Impostor.Api.Net.Messages.C2S
 {
@@ -11,8 +12,10 @@ namespace Impostor.Api.Net.Messages.C2S
 
         public static void Deserialize(IMessageReader reader, out AlterGameTags gameTag, out bool isPublic)
         {
-            gameTag = (AlterGameTags)reader.ReadByte();
-            isPublic = reader.ReadBoolean();
+            var slice = reader.ReadBytes(sizeof(byte) + sizeof(byte)).Span;
+
+            gameTag = (AlterGameTags)slice.ReadByte();
+            isPublic = slice.ReadBoolean();
         }
     }
 }
\ No newline at end of file
index ffed6ca1bf647f042c34461d0af773515c8ec5cb..f81c482551d48e707d6246ee1527616c4e5958b5 100644 (file)
@@ -28,7 +28,7 @@ namespace Impostor.Hazel
         public IMessageReader ReadMessage()
         {
             var length = ReadUInt16();
-            var tag = ReadByte();
+            var tag = FastByte();
             var pos = Position;
 
             Position += length;
@@ -121,7 +121,7 @@ namespace Impostor.Hazel
 
             while (readMore)
             {
-                byte b = ReadByte();
+                byte b = FastByte();
                 if (b >= 0x80)
                 {
                     readMore = true;
index bf8dbcdcbe300c032044b8ef3303171fb5e00102..10f006515ebf431366da9887bfc369d0fe19d705 100644 (file)
@@ -1,9 +1,10 @@
-using System;
+using Impostor.Api.Games;
+using Impostor.Api.Net.Messages;
+
+using System;
 using System.Collections.Generic;
 using System.Net;
 using System.Text;
-using Impostor.Api.Games;
-using Impostor.Api.Net.Messages;
 
 namespace Impostor.Hazel
 {
@@ -16,7 +17,7 @@ namespace Impostor.Hazel
         public MessageType SendOption { get; private set; }
 
         private Stack<int> messageStarts = new Stack<int>();
-        
+
         public MessageWriter(byte[] buffer)
         {
             this.Buffer = buffer;
@@ -56,6 +57,8 @@ namespace Impostor.Hazel
                             System.Buffer.BlockCopy(this.Buffer, 1, output, 0, this.Length - 1);
                             return output;
                         }
+                    default:
+                        throw new ArgumentOutOfRangeException();
                 }
             }
 
@@ -122,6 +125,7 @@ namespace Impostor.Hazel
                 case MessageType.Unreliable:
                     this.Length = this.Position = 1;
                     break;
+
                 case MessageType.Reliable:
                     this.Length = this.Position = 3;
                     break;
@@ -235,7 +239,15 @@ namespace Impostor.Hazel
 
         public void Write(ReadOnlyMemory<byte> data)
         {
-            this.Write(data.ToArray()); // TODO: Fix memory allocation.
+            Write(data.Span);
+        }
+
+        public void Write(ReadOnlySpan<byte> bytes)
+        {
+            bytes.CopyTo(this.Buffer.AsSpan(this.Position, bytes.Length));
+
+            this.Position += bytes.Length;
+            if (this.Position > this.Length) this.Length = this.Position;
         }
 
         public void Write(byte[] bytes)
@@ -280,7 +292,8 @@ namespace Impostor.Hazel
                 value >>= 7;
             } while (value > 0);
         }
-        #endregion
+
+        #endregion WriteMethods
 
         public void Write(MessageWriter msg, bool includeHeader)
         {
@@ -292,6 +305,7 @@ namespace Impostor.Hazel
                     case MessageType.Unreliable:
                         offset = 1;
                         break;
+
                     case MessageType.Reliable:
                         offset = 3;
                         break;
@@ -319,4 +333,4 @@ namespace Impostor.Hazel
             Recycle();
         }
     }
-}
+}
\ No newline at end of file
index 084174eb6d3433b18bb83198d61beb54db0280e9..9518e48e9709d3046dcbf10db5cbdc05a025c2cf 100644 (file)
@@ -1,4 +1,4 @@
-<Project Sdk="Microsoft.NET.Sdk.Web">
+<Project Sdk="Microsoft.NET.Sdk.Web">
 
     <PropertyGroup>
         <TargetFramework>net5.0</TargetFramework>
@@ -9,4 +9,8 @@
       <ProjectReference Include="..\Impostor.Api\Impostor.Api.csproj" />
     </ItemGroup>
 
+    <ItemGroup>
+      <Folder Include="Properties\" />
+    </ItemGroup>
+
 </Project>
\ No newline at end of file
index e4a156b2dcd9a834540fc9dc74cd4a8b50b82a74..72253bafda9f30491764d436a785fa9782cfdb83 100644 (file)
@@ -25,6 +25,7 @@ namespace Impostor.Server.GameData.Objects.Components
 
         public override void Deserialize(IMessageReader reader, bool initialState)
         {
+            var votes = _votes;
             var unknown = reader.ReadBoolean();
             if (unknown)
             {
@@ -32,10 +33,10 @@ namespace Impostor.Server.GameData.Objects.Components
                 {
                     var v4 = reader.ReadInt32();
 
-                    if (!_votes.TryGetValue(v4, out var v12))
+                    if (!votes.TryGetValue(v4, out var v12))
                     {
                         v12 = new int[3];
-                        _votes[v4] = v12;
+                        votes[v4] = v12;
                     }
 
                     for (var i = 0; i < 3; i++)
index 93321e21b911a95b7a952d01150cd982b6f3b4f7..de981235159475eaea8e62b79daada87cb9c53bb 100644 (file)
@@ -1,4 +1,5 @@
 using Impostor.Api.Innersloth;
+
 using Xunit;
 
 namespace Impostor.Tests
@@ -10,17 +11,17 @@ namespace Impostor.Tests
         {
             const string code = "ABCD";
             const int codeInt = 0x44434241;
-            
+
             Assert.Equal(code, GameCodeParser.IntToGameName(codeInt));
             Assert.Equal(codeInt, GameCodeParser.GameNameToInt(code));
         }
-        
+
         [Fact]
         public void CodeV2()
         {
             const string code = "ABCDEF";
             const int codeInt = -1943683525;
-            
+
             Assert.Equal(code, GameCodeParser.IntToGameName(codeInt));
             Assert.Equal(codeInt, GameCodeParser.GameNameToInt(code));
         }
diff --git a/src/Impostor.Tests/Hazel/MessageWriterTests.cs b/src/Impostor.Tests/Hazel/MessageWriterTests.cs
new file mode 100644 (file)
index 0000000..83d67da
--- /dev/null
@@ -0,0 +1,38 @@
+using System;
+using System.Linq;
+using Impostor.Hazel;
+using Xunit;
+
+namespace Impostor.Tests.Hazel
+{
+    public class MessageWriterTests
+    {
+        [Fact]
+        public void ReadOnlyMemoryWriteWorksTheSameAsArray()
+        {
+            var oldVer = new MessageWriter(1024);
+            var newVer = new MessageWriter(1024);
+
+            var data = Enumerable.Repeat
+            (
+                Enumerable.Range(0, byte.MaxValue)
+                    .Select(x => (byte)x),
+                2
+            ).SelectMany(x => x).ToArray();
+
+            WriteSomeData(oldVer);
+            WriteSomeData(newVer);
+
+            oldVer.Write(data);
+            newVer.Write(data.AsMemory());
+
+            Assert.True(oldVer.Buffer.AsSpan().SequenceEqual(newVer.Buffer.AsSpan()));
+
+            static void WriteSomeData(MessageWriter oldVer)
+            {
+                oldVer.WritePacked(99);
+                oldVer.WritePacked(101);
+            }
+        }
+    }
+}
\ No newline at end of file