diff --git a/Projects/Benchmarks/Benchmarks/Collections/BenchmarkOrderedHashSet.cs b/Projects/Benchmarks/Benchmarks/Collections/BenchmarkOrderedHashSet.cs index 97a9adf66..1414c571e 100644 --- a/Projects/Benchmarks/Benchmarks/Collections/BenchmarkOrderedHashSet.cs +++ b/Projects/Benchmarks/Benchmarks/Collections/BenchmarkOrderedHashSet.cs @@ -9,51 +9,37 @@ namespace Benchmarks [SimpleJob(RuntimeMoniker.NetCoreApp50)] public class BenchmarkOrderedHashSet { - private List _list; - private OrderedHashSet _ordered; - private HashSet _hashSet; - private const int iterations = 16; + private readonly string[] _iterations = new string[16]; [IterationSetup] public void IterationSetup() { - _list = new List(); - _ordered = new OrderedHashSet(); - _hashSet = new HashSet(); - } - - [IterationCleanup] - public void IterationCleanup() - { - _list = null; - _ordered = null; - _hashSet = null; + for (var i = 0; i < _iterations.Length; i++) + { + _iterations[i] = i.ToString(); + } } [Benchmark] public int UsingList() { - for (int i = 0; i < iterations / 2; i++) + var list = new List(); + for (int i = 0; i < _iterations.Length / 2; i++) { - AddIfNotPresent(_list, i.ToString()); + AddIfNotPresent(list, _iterations[i]); } - for (int i = 0; i < iterations; i++) + for (int i = 0; i < _iterations.Length; i++) { - AddIfNotPresent(_list, i.ToString()); + AddIfNotPresent(list, _iterations[i]); } - for (int i = 0; i < iterations * 2; i++) + for (int i = 0; i < list.Count; i++) { - AddIfNotPresent(_list, i.ToString()); + list[i].ToString(); } - for (int i = 0; i < _list.Count; i++) - { - _list[i].ToString(); - } - - return _list.Count; + return list.Count; } private static int AddIfNotPresent(List list, T item) @@ -71,53 +57,74 @@ namespace Benchmarks [Benchmark] public int UsingOrderedHashSet() { - for (int i = 0; i < iterations / 2; i++) + var ordered = new OrderedHashSet(); + for (int i = 0; i < _iterations.Length / 2; i++) { - _ordered.GetOrAdd(i.ToString()); + ordered.GetOrAdd(_iterations[i]).ToString(); } - for (int i = 0; i < iterations; i++) + for (int i = 0; i < _iterations.Length; i++) { - _ordered.GetOrAdd(i.ToString()); + ordered.GetOrAdd(_iterations[i]).ToString(); } - for (int i = 0; i < iterations * 2; i++) - { - _ordered.GetOrAdd(i.ToString()); - } - - foreach (var str in _ordered) + foreach (var str in ordered) { str.ToString(); } - return _ordered.Count; + return ordered.Count; + } + + [Benchmark] + public int UsingPooledOrderedHashSet() + { + var ordered = new PooledOrderedHashSet(); + for (int i = 0; i < _iterations.Length / 2; i++) + { + ordered.GetOrAdd(_iterations[i]).ToString(); + } + + for (int i = 0; i < _iterations.Length; i++) + { + ordered.GetOrAdd(_iterations[i]).ToString(); + } + + foreach (var str in ordered) + { + str.ToString(); + } + + return ordered.Count; } [Benchmark] public int UsingHashSet() { - for (int i = 0; i < iterations / 2; i++) + var hashSet = new HashSet<(string, int)>(new OrderedStringComparer()); + for (int i = 0; i < _iterations.Length / 2; i++) { - _hashSet.Add(i.ToString()); + hashSet.Add((_iterations[i], i)); } - for (int i = 0; i < iterations; i++) + for (int i = 0; i < _iterations.Length; i++) { - _hashSet.Add(i.ToString()); + hashSet.Add((_iterations[i], i)); } - for (int i = 0; i < iterations * 2; i++) - { - _hashSet.Add(i.ToString()); - } - - foreach (var str in _hashSet) + foreach (var str in hashSet) { str.ToString(); } - return _hashSet.Count; + return hashSet.Count; + } + + private class OrderedStringComparer : EqualityComparer<(string, int)> + { + public override bool Equals((string, int) x, (string, int) y) => x.Item1.Equals(y.Item1, System.StringComparison.Ordinal); + + public override int GetHashCode((string, int) obj) => obj.Item1.GetHashCode(); } } } diff --git a/Projects/Benchmarks/Benchmarks/Packets/BenchmarkOutgoingGumpPacket.cs b/Projects/Benchmarks/Benchmarks/Packets/BenchmarkOutgoingGumpPacket.cs new file mode 100644 index 000000000..0225af652 --- /dev/null +++ b/Projects/Benchmarks/Benchmarks/Packets/BenchmarkOutgoingGumpPacket.cs @@ -0,0 +1,173 @@ +using System; +using System.Buffers; +using System.IO; +using System.IO.Compression; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Jobs; +using Server.Collections; +using Server.Gumps; +using Server.Network; +using Server.Tests.Network; + +namespace Benchmarks +{ + [MemoryDiagnoser] + [SimpleJob(RuntimeMoniker.NetCoreApp50)] + public class OutgoingGumpPacketBenchmarks + { + private static readonly byte[] _layoutBuffer = GC.AllocateUninitializedArray(0x20000); + private static readonly byte[] _stringsBuffer = GC.AllocateUninitializedArray(0x20000); + + public static void CreateDisplayGump(Gump gump, out int switches, out int entries) + { + switches = 0; + entries = 0; + + const bool packed = false; + + var layoutWriter = new SpanWriter(_layoutBuffer); + + if (!gump.Draggable) + { + layoutWriter.Write(Gump.NoMove); + } + + if (!gump.Closable) + { + layoutWriter.Write(Gump.NoClose); + } + + if (!gump.Disposable) + { + layoutWriter.Write(Gump.NoDispose); + } + + if (!gump.Resizable) + { + layoutWriter.Write(Gump.NoResize); + } + + var stringsList = new OrderedHashSet(32); + + foreach (var entry in gump.Entries) + { + entry.AppendTo(ref layoutWriter, stringsList, ref entries, ref switches); + } + + var stringsWriter = new SpanWriter(_stringsBuffer); + + foreach (var str in stringsList) + { + var s = str ?? ""; + stringsWriter.Write((ushort)s.Length); + stringsWriter.WriteBigUni(s); + } + + int maxLength; + if (packed) + { + var worstLayoutLength = Zlib.MaxPackSize(layoutWriter.BytesWritten); + var worstStringsLength = Zlib.MaxPackSize(stringsWriter.BytesWritten); + maxLength = 40 + worstLayoutLength + worstStringsLength; + } + else + { + maxLength = 23 + layoutWriter.BytesWritten + stringsWriter.BytesWritten; + } + + var writer = new SpanWriter(maxLength); + writer.Write((byte)(packed ? 0xDD : 0xB0)); // Packet ID + writer.Seek(2, SeekOrigin.Current); + + writer.Write(gump.Serial); + writer.Write(gump.TypeID); + writer.Write(gump.X); + writer.Write(gump.Y); + + if (packed) + { + layoutWriter.Write((byte)0); // Layout text terminator + OutgoingGumpPackets.WritePacked(layoutWriter.Span, ref writer); + + writer.Write(stringsList.Count); + OutgoingGumpPackets.WritePacked(stringsWriter.Span, ref writer); + } + else + { + writer.Write((ushort)layoutWriter.BytesWritten); + writer.Write(layoutWriter.Span); + + writer.Write((ushort)stringsList.Count); + writer.Write(stringsWriter.Span); + } + + writer.WritePacketLength(); + + layoutWriter.Dispose(); // Just in case + stringsWriter.Dispose(); // Just in case + } + + public class NameChangeDeedGump : Gump + { + public NameChangeDeedGump() : base(50, 50) + { + Closable = false; + Draggable = false; + Resizable = false; + + AddPage(0); + + AddBlackAlpha(10, 120, 250, 85); + AddHtml(10, 125, 250, 20, Color(Center("Name Change Deed"), 0xFFFFFF)); + + AddLabel(73, 15, 1152, ""); + AddLabel(20, 150, 0x480, "New Name:"); + AddTextField(100, 150, 150, 20, 0); + + AddButtonLabeled(75, 180, 1, "Submit"); + } + + public void AddBlackAlpha(int x, int y, int width, int height) + { + AddImageTiled(x, y, width, height, 2624); + AddAlphaRegion(x, y, width, height); + } + + public void AddTextField(int x, int y, int width, int height, int index) + { + AddBackground(x - 2, y - 2, width + 4, height + 4, 0x2486); + AddTextEntry(x + 2, y + 2, width - 4, height - 4, 0, index, ""); + } + + public static string Center(string text) => $"
{text}
"; + + public static string Color(string text, int color) => $"{text}"; + + public void AddButtonLabeled(int x, int y, int buttonID, string text) + { + AddButton(x, y - 1, 4005, 4007, buttonID); + AddHtml(x + 35, y, 240, 20, Color(text, 0xFFFFFF)); + } + } + + private static Gump _gump; + + [GlobalSetup] + public void Setup() + { + _gump = new NameChangeDeedGump(); + } + + [Benchmark] + public void TestNewStack() + { + CreateDisplayGump(_gump, out var _, out var _); + } + + [Benchmark] + public void TestOldStack() + { + _gump.Compile().Compile(false, out var _); + } + } +} diff --git a/Projects/Benchmarks/Benchmarks/Packets/GumpPackets.cs b/Projects/Benchmarks/Benchmarks/Packets/GumpPackets.cs new file mode 100644 index 000000000..be305fa48 --- /dev/null +++ b/Projects/Benchmarks/Benchmarks/Packets/GumpPackets.cs @@ -0,0 +1,310 @@ +using System.Buffers; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Text; +using Server.Gumps; +using Server.Network; + +namespace Server.Tests +{ + public interface IGumpWriter + { + int TextEntries { get; set; } + int Switches { get; set; } + + void AppendLayout(bool val); + void AppendLayout(int val); + void AppendLayout(uint val); + void AppendLayoutNS(int val); + void AppendLayout(string text); + void AppendLayoutNS(string text); + void AppendLayout(byte[] buffer); + void WriteStrings(List strings); + void Flush(); + } + + public sealed class CloseGump : Packet + { + public CloseGump(int typeID, int buttonID) : base(0xBF) + { + EnsureCapacity(13); + + Stream.Write((short)0x04); + Stream.Write(typeID); + Stream.Write(buttonID); + } + } + + public sealed class DisplayGumpPacked : Packet, IGumpWriter + { + private static readonly byte[] m_True = Gump.StringToBuffer(" 1"); + private static readonly byte[] m_False = Gump.StringToBuffer(" 0"); + + private static readonly byte[] m_BeginTextSeparator = Gump.StringToBuffer(" @"); + private static readonly byte[] m_EndTextSeparator = Gump.StringToBuffer("@"); + + private static readonly byte[] m_Buffer = new byte[48]; + + private readonly Gump m_Gump; + + private readonly PacketWriter m_Layout; + private readonly PacketWriter m_Strings; + + private int m_StringCount; + + static DisplayGumpPacked() => m_Buffer[0] = (byte)' '; + + public DisplayGumpPacked(Gump gump) + : base(0xDD) + { + m_Gump = gump; + + m_Layout = PacketWriter.CreateInstance(8192); + m_Strings = PacketWriter.CreateInstance(8192); + } + + public int TextEntries { get; set; } + + public int Switches { get; set; } + + public void AppendLayout(bool val) + { + AppendLayout(val ? m_True : m_False); + } + + public void AppendLayout(int val) + { + var toString = val.ToString(); + var bytes = Encoding.ASCII.GetBytes(toString, 0, toString.Length, m_Buffer, 1) + 1; + + m_Layout.Write(m_Buffer, 0, bytes); + } + + public void AppendLayout(uint val) + { + var toString = val.ToString(); + var bytes = Encoding.ASCII.GetBytes(toString, 0, toString.Length, m_Buffer, 1) + 1; + + m_Layout.Write(m_Buffer, 0, bytes); + } + + public void AppendLayoutNS(int val) + { + var toString = val.ToString(); + var bytes = Encoding.ASCII.GetBytes(toString, 0, toString.Length, m_Buffer, 1); + + m_Layout.Write(m_Buffer, 1, bytes); + } + + public void AppendLayoutNS(string text) + { + m_Layout.WriteAsciiFixed(text, text.Length); + } + + public void AppendLayout(string text) + { + AppendLayout(m_BeginTextSeparator); + + m_Layout.WriteAsciiFixed(text, text.Length); + + AppendLayout(m_EndTextSeparator); + } + + public void AppendLayout(byte[] buffer) + { + m_Layout.Write(buffer, 0, buffer.Length); + } + + public void WriteStrings(List strings) + { + m_StringCount = strings.Count; + + for (var i = 0; i < strings.Count; ++i) + { + var v = strings[i] ?? ""; + + m_Strings.Write((ushort)v.Length); + m_Strings.WriteBigUniFixed(v, v.Length); + } + } + + public void Flush() + { + EnsureCapacity(28 + (int)m_Layout.Length + (int)m_Strings.Length); + + Stream.Write(m_Gump.Serial); + Stream.Write(m_Gump.TypeID); + Stream.Write(m_Gump.X); + Stream.Write(m_Gump.Y); + + // Note: layout MUST be null terminated (don't listen to krrios) + m_Layout.Write((byte)0); + + WritePacked(m_Layout); + + Stream.Write(m_StringCount); + + WritePacked(m_Strings); + + PacketWriter.ReleaseInstance(m_Layout); + PacketWriter.ReleaseInstance(m_Strings); + } + + private void WritePacked(PacketWriter src) + { + var buffer = src.UnderlyingStream.GetBuffer(); + var length = (int)src.Length; + + if (length == 0) + { + Stream.Write(0); + return; + } + + var wantLength = 1 + length * 1024 / 1000; + + wantLength += 4095; + wantLength &= ~4095; + + var packBuffer = ArrayPool.Shared.Rent(wantLength); + + var packLength = wantLength; + + Zlib.Pack(packBuffer, ref packLength, buffer, length, ZlibQuality.Default); + + Stream.Write(4 + packLength); + Stream.Write(length); + Stream.Write(packBuffer, 0, packLength); + + ArrayPool.Shared.Return(packBuffer); + } + } + + public sealed class DisplayGumpFast : Packet, IGumpWriter + { + private static readonly byte[] m_True = Gump.StringToBuffer(" 1"); + private static readonly byte[] m_False = Gump.StringToBuffer(" 0"); + + private static readonly byte[] m_BeginTextSeparator = Gump.StringToBuffer(" @"); + private static readonly byte[] m_EndTextSeparator = Gump.StringToBuffer("@"); + + private readonly byte[] m_Buffer = new byte[48]; + private int m_LayoutLength; + + public DisplayGumpFast(Gump g) : base(0xB0) + { + m_Buffer[0] = (byte)' '; + + EnsureCapacity(4096); + + Stream.Write(g.Serial); + Stream.Write(g.TypeID); + Stream.Write(g.X); + Stream.Write(g.Y); + Stream.Write((ushort)0xFFFF); + } + + public int TextEntries { get; set; } + + public int Switches { get; set; } + + public void AppendLayout(bool val) + { + AppendLayout(val ? m_True : m_False); + } + + public void AppendLayout(int val) + { + var toString = val.ToString(); + var bytes = Encoding.ASCII.GetBytes(toString, 0, toString.Length, m_Buffer, 1) + 1; + + Stream.Write(m_Buffer, 0, bytes); + m_LayoutLength += bytes; + } + + public void AppendLayout(uint val) + { + var toString = val.ToString(); + var bytes = Encoding.ASCII.GetBytes(toString, 0, toString.Length, m_Buffer, 1) + 1; + + Stream.Write(m_Buffer, 0, bytes); + m_LayoutLength += bytes; + } + + public void AppendLayoutNS(int val) + { + var toString = val.ToString(); + var bytes = Encoding.ASCII.GetBytes(toString, 0, toString.Length, m_Buffer, 1); + + Stream.Write(m_Buffer, 1, bytes); + m_LayoutLength += bytes; + } + + public void AppendLayoutNS(string text) + { + var length = text.Length; + Stream.WriteAsciiFixed(text, length); + m_LayoutLength += length; + } + + public void AppendLayout(string text) + { + AppendLayout(m_BeginTextSeparator); + + var length = text.Length; + Stream.WriteAsciiFixed(text, length); + m_LayoutLength += length; + + AppendLayout(m_EndTextSeparator); + } + + public void AppendLayout(byte[] buffer) + { + var length = buffer.Length; + Stream.Write(buffer, 0, length); + m_LayoutLength += length; + } + + public void WriteStrings(List text) + { + Stream.Seek(19, SeekOrigin.Begin); + Stream.Write((ushort)m_LayoutLength); + Stream.Seek(0, SeekOrigin.End); + + Stream.Write((ushort)text.Count); + + for (var i = 0; i < text.Count; ++i) + { + var v = text[i] ?? ""; + + int length = (ushort)v.Length; + + Stream.Write((ushort)length); + Stream.WriteBigUniFixed(v, length); + } + } + + public void Flush() + { + } + } + + public sealed class DisplaySignGump : Packet + { + public DisplaySignGump(Serial serial, int gumpID, string unknown, string caption) : base(0x8B) + { + unknown ??= ""; + caption ??= ""; + + EnsureCapacity(15 + unknown.Length + caption.Length); + + Stream.Write(serial); + Stream.Write((short)gumpID); + Stream.Write((short)(unknown.Length + 1)); + Stream.WriteAsciiNull(unknown); + Stream.Write((short)(caption.Length + 1)); + Stream.WriteAsciiNull(caption); + } + } +} diff --git a/Projects/Benchmarks/Benchmarks/Packets/GumpUtilities.cs b/Projects/Benchmarks/Benchmarks/Packets/GumpUtilities.cs new file mode 100644 index 000000000..5df0a12fe --- /dev/null +++ b/Projects/Benchmarks/Benchmarks/Packets/GumpUtilities.cs @@ -0,0 +1,476 @@ +using System.Collections.Generic; +using Server.Gumps; +using Server.Network; + +namespace Server.Tests.Network +{ + public static class GumpUtilities + { + private static readonly byte[] m_BeginLayout = Gump.StringToBuffer("{ "); + private static readonly byte[] m_EndLayout = Gump.StringToBuffer(" }"); + + public static Packet Compile(this Gump g, NetState ns = null) + { + IGumpWriter disp = new DisplayGumpFast(g); + // IGumpWriter disp = new DisplayGumpPacked(g); + + if (!g.Draggable) + { + disp.AppendLayout(Gump.NoMove); + } + + if (!g.Closable) + { + disp.AppendLayout(Gump.NoClose); + } + + if (!g.Disposable) + { + disp.AppendLayout(Gump.NoDispose); + } + + if (!g.Resizable) + { + disp.AppendLayout(Gump.NoResize); + } + + var count = g.Entries.Count; + var strings = new List(); + + for (var i = 0; i < count; ++i) + { + var e = g.Entries[i]; + + disp.AppendLayout(m_BeginLayout); + e.AppendToByType(disp, strings); + disp.AppendLayout(m_EndLayout); + } + + disp.WriteStrings(strings); + + disp.Flush(); + + return (Packet)disp; + } + + public static int Intern(this List strings, string value) + { + var indexOf = strings.IndexOf(value); + + if (indexOf >= 0) + { + return indexOf; + } + + strings.Add(value); + return strings.Count - 1; + } + + public static void AppendToByType(this GumpEntry e, IGumpWriter disp, List strings) + { + switch (e) + { + case GumpAlphaRegion g: + { + g.AppendTo(disp, strings); + break; + } + case GumpBackground g: + { + g.AppendTo(disp, strings); + break; + } + case GumpButton g: + { + g.AppendTo(disp, strings); + break; + } + case GumpCheck g: + { + g.AppendTo(disp, strings); + break; + } + case GumpGroup g: + { + g.AppendTo(disp, strings); + break; + } + case GumpECHandleInput g: + { + g.AppendTo(disp, strings); + break; + } + case GumpHtml g: + { + g.AppendTo(disp, strings); + break; + } + case GumpHtmlLocalized g: + { + g.AppendTo(disp, strings); + break; + } + case GumpImage g: + { + g.AppendTo(disp, strings); + break; + } + case GumpImageTileButton g: + { + g.AppendTo(disp, strings); + break; + } + case GumpImageTiled g: + { + g.AppendTo(disp, strings); + break; + } + case GumpItem g: + { + g.AppendTo(disp, strings); + break; + } + case GumpItemProperty g: + { + g.AppendTo(disp, strings); + break; + } + case GumpLabel g: + { + g.AppendTo(disp, strings); + break; + } + case GumpLabelCropped g: + { + g.AppendTo(disp, strings); + break; + } + case GumpMasterGump g: + { + g.AppendTo(disp, strings); + break; + } + case GumpPage g: + { + g.AppendTo(disp, strings); + break; + } + case GumpRadio g: + { + g.AppendTo(disp, strings); + break; + } + case GumpSpriteImage g: + { + g.AppendTo(disp, strings); + break; + } + case GumpTextEntry g: + { + g.AppendTo(disp, strings); + break; + } + case GumpTextEntryLimited g: + { + g.AppendTo(disp, strings); + break; + } + case GumpTooltip g: + { + g.AppendTo(disp, strings); + break; + } + } + } + + public static void AppendTo(this GumpAlphaRegion g, IGumpWriter disp, List strings) + { + disp.AppendLayout(GumpAlphaRegion.LayoutName); + disp.AppendLayout(g.X); + disp.AppendLayout(g.Y); + disp.AppendLayout(g.Width); + disp.AppendLayout(g.Height); + } + + public static void AppendTo(this GumpBackground g, IGumpWriter disp, List strings) + { + disp.AppendLayout(GumpBackground.LayoutName); + disp.AppendLayout(g.X); + disp.AppendLayout(g.Y); + disp.AppendLayout(g.GumpID); + disp.AppendLayout(g.Width); + disp.AppendLayout(g.Height); + } + + public static void AppendTo(this GumpButton g, IGumpWriter disp, List strings) + { + disp.AppendLayout(GumpButton.LayoutName); + disp.AppendLayout(g.X); + disp.AppendLayout(g.Y); + disp.AppendLayout(g.NormalID); + disp.AppendLayout(g.PressedID); + disp.AppendLayout((int)g.Type); + disp.AppendLayout(g.Param); + disp.AppendLayout(g.ButtonID); + } + + public static void AppendTo(this GumpCheck g, IGumpWriter disp, List strings) + { + disp.AppendLayout(GumpButton.LayoutName); + disp.AppendLayout(g.X); + disp.AppendLayout(g.Y); + disp.AppendLayout(g.InactiveID); + disp.AppendLayout(g.ActiveID); + disp.AppendLayout(g.InitialState); + disp.AppendLayout(g.SwitchID); + + disp.Switches++; + } + + public static void AppendTo(this GumpGroup g, IGumpWriter disp, List strings) + { + disp.AppendLayout(GumpGroup.LayoutName); + disp.AppendLayout(g.Group); + } + + public static void AppendTo(this GumpECHandleInput g, IGumpWriter disp, List strings) + { + disp.AppendLayout(GumpECHandleInput.LayoutName); + } + + public static void AppendTo(this GumpHtml g, IGumpWriter disp, List strings) + { + disp.AppendLayout(GumpHtml.LayoutName); + disp.AppendLayout(g.X); + disp.AppendLayout(g.Y); + disp.AppendLayout(g.Width); + disp.AppendLayout(g.Height); + disp.AppendLayout(strings.Intern(g.Text)); + disp.AppendLayout(g.Background); + disp.AppendLayout(g.Scrollbar); + } + + public static void AppendTo(this GumpHtmlLocalized g, IGumpWriter disp, List strings) + { + switch (g.Type) + { + case GumpHtmlLocalizedType.Plain: + { + disp.AppendLayout(GumpHtmlLocalized.LayoutNamePlain); + + disp.AppendLayout(g.X); + disp.AppendLayout(g.Y); + disp.AppendLayout(g.Width); + disp.AppendLayout(g.Height); + disp.AppendLayout(g.Number); + disp.AppendLayout(g.Background); + disp.AppendLayout(g.Scrollbar); + + break; + } + + case GumpHtmlLocalizedType.Color: + { + disp.AppendLayout(GumpHtmlLocalized.LayoutNameColor); + + disp.AppendLayout(g.X); + disp.AppendLayout(g.Y); + disp.AppendLayout(g.Width); + disp.AppendLayout(g.Height); + disp.AppendLayout(g.Number); + disp.AppendLayout(g.Background); + disp.AppendLayout(g.Scrollbar); + disp.AppendLayout(g.Color); + + break; + } + + case GumpHtmlLocalizedType.Args: + { + disp.AppendLayout(GumpHtmlLocalized.LayoutNameArgs); + + disp.AppendLayout(g.X); + disp.AppendLayout(g.Y); + disp.AppendLayout(g.Width); + disp.AppendLayout(g.Height); + disp.AppendLayout(g.Background); + disp.AppendLayout(g.Scrollbar); + disp.AppendLayout(g.Color); + disp.AppendLayout(g.Number); + disp.AppendLayout(g.Args); + + break; + } + } + } + + public static void AppendTo(this GumpImage g, IGumpWriter disp, List strings) + { + disp.AppendLayout(GumpImage.LayoutName); + disp.AppendLayout(g.X); + disp.AppendLayout(g.Y); + disp.AppendLayout(g.GumpID); + + if (g.Hue != 0) + { + disp.AppendLayout(GumpImage.HueEquals); + disp.AppendLayoutNS(g.Hue); + } + + if (!string.IsNullOrEmpty(g.Class)) + { + disp.AppendLayout(GumpImage.ClassEquals); + disp.AppendLayoutNS(g.Class); + } + } + + public static void AppendTo(this GumpImageTileButton g, IGumpWriter disp, List strings) + { + disp.AppendLayout(GumpImageTileButton.LayoutName); + disp.AppendLayout(g.X); + disp.AppendLayout(g.Y); + disp.AppendLayout(g.NormalID); + disp.AppendLayout(g.PressedID); + disp.AppendLayout((int)g.Type); + disp.AppendLayout(g.Param); + disp.AppendLayout(g.ButtonID); + + disp.AppendLayout(g.ItemID); + disp.AppendLayout(g.Hue); + disp.AppendLayout(g.Width); + disp.AppendLayout(g.Height); + + if (g.LocalizedTooltip > 0) + { + disp.AppendLayout(GumpImageTileButton.LayoutTooltip); + disp.AppendLayout(g.LocalizedTooltip); + } + } + + public static void AppendTo(this GumpImageTiled g, IGumpWriter disp, List strings) + { + disp.AppendLayout(GumpImageTiled.LayoutName); + disp.AppendLayout(g.X); + disp.AppendLayout(g.Y); + disp.AppendLayout(g.Width); + disp.AppendLayout(g.Height); + disp.AppendLayout(g.GumpID); + } + + public static void AppendTo(this GumpItem g, IGumpWriter disp, List strings) + { + disp.AppendLayout(g.Hue == 0 ? GumpItem.LayoutName : GumpItem.LayoutNameHue); + disp.AppendLayout(g.X); + disp.AppendLayout(g.Y); + disp.AppendLayout(g.ItemID); + + if (g.Hue != 0) + { + disp.AppendLayout(g.Hue); + } + } + + public static void AppendTo(this GumpItemProperty g, IGumpWriter disp, List strings) + { + disp.AppendLayout(GumpItemProperty.LayoutName); + disp.AppendLayout(g.Serial); + } + + public static void AppendTo(this GumpLabel g, IGumpWriter disp, List strings) + { + disp.AppendLayout(GumpLabel.LayoutName); + disp.AppendLayout(g.X); + disp.AppendLayout(g.Y); + disp.AppendLayout(g.Hue); + disp.AppendLayout(strings.Intern(g.Text)); + } + + public static void AppendTo(this GumpLabelCropped g, IGumpWriter disp, List strings) + { + disp.AppendLayout(GumpLabelCropped.LayoutName); + disp.AppendLayout(g.X); + disp.AppendLayout(g.Y); + disp.AppendLayout(g.Width); + disp.AppendLayout(g.Height); + disp.AppendLayout(g.Hue); + disp.AppendLayout(strings.Intern(g.Text)); + } + + public static void AppendTo(this GumpMasterGump g, IGumpWriter disp, List strings) + { + disp.AppendLayout(GumpMasterGump.LayoutName); + disp.AppendLayout(g.GumpID); + } + + public static void AppendTo(this GumpPage g, IGumpWriter disp, List strings) + { + disp.AppendLayout(GumpPage.LayoutName); + disp.AppendLayout(g.Page); + } + + public static void AppendTo(this GumpRadio g, IGumpWriter disp, List strings) + { + disp.AppendLayout(GumpRadio.LayoutName); + disp.AppendLayout(g.X); + disp.AppendLayout(g.Y); + disp.AppendLayout(g.InactiveID); + disp.AppendLayout(g.ActiveID); + disp.AppendLayout(g.InitialState); + disp.AppendLayout(g.SwitchID); + + disp.Switches++; + } + + public static void AppendTo(this GumpSpriteImage g, IGumpWriter disp, List strings) + { + disp.AppendLayout(GumpSpriteImage.LayoutName); + disp.AppendLayout(g.X); + disp.AppendLayout(g.Y); + disp.AppendLayout(g.GumpID); + disp.AppendLayout(g.Width); + disp.AppendLayout(g.Height); + disp.AppendLayout(g.SX); + disp.AppendLayout(g.SY); + } + + public static void AppendTo(this GumpTextEntry g, IGumpWriter disp, List strings) + { + disp.AppendLayout(GumpTextEntry.LayoutName); + disp.AppendLayout(g.X); + disp.AppendLayout(g.Y); + disp.AppendLayout(g.Width); + disp.AppendLayout(g.Height); + disp.AppendLayout(g.Hue); + disp.AppendLayout(g.EntryID); + disp.AppendLayout(strings.Intern(g.InitialText)); + + disp.TextEntries++; + } + + public static void AppendTo(this GumpTextEntryLimited g, IGumpWriter disp, List strings) + { + disp.AppendLayout(GumpTextEntryLimited.LayoutName); + disp.AppendLayout(g.X); + disp.AppendLayout(g.Y); + disp.AppendLayout(g.Width); + disp.AppendLayout(g.Height); + disp.AppendLayout(g.Hue); + disp.AppendLayout(g.EntryID); + disp.AppendLayout(strings.Intern(g.InitialText)); + disp.AppendLayout(g.Size); + + disp.TextEntries++; + } + + public static void AppendTo(this GumpTooltip g, IGumpWriter disp, List strings) + { + disp.AppendLayout(GumpTooltip.LayoutName); + disp.AppendLayout(g.Number); + + if (!string.IsNullOrEmpty(g.Args)) + { + disp.AppendLayout(g.Args); + } + } + } +} diff --git a/Projects/Benchmarks/Benchmarks/Packets/Packet.cs b/Projects/Benchmarks/Benchmarks/Packets/Packet.cs new file mode 100644 index 000000000..7b634fcce --- /dev/null +++ b/Projects/Benchmarks/Benchmarks/Packets/Packet.cs @@ -0,0 +1,265 @@ +using System; +using System.Buffers; +using System.Diagnostics; +using System.IO; +using Server.Diagnostics; + +namespace Server.Network +{ + public abstract class Packet + { + private const int CompressorBufferSize = 0x10000; + + private readonly int m_Length; + + private byte[] m_CompiledBuffer; + private int m_CompiledLength; + private State m_State; + + protected Packet(int packetID) + { + PacketID = packetID; + + if (Core.Profiling) + { + var prof = PacketSendProfile.Acquire(PacketID); + prof.Increment(); + } + } + + protected Packet(int packetID, int length) + { + PacketID = packetID; + m_Length = length; + + Stream = PacketWriter.CreateInstance(length); // new PacketWriter( length ); + Stream.Write((byte)packetID); + + if (Core.Profiling) + { + var prof = PacketSendProfile.Acquire(PacketID); + prof.Increment(); + } + } + + public int PacketID { get; } + + public PacketWriter Stream { get; protected set; } + + public void EnsureCapacity(int length) + { + Stream = PacketWriter.CreateInstance(length); // new PacketWriter( length ); + Stream.Write((byte)PacketID); + Stream.Write((short)0); + } + + public static Packet SetStatic(Packet p) + { + p.SetStatic(); + return p; + } + + public static Packet Acquire(Packet p) + { + p.Acquire(); + return p; + } + + public static void Release(ref Packet p) + { + p?.Release(); + p = null; + } + + public static void Release(Packet p) + { + p?.Release(); + } + + public void SetStatic() + { + m_State |= State.Static | State.Acquired; + } + + public void Acquire() + { + m_State |= State.Acquired; + } + + public void OnSend() + { + if ((m_State & (State.Acquired | State.Static)) == 0) + { + Free(); + } + } + + private void Free() + { + if (m_CompiledBuffer == null) + { + return; + } + + if ((m_State & State.Buffered) != 0) + { + ArrayPool.Shared.Return(m_CompiledBuffer); + } + + m_State &= ~(State.Static | State.Acquired | State.Buffered); + + m_CompiledBuffer = null; + } + + public void Release() + { + if ((m_State & State.Acquired) != 0) + { + Free(); + } + } + + private readonly object _object = new(); + + public byte[] Compile(bool compress, out int length) + { + lock (_object) + { + if (m_CompiledBuffer == null) + { + if ((m_State & State.Accessed) == 0) + { + m_State |= State.Accessed; + } + else + { + if ((m_State & State.Warned) == 0) + { + m_State |= State.Warned; + + try + { + using var op = new StreamWriter("net_opt.log", true); + op.WriteLine("Redundant compile for packet {0}, use Acquire() and Release()", GetType()); + op.WriteLine(new StackTrace()); + } + catch + { + // ignored + } + } + + m_CompiledBuffer = Array.Empty(); + m_CompiledLength = 0; + + length = m_CompiledLength; + return m_CompiledBuffer; + } + + InternalCompile(compress); + } + + length = m_CompiledLength; + return m_CompiledBuffer; + } + } + + private void InternalCompile(bool compress) + { + if (m_Length == 0) + { + var streamLen = Stream.Length; + + Stream.Seek(1, SeekOrigin.Begin); + Stream.Write((ushort)streamLen); + } + else if (Stream.Length != m_Length) + { + var diff = (int)Stream.Length - m_Length; + + Console.WriteLine( + "Packet: 0x{0:X2}: Bad packet length! ({1}{2} bytes)", + PacketID, + diff >= 0 ? "+" : "", + diff + ); + } + + var ms = Stream.UnderlyingStream; + + m_CompiledBuffer = ms.GetBuffer(); + var length = (int)ms.Length; + + if (compress) + { + var compressorBuffer = new byte[CompressorBufferSize]; + var compressedLength = NetworkCompression.Compress(m_CompiledBuffer.AsSpan(0, length), compressorBuffer); + + if (length <= 0) + { + Console.WriteLine( + "Warning: Compression buffer overflowed on packet 0x{0:X2} ('{1}') (length={2})", + PacketID, + GetType().Name, + length + ); + using var op = new StreamWriter("compression_overflow.log", true); + op.WriteLine( + "{0} Warning: Compression buffer overflowed on packet 0x{1:X2} ('{2}') (length={3})", + Core.Now, + PacketID, + GetType().Name, + length + ); + op.WriteLine(new StackTrace()); + } + else + { + m_CompiledBuffer = compressorBuffer; + m_CompiledLength = compressedLength; + } + } + else + { + m_CompiledLength = length; + } + + if (m_CompiledLength > 0) + { + var old = m_CompiledBuffer; + + if ((m_State & State.Static) != 0) + { + m_CompiledBuffer = new byte[m_CompiledLength]; + } + else + { + // Release it later using Release() + m_CompiledBuffer = ArrayPool.Shared.Rent(m_CompiledLength); + m_State |= State.Buffered; + } + + Buffer.BlockCopy(old, 0, m_CompiledBuffer, 0, m_CompiledLength); + + if (compress) + { + ArrayPool.Shared.Return(old); + } + } + + PacketWriter.ReleaseInstance(Stream); + Stream = null; + } + + [Flags] + private enum State + { + Inactive = 0x00, + Static = 0x01, + Acquired = 0x02, + Accessed = 0x04, + Buffered = 0x08, + Warned = 0x10 + } + } +} diff --git a/Projects/Benchmarks/Benchmarks/Packets/PacketTestUtilities.cs b/Projects/Benchmarks/Benchmarks/Packets/PacketTestUtilities.cs index a782423ee..757495040 100644 --- a/Projects/Benchmarks/Benchmarks/Packets/PacketTestUtilities.cs +++ b/Projects/Benchmarks/Benchmarks/Packets/PacketTestUtilities.cs @@ -2,11 +2,15 @@ using System; using System.Buffers.Binary; using System.Runtime.CompilerServices; using Server; +using Server.Network; namespace Benchmarks { public static class PacketTestUtilities { + public static Span Compile(this Packet p) => + p.Compile(false, out var length).AsSpan(0, length); + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Write(this Span data, ref int pos, Serial serial) { diff --git a/Projects/Benchmarks/Benchmarks/Packets/PacketWriter.cs b/Projects/Benchmarks/Benchmarks/Packets/PacketWriter.cs new file mode 100644 index 000000000..bd85aa27c --- /dev/null +++ b/Projects/Benchmarks/Benchmarks/Packets/PacketWriter.cs @@ -0,0 +1,352 @@ +using System; +using System.Collections.Concurrent; +using System.IO; +using System.Text; + +namespace Server.Network +{ + /// + /// Provides functionality for writing primitive binary data. + /// + public class PacketWriter + { + private static readonly ConcurrentQueue m_Pool = new(); + + /// + /// Internal format buffer. + /// + private readonly byte[] m_Buffer = new byte[4]; + + private int m_Capacity; + + /// + /// Instantiates a new PacketWriter instance with a given capacity. + /// + /// Initial capacity for the internal stream. + public PacketWriter(int capacity = 32) + { + UnderlyingStream = new MemoryStream(capacity); + m_Capacity = capacity; + } + + /// + /// Gets the total stream length. + /// + public long Length => UnderlyingStream.Length; + + /// + /// Gets or sets the current stream position. + /// + public long Position + { + get => UnderlyingStream.Position; + set => UnderlyingStream.Position = value; + } + + /// + /// The internal stream used by this PacketWriter instance. + /// + public MemoryStream UnderlyingStream { get; } + + public static PacketWriter CreateInstance(int capacity = 32) + { + if (m_Pool.TryDequeue(out var pw)) + { + pw.m_Capacity = capacity; + pw.UnderlyingStream.SetLength(0); + return pw; + } + + return new PacketWriter(capacity); + } + + public static void ReleaseInstance(PacketWriter pw) + { + m_Pool.Enqueue(pw); + } + + /// + /// Writes a 1-byte boolean value to the underlying stream. False is represented by 0, true by 1. + /// + public void Write(bool value) + { + UnderlyingStream.WriteByte((byte)(value ? 1 : 0)); + } + + /// + /// Writes a 1-byte unsigned integer value to the underlying stream. + /// + public void Write(byte value) + { + UnderlyingStream.WriteByte(value); + } + + /// + /// Writes a 1-byte signed integer value to the underlying stream. + /// + public void Write(sbyte value) + { + UnderlyingStream.WriteByte((byte)value); + } + + /// + /// Writes a 2-byte signed integer value to the underlying stream. + /// + public void Write(short value) + { + m_Buffer[0] = (byte)(value >> 8); + m_Buffer[1] = (byte)value; + + UnderlyingStream.Write(m_Buffer, 0, 2); + } + + /// + /// Writes a 2-byte unsigned integer value to the underlying stream. + /// + public void Write(ushort value) + { + m_Buffer[0] = (byte)(value >> 8); + m_Buffer[1] = (byte)value; + + UnderlyingStream.Write(m_Buffer, 0, 2); + } + + /// + /// Writes a 4-byte signed integer value to the underlying stream. + /// + public void Write(int value) + { + m_Buffer[0] = (byte)(value >> 24); + m_Buffer[1] = (byte)(value >> 16); + m_Buffer[2] = (byte)(value >> 8); + m_Buffer[3] = (byte)value; + + UnderlyingStream.Write(m_Buffer, 0, 4); + } + + /// + /// Writes a 4-byte unsigned integer value to the underlying stream. + /// + public void Write(uint value) + { + m_Buffer[0] = (byte)(value >> 24); + m_Buffer[1] = (byte)(value >> 16); + m_Buffer[2] = (byte)(value >> 8); + m_Buffer[3] = (byte)value; + + UnderlyingStream.Write(m_Buffer, 0, 4); + } + + /// + /// Writes a sequence of bytes to the underlying stream + /// + public void Write(byte[] buffer, int offset, int size) + { + UnderlyingStream.Write(buffer, offset, size); + } + + /// + /// Writes a fixed-length ASCII-encoded string value to the underlying stream. To fit (size), the string content is either + /// truncated or padded with null characters. + /// + public void WriteAsciiFixed(string value, int size) + { + if (value == null) + { + Console.WriteLine("Network: Attempted to WriteAsciiFixed() with null value"); + value = string.Empty; + } + + var length = value.Length; + + UnderlyingStream.SetLength(UnderlyingStream.Length + size); + + if (length >= size) + { + UnderlyingStream.Position += + Encoding.ASCII.GetBytes(value, 0, size, UnderlyingStream.GetBuffer(), (int)UnderlyingStream.Position); + } + else + { + Encoding.ASCII.GetBytes(value, 0, length, UnderlyingStream.GetBuffer(), (int)UnderlyingStream.Position); + UnderlyingStream.Position += size; + } + } + + /// + /// Writes a dynamic-length ASCII-encoded string value to the underlying stream, followed by a 1-byte null character. + /// + public void WriteAsciiNull(string value) + { + if (value == null) + { + Console.WriteLine("Network: Attempted to WriteAsciiNull() with null value"); + value = string.Empty; + } + + var length = value.Length; + + UnderlyingStream.SetLength(UnderlyingStream.Length + length + 1); + + Encoding.ASCII.GetBytes(value, 0, length, UnderlyingStream.GetBuffer(), (int)UnderlyingStream.Position); + UnderlyingStream.Position += length + 1; + } + + /// + /// Writes a dynamic-length little-endian unicode string value to the underlying stream, followed by a 2-byte null + /// character. + /// + public void WriteLittleUniNull(string value) + { + if (value == null) + { + Console.WriteLine("Network: Attempted to WriteLittleUniNull() with null value"); + value = string.Empty; + } + + var length = value.Length; + + UnderlyingStream.SetLength(UnderlyingStream.Length + (length + 1) * 2); + + UnderlyingStream.Position += + Encoding.Unicode.GetBytes(value, 0, length, UnderlyingStream.GetBuffer(), (int)UnderlyingStream.Position); + UnderlyingStream.Position += 2; + } + + /// + /// Writes a fixed-length little-endian unicode string value to the underlying stream. To fit (size), the string content is + /// either truncated or padded with null characters. + /// + public void WriteLittleUniFixed(string value, int size) + { + if (value == null) + { + Console.WriteLine("Network: Attempted to WriteLittleUniFixed() with null value"); + value = string.Empty; + } + + var length = value.Length; + size *= 2; + + UnderlyingStream.SetLength(UnderlyingStream.Length + size); + + if (length * 2 >= size) + { + UnderlyingStream.Position += + Encoding.Unicode.GetBytes( + value, + 0, + size / 2, + UnderlyingStream.GetBuffer(), + (int)UnderlyingStream.Position + ); + } + else + { + Encoding.Unicode.GetBytes(value, 0, length, UnderlyingStream.GetBuffer(), (int)UnderlyingStream.Position); + UnderlyingStream.Position += size; + } + } + + /// + /// Writes a dynamic-length big-endian unicode string value to the underlying stream, followed by a 2-byte null character. + /// + public void WriteBigUniNull(string value) + { + if (value == null) + { + Console.WriteLine("Network: Attempted to WriteBigUniNull() with null value"); + value = string.Empty; + } + + var length = value.Length; + + UnderlyingStream.SetLength(UnderlyingStream.Length + (length + 1) * 2); + + UnderlyingStream.Position += + Encoding.BigEndianUnicode.GetBytes( + value, + 0, + length, + UnderlyingStream.GetBuffer(), + (int)UnderlyingStream.Position + ); + UnderlyingStream.Position += 2; + } + + /// + /// Writes a fixed-length big-endian unicode string value to the underlying stream. To fit (size), the string content is + /// either truncated or padded with null characters. + /// + public void WriteBigUniFixed(string value, int size) + { + if (value == null) + { + Console.WriteLine("Network: Attempted to WriteBigUniFixed() with null value"); + value = string.Empty; + } + + var length = value.Length; + size *= 2; + + UnderlyingStream.SetLength(UnderlyingStream.Length + size); + + if (length * 2 >= size) + { + UnderlyingStream.Position += + Encoding.BigEndianUnicode.GetBytes( + value, + 0, + size / 2, + UnderlyingStream.GetBuffer(), + (int)UnderlyingStream.Position + ); + } + else + { + Encoding.BigEndianUnicode.GetBytes( + value, + 0, + length, + UnderlyingStream.GetBuffer(), + (int)UnderlyingStream.Position + ); + UnderlyingStream.Position += size; + } + } + + /// + /// Fills the stream from the current position up to (capacity) with 0x00's + /// + public void Fill() + { + Fill(m_Capacity - UnderlyingStream.Length); + } + + /// + /// Writes a number of 0x00 byte values to the underlying stream. + /// + public void Fill(long length) + { + if (UnderlyingStream.Position == UnderlyingStream.Length) + { + UnderlyingStream.SetLength(UnderlyingStream.Length + length); + UnderlyingStream.Seek(0, SeekOrigin.End); + } + else + { + UnderlyingStream.Write(new byte[length], 0, (int)length); + } + } + + /// + /// Offsets the current position from an origin. + /// + public long Seek(long offset, SeekOrigin origin) => UnderlyingStream.Seek(offset, origin); + + /// + /// Gets the entire stream content as a byte array. + /// + public byte[] ToArray() => UnderlyingStream.ToArray(); + } +} diff --git a/Projects/Benchmarks/Program.cs b/Projects/Benchmarks/Program.cs index 6ad1f5623..0315717a1 100644 --- a/Projects/Benchmarks/Program.cs +++ b/Projects/Benchmarks/Program.cs @@ -1,3 +1,4 @@ +using BenchmarkDotNet.Configs; using BenchmarkDotNet.Running; namespace Benchmarks @@ -10,9 +11,10 @@ namespace Benchmarks // var packetConstruction = BenchmarkRunner.Run(); // var broadcast = BenchmarkRunner.Run(); // var stringHelpers = BenchmarkRunner.Run(); - // var indexList = BenchmarkRunner.Run(); + var indexList = BenchmarkRunner.Run(); // var textEncoding = BenchmarkRunner.Run(); - var logging = BenchmarkRunner.Run(); + // var logging = BenchmarkRunner.Run(); + // var gumpPacket = BenchmarkRunner.Run(); } } } diff --git a/Projects/Server.Tests/Helpers/GumpUtilities.cs b/Projects/Server.Tests/Helpers/GumpUtilities.cs index f6c6b6450..70e7c69af 100644 --- a/Projects/Server.Tests/Helpers/GumpUtilities.cs +++ b/Projects/Server.Tests/Helpers/GumpUtilities.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using Server.Gumps; using Server.Network; @@ -42,141 +43,155 @@ namespace Server.Tests.Network } var count = g.Entries.Count; + var strings = new List(); for (var i = 0; i < count; ++i) { var e = g.Entries[i]; disp.AppendLayout(m_BeginLayout); - e.AppendToByType(disp); + e.AppendToByType(disp, strings); disp.AppendLayout(m_EndLayout); } - disp.WriteStrings(g.Strings); + disp.WriteStrings(strings); disp.Flush(); return (Packet)disp; } - public static void AppendToByType(this GumpEntry e, IGumpWriter disp) + public static int Intern(this List strings, string value) + { + var indexOf = strings.IndexOf(value); + + if (indexOf >= 0) + { + return indexOf; + } + + strings.Add(value); + return strings.Count - 1; + } + + public static void AppendToByType(this GumpEntry e, IGumpWriter disp, List strings) { switch (e) { case GumpAlphaRegion g: { - g.AppendTo(disp); + g.AppendTo(disp, strings); break; } case GumpBackground g: { - g.AppendTo(disp); + g.AppendTo(disp, strings); break; } case GumpButton g: { - g.AppendTo(disp); + g.AppendTo(disp, strings); break; } case GumpCheck g: { - g.AppendTo(disp); + g.AppendTo(disp, strings); break; } case GumpGroup g: { - g.AppendTo(disp); + g.AppendTo(disp, strings); break; } case GumpECHandleInput g: { - g.AppendTo(disp); + g.AppendTo(disp, strings); break; } case GumpHtml g: { - g.AppendTo(disp); + g.AppendTo(disp, strings); break; } case GumpHtmlLocalized g: { - g.AppendTo(disp); + g.AppendTo(disp, strings); break; } case GumpImage g: { - g.AppendTo(disp); + g.AppendTo(disp, strings); break; } case GumpImageTileButton g: { - g.AppendTo(disp); + g.AppendTo(disp, strings); break; } case GumpImageTiled g: { - g.AppendTo(disp); + g.AppendTo(disp, strings); break; } case GumpItem g: { - g.AppendTo(disp); + g.AppendTo(disp, strings); break; } case GumpItemProperty g: { - g.AppendTo(disp); + g.AppendTo(disp, strings); break; } case GumpLabel g: { - g.AppendTo(disp); + g.AppendTo(disp, strings); break; } case GumpLabelCropped g: { - g.AppendTo(disp); + g.AppendTo(disp, strings); break; } case GumpMasterGump g: { - g.AppendTo(disp); + g.AppendTo(disp, strings); break; } case GumpPage g: { - g.AppendTo(disp); + g.AppendTo(disp, strings); break; } case GumpRadio g: { - g.AppendTo(disp); + g.AppendTo(disp, strings); break; } case GumpSpriteImage g: { - g.AppendTo(disp); + g.AppendTo(disp, strings); break; } case GumpTextEntry g: { - g.AppendTo(disp); + g.AppendTo(disp, strings); break; } case GumpTextEntryLimited g: { - g.AppendTo(disp); + g.AppendTo(disp, strings); break; } case GumpTooltip g: { - g.AppendTo(disp); + g.AppendTo(disp, strings); break; } } } - public static void AppendTo(this GumpAlphaRegion g, IGumpWriter disp) + public static void AppendTo(this GumpAlphaRegion g, IGumpWriter disp, List strings) { disp.AppendLayout(GumpAlphaRegion.LayoutName); disp.AppendLayout(g.X); @@ -185,7 +200,7 @@ namespace Server.Tests.Network disp.AppendLayout(g.Height); } - public static void AppendTo(this GumpBackground g, IGumpWriter disp) + public static void AppendTo(this GumpBackground g, IGumpWriter disp, List strings) { disp.AppendLayout(GumpBackground.LayoutName); disp.AppendLayout(g.X); @@ -195,7 +210,7 @@ namespace Server.Tests.Network disp.AppendLayout(g.Height); } - public static void AppendTo(this GumpButton g, IGumpWriter disp) + public static void AppendTo(this GumpButton g, IGumpWriter disp, List strings) { disp.AppendLayout(GumpButton.LayoutName); disp.AppendLayout(g.X); @@ -207,7 +222,7 @@ namespace Server.Tests.Network disp.AppendLayout(g.ButtonID); } - public static void AppendTo(this GumpCheck g, IGumpWriter disp) + public static void AppendTo(this GumpCheck g, IGumpWriter disp, List strings) { disp.AppendLayout(GumpButton.LayoutName); disp.AppendLayout(g.X); @@ -220,30 +235,30 @@ namespace Server.Tests.Network disp.Switches++; } - public static void AppendTo(this GumpGroup g, IGumpWriter disp) + public static void AppendTo(this GumpGroup g, IGumpWriter disp, List strings) { disp.AppendLayout(GumpGroup.LayoutName); disp.AppendLayout(g.Group); } - public static void AppendTo(this GumpECHandleInput g, IGumpWriter disp) + public static void AppendTo(this GumpECHandleInput g, IGumpWriter disp, List strings) { disp.AppendLayout(GumpECHandleInput.LayoutName); } - public static void AppendTo(this GumpHtml g, IGumpWriter disp) + public static void AppendTo(this GumpHtml g, IGumpWriter disp, List strings) { disp.AppendLayout(GumpHtml.LayoutName); disp.AppendLayout(g.X); disp.AppendLayout(g.Y); disp.AppendLayout(g.Width); disp.AppendLayout(g.Height); - disp.AppendLayout(g.Parent.Intern(g.Text)); + disp.AppendLayout(strings.Intern(g.Text)); disp.AppendLayout(g.Background); disp.AppendLayout(g.Scrollbar); } - public static void AppendTo(this GumpHtmlLocalized g, IGumpWriter disp) + public static void AppendTo(this GumpHtmlLocalized g, IGumpWriter disp, List strings) { switch (g.Type) { @@ -297,7 +312,7 @@ namespace Server.Tests.Network } } - public static void AppendTo(this GumpImage g, IGumpWriter disp) + public static void AppendTo(this GumpImage g, IGumpWriter disp, List strings) { disp.AppendLayout(GumpImage.LayoutName); disp.AppendLayout(g.X); @@ -317,7 +332,7 @@ namespace Server.Tests.Network } } - public static void AppendTo(this GumpImageTileButton g, IGumpWriter disp) + public static void AppendTo(this GumpImageTileButton g, IGumpWriter disp, List strings) { disp.AppendLayout(GumpImageTileButton.LayoutName); disp.AppendLayout(g.X); @@ -340,7 +355,7 @@ namespace Server.Tests.Network } } - public static void AppendTo(this GumpImageTiled g, IGumpWriter disp) + public static void AppendTo(this GumpImageTiled g, IGumpWriter disp, List strings) { disp.AppendLayout(GumpImageTiled.LayoutName); disp.AppendLayout(g.X); @@ -350,7 +365,7 @@ namespace Server.Tests.Network disp.AppendLayout(g.GumpID); } - public static void AppendTo(this GumpItem g, IGumpWriter disp) + public static void AppendTo(this GumpItem g, IGumpWriter disp, List strings) { disp.AppendLayout(g.Hue == 0 ? GumpItem.LayoutName : GumpItem.LayoutNameHue); disp.AppendLayout(g.X); @@ -363,22 +378,22 @@ namespace Server.Tests.Network } } - public static void AppendTo(this GumpItemProperty g, IGumpWriter disp) + public static void AppendTo(this GumpItemProperty g, IGumpWriter disp, List strings) { disp.AppendLayout(GumpItemProperty.LayoutName); disp.AppendLayout(g.Serial); } - public static void AppendTo(this GumpLabel g, IGumpWriter disp) + public static void AppendTo(this GumpLabel g, IGumpWriter disp, List strings) { disp.AppendLayout(GumpLabel.LayoutName); disp.AppendLayout(g.X); disp.AppendLayout(g.Y); disp.AppendLayout(g.Hue); - disp.AppendLayout(g.Parent.Intern(g.Text)); + disp.AppendLayout(strings.Intern(g.Text)); } - public static void AppendTo(this GumpLabelCropped g, IGumpWriter disp) + public static void AppendTo(this GumpLabelCropped g, IGumpWriter disp, List strings) { disp.AppendLayout(GumpLabelCropped.LayoutName); disp.AppendLayout(g.X); @@ -386,22 +401,22 @@ namespace Server.Tests.Network disp.AppendLayout(g.Width); disp.AppendLayout(g.Height); disp.AppendLayout(g.Hue); - disp.AppendLayout(g.Parent.Intern(g.Text)); + disp.AppendLayout(strings.Intern(g.Text)); } - public static void AppendTo(this GumpMasterGump g, IGumpWriter disp) + public static void AppendTo(this GumpMasterGump g, IGumpWriter disp, List strings) { disp.AppendLayout(GumpMasterGump.LayoutName); disp.AppendLayout(g.GumpID); } - public static void AppendTo(this GumpPage g, IGumpWriter disp) + public static void AppendTo(this GumpPage g, IGumpWriter disp, List strings) { disp.AppendLayout(GumpPage.LayoutName); disp.AppendLayout(g.Page); } - public static void AppendTo(this GumpRadio g, IGumpWriter disp) + public static void AppendTo(this GumpRadio g, IGumpWriter disp, List strings) { disp.AppendLayout(GumpRadio.LayoutName); disp.AppendLayout(g.X); @@ -414,7 +429,7 @@ namespace Server.Tests.Network disp.Switches++; } - public static void AppendTo(this GumpSpriteImage g, IGumpWriter disp) + public static void AppendTo(this GumpSpriteImage g, IGumpWriter disp, List strings) { disp.AppendLayout(GumpSpriteImage.LayoutName); disp.AppendLayout(g.X); @@ -426,7 +441,7 @@ namespace Server.Tests.Network disp.AppendLayout(g.SY); } - public static void AppendTo(this GumpTextEntry g, IGumpWriter disp) + public static void AppendTo(this GumpTextEntry g, IGumpWriter disp, List strings) { disp.AppendLayout(GumpTextEntry.LayoutName); disp.AppendLayout(g.X); @@ -435,12 +450,12 @@ namespace Server.Tests.Network disp.AppendLayout(g.Height); disp.AppendLayout(g.Hue); disp.AppendLayout(g.EntryID); - disp.AppendLayout(g.Parent.Intern(g.InitialText)); + disp.AppendLayout(strings.Intern(g.InitialText)); disp.TextEntries++; } - public static void AppendTo(this GumpTextEntryLimited g, IGumpWriter disp) + public static void AppendTo(this GumpTextEntryLimited g, IGumpWriter disp, List strings) { disp.AppendLayout(GumpTextEntryLimited.LayoutName); disp.AppendLayout(g.X); @@ -449,13 +464,13 @@ namespace Server.Tests.Network disp.AppendLayout(g.Height); disp.AppendLayout(g.Hue); disp.AppendLayout(g.EntryID); - disp.AppendLayout(g.Parent.Intern(g.InitialText)); + disp.AppendLayout(strings.Intern(g.InitialText)); disp.AppendLayout(g.Size); disp.TextEntries++; } - public static void AppendTo(this GumpTooltip g, IGumpWriter disp) + public static void AppendTo(this GumpTooltip g, IGumpWriter disp, List strings) { disp.AppendLayout(GumpTooltip.LayoutName); disp.AppendLayout(g.Number); diff --git a/Projects/Server/Collections/CollectionHelpers.cs b/Projects/Server/Collections/CollectionHelpers.cs index 15e1dd26f..1989de97f 100644 --- a/Projects/Server/Collections/CollectionHelpers.cs +++ b/Projects/Server/Collections/CollectionHelpers.cs @@ -14,14 +14,16 @@ *************************************************************************/ using System.Collections.Generic; +using System.Runtime.CompilerServices; namespace Server.Collections { public static class CollectionHelpers { + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void AddNotNull(this ICollection coll, T t) where T : class { - if (t != default) + if (t != null) { coll.Add(t); } diff --git a/Projects/Server/Collections/CollectionThrowStrings.cs b/Projects/Server/Collections/CollectionThrowStrings.cs new file mode 100644 index 000000000..41feda828 --- /dev/null +++ b/Projects/Server/Collections/CollectionThrowStrings.cs @@ -0,0 +1,45 @@ +/************************************************************************* + * ModernUO * + * Copyright 2019-2021 - ModernUO Development Team * + * Email: hi@modernuo.com * + * File: CollectionThrowStrings.cs * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see . * + *************************************************************************/ + +namespace Server.Collections +{ + public static class CollectionThrowStrings + { + public const string ArgumentOutOfRange_Index = + "Index was out of range. Must be non-negative and less than the size of the collection."; + + public const string ArgumentOutOfRange_NeedNonNegNum = "Non-negative number required."; + + public const string Argument_InvalidOffLen = + "Offset and length were out of bounds for the array or count is greater than the number of elements from index to the end of the source collection."; + + public const string Argument_AddingDuplicate = "An item with the same value has already been added. Value: {0}"; + + public const string Arg_ArrayPlusOffTooSmall = + "Destination array is not long enough to copy all the items in the collection. Check array index and length."; + + public const string InvalidOperation_ConcurrentOperationsNotSupported = + "Operations that change non-concurrent collections must have exclusive access. A concurrent update was performed on this collection and corrupted its state. The collection's state is no longer correct."; + + public const string InvalidOperation_EnumFailedVersion = + "Collection was modified after the enumerator was instantiated."; + + public const string InvalidOperation_EmptyQueue = "Queue empty."; + + public const string InvalidOperation_EnumNotStarted = "Enumeration has not started. Call MoveNext."; + + public const string InvalidOperation_EnumEnded = "Enumeration already finished."; + } +} diff --git a/Projects/Server/Collections/OrderedHashSet.ValueCollection.cs b/Projects/Server/Collections/OrderedHashSet.ValueCollection.cs deleted file mode 100644 index df48f0bfd..000000000 --- a/Projects/Server/Collections/OrderedHashSet.ValueCollection.cs +++ /dev/null @@ -1,144 +0,0 @@ -/************************************************************************* - * ModernUO * - * Copyright (C) 2019-2021 - ModernUO Development Team * - * Email: hi@modernuo.com * - * File: OrderedHashSet.ValueCollection.cs * - * * - * This program is free software: you can redistribute it and/or modify * - * it under the terms of the GNU General Public License as published by * - * the Free Software Foundation, either version 3 of the License, or * - * (at your option) any later version. * - * * - * You should have received a copy of the GNU General Public License * - * along with this program. If not, see . * - *************************************************************************/ - -using System; -using System.Collections; -using System.Collections.Generic; -using System.Diagnostics; - -namespace Server.Collections -{ - public partial class OrderedHashSet - { - [DebuggerDisplay("Count = {Count}")] - public sealed class ValueCollection : IList, IReadOnlyList - { - private readonly OrderedHashSet _orderedHashSet; - - private const string NotSupported_ValueCollectionSet = - "Mutating a key collection derived from a hash set is not allowed."; - - public int Count => _orderedHashSet.Count; - - public TValue this[int index] => ((IList)_orderedHashSet)[index]; - - TValue IList.this[int index] - { - get => this[index]; - set => throw new NotSupportedException(NotSupported_ValueCollectionSet); - } - - bool ICollection.IsReadOnly => true; - - internal ValueCollection(OrderedHashSet OrderedHashSet) => - _orderedHashSet = OrderedHashSet; - - public Enumerator GetEnumerator() => new Enumerator(_orderedHashSet); - - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - - int IList.IndexOf(TValue item) => _orderedHashSet.IndexOf(item); - - void IList.Insert(int index, TValue item) => throw new NotSupportedException(NotSupported_ValueCollectionSet); - - void IList.RemoveAt(int index) => throw new NotSupportedException(NotSupported_ValueCollectionSet); - - void ICollection.Add(TValue item) => throw new NotSupportedException(NotSupported_ValueCollectionSet); - - void ICollection.Clear() => throw new NotSupportedException(NotSupported_ValueCollectionSet); - - bool ICollection.Contains(TValue item) => _orderedHashSet.Contains(item); - - void ICollection.CopyTo(TValue[] array, int arrayIndex) - { - if (array == null) - { - throw new ArgumentNullException(nameof(array)); - } - if ((uint)arrayIndex > (uint)array.Length) - { - throw new ArgumentOutOfRangeException(nameof(arrayIndex), ArgumentOutOfRange_NeedNonNegNum); - } - int count = Count; - if (array.Length - arrayIndex < count) - { - throw new ArgumentException(Arg_ArrayPlusOffTooSmall); - } - - Entry[] entries = _orderedHashSet._entries; - for (int i = 0; i < count; ++i) - { - array[i + arrayIndex] = entries[i].Value; - } - } - - bool ICollection.Remove(TValue item) => throw new NotSupportedException(NotSupported_ValueCollectionSet); - - public struct Enumerator : IEnumerator - { - private readonly OrderedHashSet _orderedHashSet; - private readonly int _version; - private int _index; - private TValue _current; - - public TValue Current => _current; - - object IEnumerator.Current => _current; - - internal Enumerator(OrderedHashSet OrderedHashSet) - { - _orderedHashSet = OrderedHashSet; - _version = OrderedHashSet._version; - _index = 0; - _current = default; - } - - public void Dispose() - { - } - - public bool MoveNext() - { - if (_version != _orderedHashSet._version) - { - throw new InvalidOperationException(InvalidOperation_EnumFailedVersion); - } - - if (_index < _orderedHashSet.Count) - { - _current = _orderedHashSet._entries[_index].Value; - ++_index; - return true; - } - _current = default; - return false; - } - - void IEnumerator.Reset() - { - if (_version != _orderedHashSet._version) - { - throw new InvalidOperationException(InvalidOperation_EnumFailedVersion); - } - - _index = 0; - _current = default; - } - } - } - } -} diff --git a/Projects/Server/Collections/OrderedHashSet.cs b/Projects/Server/Collections/OrderedHashSet.cs index 7eed4f1a3..cc9b7231b 100644 --- a/Projects/Server/Collections/OrderedHashSet.cs +++ b/Projects/Server/Collections/OrderedHashSet.cs @@ -14,7 +14,6 @@ *************************************************************************/ using System; -using System.Buffers; using System.Collections; using System.Collections.Generic; using System.Diagnostics; @@ -24,7 +23,7 @@ using Microsoft.Collections.Extensions; namespace Server.Collections { [DebuggerDisplay("Count = {Count}")] - public partial class OrderedHashSet : IList, IReadOnlyList, ISet, IReadOnlySet + public class OrderedHashSet : IList { private struct Entry { @@ -33,38 +32,20 @@ namespace Server.Collections public int Next; // the index of the next item in the same bucket, -1 if last } - private const string ArgumentOutOfRange_Index = - "Index was out of range. Must be non-negative and less than the size of the collection."; - - private const string ArgumentOutOfRange_NeedNonNegNum = - "Non-negative number required."; - - private const string Argument_InvalidOffLen = - "Offset and length were out of bounds for the array or count is greater than the number of elements from index to the end of the source collection."; - - private const string Argument_AddingDuplicate = "An item with the same value has already been added. Value: {0}"; - - private const string Arg_ArrayPlusOffTooSmall = - "Destination array is not long enough to copy all the items in the collection. Check array index and length."; - - private const string InvalidOperation_ConcurrentOperationsNotSupported = - "Operations that change non-concurrent collections must have exclusive access. A concurrent update was performed on this collection and corrupted its state. The collection's state is no longer correct."; - - private const string InvalidOperation_EnumFailedVersion = - "Collection was modified; enumeration operation may not execute."; - private static readonly Entry[] InitialEntries = new Entry[1]; private int[] _buckets = HashHelpers.SizeOneIntArray; private Entry[] _entries = InitialEntries; private ulong _fastModMultiplier; private int _count; private int _version; - private readonly IEqualityComparer _comparer; - private ValueCollection _values; +#nullable enable + private readonly IEqualityComparer? _comparer; +#nullable disable public int Count => _count; - public IEqualityComparer Comparer => _comparer ?? EqualityComparer.Default; - public ValueCollection Values => _values ??= new ValueCollection(this); +#nullable enable + public IEqualityComparer? Comparer => _comparer; +#nullable disable public OrderedHashSet() : this(0) @@ -111,48 +92,8 @@ namespace Server.Collections } } - public void ExceptWith(IEnumerable other) - { - throw new NotImplementedException(); - } - - public void IntersectWith(IEnumerable other) - { - throw new NotImplementedException(); - } - public bool Contains(TValue item) => TryGetValue(item, out var value) && EqualityComparer.Default.Equals(value); - // TODO: Implement IReadOnlySet and ISet - - bool IReadOnlySet.IsProperSubsetOf(IEnumerable other) => throw new NotImplementedException(); - - bool IReadOnlySet.IsProperSupersetOf(IEnumerable other) => throw new NotImplementedException(); - - bool IReadOnlySet.IsSubsetOf(IEnumerable other) => throw new NotImplementedException(); - - bool IReadOnlySet.IsSupersetOf(IEnumerable other) => throw new NotImplementedException(); - - bool IReadOnlySet.Overlaps(IEnumerable other) => throw new NotImplementedException(); - - bool IReadOnlySet.SetEquals(IEnumerable other) => throw new NotImplementedException(); - - bool ISet.IsProperSubsetOf(IEnumerable other) => throw new NotImplementedException(); - - bool ISet.IsProperSupersetOf(IEnumerable other) => throw new NotImplementedException(); - - bool ISet.IsSubsetOf(IEnumerable other) => throw new NotImplementedException(); - - bool ISet.IsSupersetOf(IEnumerable other) => throw new NotImplementedException(); - - bool ISet.Overlaps(IEnumerable other) => throw new NotImplementedException(); - - bool ISet.SetEquals(IEnumerable other) => throw new NotImplementedException(); - - public void SymmetricExceptWith(IEnumerable other) => throw new NotImplementedException(); - - public void UnionWith(IEnumerable other) => throw new NotImplementedException(); - public void Clear() { if (_count > 0) @@ -195,114 +136,12 @@ namespace Server.Collections { if ((uint)index > (uint)Count) { - throw new ArgumentOutOfRangeException(nameof(index), ArgumentOutOfRange_Index); + throw new ArgumentOutOfRangeException(nameof(index), CollectionThrowStrings.ArgumentOutOfRange_Index); } TryInsert(index, value); } - public void Move(int fromIndex, int toIndex) - { - if ((uint)fromIndex >= (uint)Count) - { - throw new ArgumentOutOfRangeException(nameof(fromIndex), ArgumentOutOfRange_Index); - } - if ((uint)toIndex >= (uint)Count) - { - throw new ArgumentOutOfRangeException(nameof(toIndex), ArgumentOutOfRange_Index); - } - - if (fromIndex == toIndex) - { - return; - } - - Entry[] entries = _entries; - Entry temp = entries[fromIndex]; - RemoveEntryFromBucket(fromIndex); - int direction = fromIndex < toIndex ? 1 : -1; - for (int i = fromIndex; i != toIndex; i += direction) - { - entries[i] = entries[i + direction]; - UpdateBucketIndex(i + direction, -direction); - } - AddEntryToBucket(ref temp, toIndex, _buckets); - entries[toIndex] = temp; - ++_version; - } - - public void MoveRange(int fromIndex, int toIndex, int count) - { - if (count == 1) - { - Move(fromIndex, toIndex); - return; - } - - if ((uint)fromIndex >= (uint)Count) - { - throw new ArgumentOutOfRangeException(nameof(fromIndex), ArgumentOutOfRange_Index); - } - if ((uint)toIndex >= (uint)Count) - { - throw new ArgumentOutOfRangeException(nameof(toIndex), ArgumentOutOfRange_Index); - } - if (count < 0) - { - throw new ArgumentOutOfRangeException(nameof(count), ArgumentOutOfRange_NeedNonNegNum); - } - if (fromIndex + count > Count) - { - throw new ArgumentException(Argument_InvalidOffLen); - } - if (toIndex + count > Count) - { - throw new ArgumentException(Argument_InvalidOffLen); - } - - if (fromIndex == toIndex || count == 0) - { - return; - } - - Entry[] entries = _entries; - Entry[] entriesToMove = ArrayPool.Shared.Rent(count); - for (int i = 0; i < count; ++i) - { - entriesToMove[i] = entries[fromIndex + i]; - RemoveEntryFromBucket(fromIndex + i); - } - - // Move entries in between - int direction = 1; - int amount = count; - int start = fromIndex; - int end = toIndex; - if (fromIndex > toIndex) - { - direction = -1; - amount = -count; - start = fromIndex + count - 1; - end = toIndex + count - 1; - } - for (int i = start; i != end; i += direction) - { - entries[i] = entries[i + amount]; - UpdateBucketIndex(i + amount, -amount); - } - - int[] buckets = _buckets; - // Copy entries to destination - for (int i = 0; i < count; ++i) - { - Entry temp = entriesToMove[i]; - AddEntryToBucket(ref temp, toIndex + i, buckets); - entries[toIndex + i] = temp; - } - ++_version; - ArrayPool.Shared.Return(entriesToMove); - } - [MethodImpl(MethodImplOptions.AggressiveInlining)] private ref int GetBucketRef(uint hashCode) { @@ -327,7 +166,7 @@ namespace Server.Collections int count = Count; if ((uint)index >= (uint)count) { - throw new ArgumentOutOfRangeException(nameof(index), ArgumentOutOfRange_Index); + throw new ArgumentOutOfRangeException(nameof(index), CollectionThrowStrings.ArgumentOutOfRange_Index); } // Remove the entry from the bucket @@ -345,8 +184,6 @@ namespace Server.Collections ++_version; } - public void TrimExcess() => TrimExcess(Count); - public void TrimExcess(int capacity) { if (capacity < Count) @@ -383,7 +220,7 @@ namespace Server.Collections { if ((uint)index >= (uint)Count) { - throw new ArgumentOutOfRangeException(nameof(index), ArgumentOutOfRange_Index); + throw new ArgumentOutOfRangeException(nameof(index), CollectionThrowStrings.ArgumentOutOfRange_Index); } return _entries[index].Value; @@ -392,7 +229,7 @@ namespace Server.Collections { if ((uint)index >= (uint)Count) { - throw new ArgumentOutOfRangeException(nameof(index), ArgumentOutOfRange_Index); + throw new ArgumentOutOfRangeException(nameof(index), CollectionThrowStrings.ArgumentOutOfRange_Index); } TValue v = value; @@ -412,7 +249,7 @@ namespace Server.Collections } else { - throw new ArgumentException(string.Format(Argument_AddingDuplicate, v.ToString())); + throw new ArgumentException(string.Format(CollectionThrowStrings.Argument_AddingDuplicate, v.ToString())); } } } @@ -432,13 +269,13 @@ namespace Server.Collections if ((uint)arrayIndex > (uint)array.Length) { - throw new ArgumentOutOfRangeException(nameof(arrayIndex), ArgumentOutOfRange_NeedNonNegNum); + throw new ArgumentOutOfRangeException(nameof(arrayIndex), CollectionThrowStrings.ArgumentOutOfRange_NeedNonNegNum); } int count = Count; if (array.Length - arrayIndex < count) { - throw new ArgumentException(Arg_ArrayPlusOffTooSmall); + throw new ArgumentException(CollectionThrowStrings.Arg_ArrayPlusOffTooSmall); } Entry[] entries = _entries; @@ -470,38 +307,108 @@ namespace Server.Collections return newEntries; } +#nullable enable private int IndexOf(TValue value, out uint hashCode) { - IEqualityComparer comparer = _comparer; - hashCode = (uint)(comparer?.GetHashCode(value) ?? value?.GetHashCode() ?? 0); - ref int bucket = ref GetBucketRef(hashCode); - int i = bucket - 1; - if (i >= 0) + ref int bucket = ref Unsafe.NullRef(); + int i; + + IEqualityComparer? comparer = _comparer; + if (comparer == null) { - comparer ??= EqualityComparer.Default; - Entry[] entries = _entries; - int collisionCount = 0; - do + hashCode = (uint)(value?.GetHashCode() ?? 0); + bucket = ref GetBucketRef(hashCode); + i = bucket - 1; + + if (i >= 0) { - Entry entry = entries[i]; - if (entry.HashCode == hashCode && comparer.Equals(entry.Value, value)) + if (typeof(TValue).IsValueType) { - break; + // ValueType: Devirtualize with EqualityComparer.Default intrinsic + Entry[] entries = _entries; + int collisionCount = 0; + do + { + Entry entry = entries[i]; + if (entry.HashCode == hashCode && EqualityComparer.Default.Equals(entry.Value, value)) + { + break; + } + + i = entry.Next; + if (collisionCount >= entries.Length) + { + // The chain of entries forms a loop; which means a concurrent update has happened. + // Break out of the loop and throw, rather than looping forever. + throw new InvalidOperationException( + CollectionThrowStrings.InvalidOperation_ConcurrentOperationsNotSupported + ); + } + + ++collisionCount; + } while (i >= 0); } - i = entry.Next; - if (collisionCount >= entries.Length) + else { - // The chain of entries forms a loop; which means a concurrent update has happened. - // Break out of the loop and throw, rather than looping forever. - throw new InvalidOperationException(InvalidOperation_ConcurrentOperationsNotSupported); + // Object type: Shared Generic, EqualityComparer.Default won't devirtualize (https://github.com/dotnet/runtime/issues/10050), + // so cache in a local rather than get EqualityComparer per loop iteration. + var defaultComparer = EqualityComparer.Default; + Entry[] entries = _entries; + int collisionCount = 0; + do + { + Entry entry = entries[i]; + if (entry.HashCode == hashCode && defaultComparer.Equals(entry.Value, value)) + { + break; + } + + i = entry.Next; + if (collisionCount >= entries.Length) + { + // The chain of entries forms a loop; which means a concurrent update has happened. + // Break out of the loop and throw, rather than looping forever. + throw new InvalidOperationException( + CollectionThrowStrings.InvalidOperation_ConcurrentOperationsNotSupported + ); + } + + ++collisionCount; + } while (i >= 0); } - ++collisionCount; - } while (i >= 0); + } } + else + { + hashCode = (uint)comparer.GetHashCode(value); + bucket = ref GetBucketRef(hashCode); + i = bucket - 1; + if (i >= 0) + { + Entry[] entries = _entries; + int collisionCount = 0; + do + { + Entry entry = entries[i]; + if (entry.HashCode == hashCode && comparer.Equals(entry.Value, value)) + { + break; + } + i = entry.Next; + if (collisionCount >= entries.Length) + { + // The chain of entries forms a loop; which means a concurrent update has happened. + // Break out of the loop and throw, rather than looping forever. + throw new InvalidOperationException(CollectionThrowStrings.InvalidOperation_ConcurrentOperationsNotSupported); + } + ++collisionCount; + } while (i >= 0); + } + } + return i; } -#nullable enable [MethodImpl(MethodImplOptions.AggressiveInlining)] private int TryInsert(int? index, TValue value) { @@ -574,7 +481,7 @@ namespace Server.Collections { // The chain of entries forms a loop; which means a concurrent update has happened. // Break out of the loop and throw, rather than looping forever. - throw new InvalidOperationException(InvalidOperation_ConcurrentOperationsNotSupported); + throw new InvalidOperationException(CollectionThrowStrings.InvalidOperation_ConcurrentOperationsNotSupported); } ++collisionCount; } @@ -609,7 +516,7 @@ namespace Server.Collections { // The chain of entries forms a loop; which means a concurrent update has happened. // Break out of the loop and throw, rather than looping forever. - throw new InvalidOperationException(InvalidOperation_ConcurrentOperationsNotSupported); + throw new InvalidOperationException(CollectionThrowStrings.InvalidOperation_ConcurrentOperationsNotSupported); } ++collisionCount; } @@ -643,7 +550,7 @@ namespace Server.Collections { if (_version != _orderedHashSet._version) { - throw new InvalidOperationException(InvalidOperation_EnumFailedVersion); + throw new InvalidOperationException(CollectionThrowStrings.InvalidOperation_EnumFailedVersion); } if (_index < _orderedHashSet.Count) @@ -661,7 +568,7 @@ namespace Server.Collections { if (_version != _orderedHashSet._version) { - throw new InvalidOperationException(InvalidOperation_EnumFailedVersion); + throw new InvalidOperationException(CollectionThrowStrings.InvalidOperation_EnumFailedVersion); } _index = 0; diff --git a/Projects/Server/Collections/PooledOrderedHashSet.cs b/Projects/Server/Collections/PooledOrderedHashSet.cs new file mode 100644 index 000000000..4a6e51f1b --- /dev/null +++ b/Projects/Server/Collections/PooledOrderedHashSet.cs @@ -0,0 +1,603 @@ +/************************************************************************* + * ModernUO * + * Copyright 2019-2021 - ModernUO Development Team * + * Email: hi@modernuo.com * + * File: PooledOrderedHashSet.cs * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see . * + *************************************************************************/ + +using System; +using System.Buffers; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using Microsoft.Collections.Extensions; + +namespace Server.Collections +{ + [DebuggerDisplay("Count = {Count}")] + public class PooledOrderedHashSet : IList, IDisposable + { + private struct Entry + { + public uint HashCode; + public TValue Value; + public int Next; // the index of the next item in the same bucket, -1 if last + } + + private static readonly Entry[] InitialEntries = new Entry[1]; + private int[] _buckets = HashHelpers.SizeOneIntArray; + private int _bucketsLength = 1; + private Entry[] _entries = InitialEntries; + private int _entriesLength = 1; + private ulong _fastModMultiplier; + private int _count; + private int _version; +#nullable enable + private readonly IEqualityComparer? _comparer; +#nullable disable + + public int Count => _count; +#nullable enable + public IEqualityComparer? Comparer => _comparer; +#nullable disable + + public PooledOrderedHashSet() + : this(0) + { + } + + public PooledOrderedHashSet(IEqualityComparer comparer) + : this(0, comparer) + { + } + + public PooledOrderedHashSet(int capacity, IEqualityComparer comparer = null) + { + if (capacity < 0) + { + throw new ArgumentOutOfRangeException(nameof(capacity)); + } + + if (capacity > 0) + { + int newSize = HashHelpers.GetPrime(capacity); + _buckets = ArrayPool.Shared.Rent(newSize); + _bucketsLength = newSize; + _entries = ArrayPool.Shared.Rent(newSize); + _entriesLength = newSize; + _fastModMultiplier = HashHelpers.GetFastModMultiplier((uint)newSize); + } + + if (comparer != EqualityComparer.Default) + { + _comparer = comparer; + } + } + + public PooledOrderedHashSet(IEnumerable collection, IEqualityComparer comparer = null) + : this((collection as ICollection)?.Count ?? 0, comparer) + { + if (collection == null) + { + throw new ArgumentNullException(nameof(collection)); + } + + foreach (TValue value in collection) + { + Add(value); + } + } + + public bool Contains(TValue item) => TryGetValue(item, out var value) && EqualityComparer.Default.Equals(value); + + public void Clear() + { + if (_count > 0) + { + Array.Clear(_buckets, 0, _bucketsLength); + Array.Clear(_entries, 0, _count); + _count = 0; + ++_version; + } + } + + public Enumerator GetEnumerator() => new(this); + + void ICollection.Add(TValue item) => TryAdd(item); + + public bool Add(TValue item) => TryAdd(item); + + public int GetOrAdd(TValue value) => TryInsert(null, value); + + public int IndexOf(TValue value) => IndexOf(value, out _); + + public void Insert(int index, TValue value) + { + if ((uint)index > (uint)Count) + { + throw new ArgumentOutOfRangeException(nameof(index), CollectionThrowStrings.ArgumentOutOfRange_Index); + } + + TryInsert(index, value); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private ref int GetBucketRef(uint hashCode) + { + int[] buckets = _buckets!; + return ref buckets[HashHelpers.FastMod(hashCode, (uint)_bucketsLength, _fastModMultiplier)]; + } + + public bool Remove(TValue value) + { + int index = IndexOf(value); + if (index >= 0) + { + RemoveAt(index); + return true; + } + + return false; + } + + public void RemoveAt(int index) + { + int count = Count; + if ((uint)index >= (uint)count) + { + throw new ArgumentOutOfRangeException(nameof(index), CollectionThrowStrings.ArgumentOutOfRange_Index); + } + + // Remove the entry from the bucket + RemoveEntryFromBucket(index); + + // Decrement the indices > index + Entry[] entries = _entries; + for (int i = index + 1; i < count; ++i) + { + entries[i - 1] = entries[i]; + UpdateBucketIndex(i, incrementAmount: -1); + } + --_count; + entries[_count] = default; + ++_version; + } + + public bool TryAdd(TValue value) => TryInsert(null, value) != _count - 1; + + public bool TryGetValue(TValue value, out TValue actualValue) + { + int index = IndexOf(value); + if (index >= 0) + { + actualValue = _entries[index].Value; + return true; + } + + actualValue = default; + return false; + } + + public TValue this[int index] + { + get + { + if ((uint)index >= (uint)Count) + { + throw new ArgumentOutOfRangeException(nameof(index), CollectionThrowStrings.ArgumentOutOfRange_Index); + } + + return _entries[index].Value; + } + set + { + if ((uint)index >= (uint)Count) + { + throw new ArgumentOutOfRangeException(nameof(index), CollectionThrowStrings.ArgumentOutOfRange_Index); + } + + TValue v = value; + int foundIndex = IndexOf(v, out uint hashCode); + if (foundIndex < 0) + { + RemoveEntryFromBucket(index); + Entry entry = new Entry { HashCode = hashCode, Value = value }; + AddEntryToBucket(ref entry, index, _buckets, _bucketsLength); + _entries[index] = entry; + ++_version; + } + else if (foundIndex == index) + { + ref Entry entry = ref _entries[index]; + entry.Value = value; + } + else + { + throw new ArgumentException(string.Format(CollectionThrowStrings.Argument_AddingDuplicate, v.ToString())); + } + } + } + + public bool IsReadOnly => false; + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public void CopyTo(TValue[] array, int arrayIndex) + { + if (array == null) + { + throw new ArgumentNullException(nameof(array)); + } + + if ((uint)arrayIndex > (uint)array.Length) + { + throw new ArgumentOutOfRangeException(nameof(arrayIndex), CollectionThrowStrings.ArgumentOutOfRange_NeedNonNegNum); + } + + int count = Count; + if (array.Length - arrayIndex < count) + { + throw new ArgumentException(CollectionThrowStrings.Arg_ArrayPlusOffTooSmall); + } + + Entry[] entries = _entries; + for (int i = 0; i < count; ++i) + { + Entry entry = entries[i]; + array[i + arrayIndex] = entry.Value; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private Entry[] Resize(int newSize) + { + int[] newBuckets = _buckets.Length < newSize ? ArrayPool.Shared.Rent(newSize) : _buckets; + Entry[] newEntries = _entries.Length < newSize ? ArrayPool.Shared.Rent(newSize) : _entries; + + int count = Count; + Array.Copy(_entries, newEntries, count); + + _fastModMultiplier = HashHelpers.GetFastModMultiplier((uint)newSize); + + for (int i = 0; i < count; ++i) + { + AddEntryToBucket(ref newEntries[i], i, newBuckets, newSize); + } + + var oldBuckets = _buckets; + var oldEntries = _entries; + + if (oldBuckets.Length > 1 && oldBuckets != newBuckets) + { + ArrayPool.Shared.Return(oldBuckets, true); + } + + if (oldEntries.Length > 1 && oldEntries != newEntries) + { + ArrayPool.Shared.Return(oldEntries, true); + } + + _buckets = newBuckets; + _bucketsLength = newSize; + _entries = newEntries; + _entriesLength = newSize; + return newEntries; + } + +#nullable enable + private int IndexOf(TValue value, out uint hashCode) + { + ref int bucket = ref Unsafe.NullRef(); + int i; + + IEqualityComparer? comparer = _comparer; + if (comparer == null) + { + hashCode = (uint)value.GetHashCode(); + bucket = ref GetBucketRef(hashCode); + i = bucket - 1; + + if (i >= 0) + { + if (typeof(TValue).IsValueType) + { + // ValueType: Devirtualize with EqualityComparer.Default intrinsic + Entry[] entries = _entries; + int collisionCount = 0; + do + { + Entry entry = entries[i]; + if (entry.HashCode == hashCode && EqualityComparer.Default.Equals(entry.Value, value)) + { + break; + } + + i = entry.Next; + if (collisionCount >= _entriesLength) + { + // The chain of entries forms a loop; which means a concurrent update has happened. + // Break out of the loop and throw, rather than looping forever. + throw new InvalidOperationException( + CollectionThrowStrings.InvalidOperation_ConcurrentOperationsNotSupported + ); + } + + ++collisionCount; + } while (i >= 0); + } + else + { + // Object type: Shared Generic, EqualityComparer.Default won't devirtualize (https://github.com/dotnet/runtime/issues/10050), + // so cache in a local rather than get EqualityComparer per loop iteration. + var defaultComparer = EqualityComparer.Default; + Entry[] entries = _entries; + int collisionCount = 0; + do + { + Entry entry = entries[i]; + if (entry.HashCode == hashCode && defaultComparer.Equals(entry.Value, value)) + { + break; + } + + i = entry.Next; + if (collisionCount >= _entriesLength) + { + // The chain of entries forms a loop; which means a concurrent update has happened. + // Break out of the loop and throw, rather than looping forever. + throw new InvalidOperationException( + CollectionThrowStrings.InvalidOperation_ConcurrentOperationsNotSupported + ); + } + + ++collisionCount; + } while (i >= 0); + } + } + } + else + { + hashCode = (uint)comparer.GetHashCode(value); + bucket = ref GetBucketRef(hashCode); + i = bucket - 1; + if (i >= 0) + { + Entry[] entries = _entries; + int collisionCount = 0; + do + { + Entry entry = entries[i]; + if (entry.HashCode == hashCode && comparer.Equals(entry.Value, value)) + { + break; + } + i = entry.Next; + if (collisionCount >= _entriesLength) + { + // The chain of entries forms a loop; which means a concurrent update has happened. + // Break out of the loop and throw, rather than looping forever. + throw new InvalidOperationException(CollectionThrowStrings.InvalidOperation_ConcurrentOperationsNotSupported); + } + ++collisionCount; + } while (i >= 0); + } + } + + return i; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private int TryInsert(int? index, TValue value) + { + int i = IndexOf(value, out uint hashCode); + return i >= 0 ? i : AddInternal(index, value, hashCode); + } + + private int AddInternal(int? index, TValue value, uint hashCode) + { + Entry[] entries = _entries; + // Check if resize is needed + int count = Count; + if (_entriesLength == count || entries.Length == 1) + { + entries = Resize(HashHelpers.ExpandPrime(_entriesLength)); + } + + // Increment indices >= index; + int actualIndex = index ?? count; + for (int i = count - 1; i >= actualIndex; --i) + { + entries[i + 1] = entries[i]; + UpdateBucketIndex(i, incrementAmount: 1); + } + + ref Entry entry = ref entries[actualIndex]; + entry.HashCode = hashCode; + entry.Value = value; + AddEntryToBucket(ref entry, actualIndex, _buckets, _bucketsLength); + ++_count; + ++_version; + return actualIndex; + } +#nullable restore + + // Returns the index of the next entry in the bucket + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void AddEntryToBucket(ref Entry entry, int entryIndex, int[] buckets, int bucketsLength) + { + ref int b = ref buckets[(int)(entry.HashCode % (uint)bucketsLength)]; + entry.Next = b - 1; + b = entryIndex + 1; + } + + private void RemoveEntryFromBucket(int entryIndex) + { + Entry[] entries = _entries; + Entry entry = entries[entryIndex]; + ref int bucket = ref GetBucketRef(entry.HashCode); + // Bucket was pointing to removed entry. Update it to point to the next in the chain + if (bucket == entryIndex + 1) + { + bucket = entry.Next + 1; + } + else + { + // Start at the entry the bucket points to, and walk the chain until we find the entry with the index we want to remove, then fix the chain + int i = bucket - 1; + int collisionCount = 0; + while (true) + { + ref Entry e = ref entries[i]; + if (e.Next == entryIndex) + { + e.Next = entry.Next; + return; + } + i = e.Next; + if (collisionCount >= _entriesLength) + { + // The chain of entries forms a loop; which means a concurrent update has happened. + // Break out of the loop and throw, rather than looping forever. + throw new InvalidOperationException(CollectionThrowStrings.InvalidOperation_ConcurrentOperationsNotSupported); + } + ++collisionCount; + } + } + } + + private void UpdateBucketIndex(int entryIndex, int incrementAmount) + { + Entry[] entries = _entries; + Entry entry = entries[entryIndex]; + ref int bucket = ref GetBucketRef(entry.HashCode); + // Bucket was pointing to entry. Increment the index by incrementAmount. + if (bucket == entryIndex + 1) + { + bucket += incrementAmount; + } + else + { + // Start at the entry the bucket points to, and walk the chain until we find the entry with the index we want to increment. + int i = bucket - 1; + int collisionCount = 0; + while (true) + { + ref Entry e = ref entries[i]; + if (e.Next == entryIndex) + { + e.Next += incrementAmount; + return; + } + i = e.Next; + if (collisionCount >= _entriesLength) + { + // The chain of entries forms a loop; which means a concurrent update has happened. + // Break out of the loop and throw, rather than looping forever. + throw new InvalidOperationException(CollectionThrowStrings.InvalidOperation_ConcurrentOperationsNotSupported); + } + ++collisionCount; + } + } + } + + public struct Enumerator : IEnumerator + { + private readonly PooledOrderedHashSet _PooledOrderedHashSet; + private readonly int _version; + private int _index; + private TValue _current; + + public TValue Current => _current; + + object IEnumerator.Current => _current; + + internal Enumerator(PooledOrderedHashSet PooledOrderedHashSet) + { + _PooledOrderedHashSet = PooledOrderedHashSet; + _version = PooledOrderedHashSet._version; + _index = 0; + _current = default; + } + + public void Dispose() + { + } + + public bool MoveNext() + { + if (_version != _PooledOrderedHashSet._version) + { + throw new InvalidOperationException(CollectionThrowStrings.InvalidOperation_EnumFailedVersion); + } + + if (_index < _PooledOrderedHashSet.Count) + { + Entry entry = _PooledOrderedHashSet._entries[_index]; + _current = entry.Value; + ++_index; + return true; + } + _current = default; + return false; + } + + void IEnumerator.Reset() + { + if (_version != _PooledOrderedHashSet._version) + { + throw new InvalidOperationException(CollectionThrowStrings.InvalidOperation_EnumFailedVersion); + } + + _index = 0; + _current = default; + } + } + + public void Dispose() + { + if (_buckets.Length > 1) + { + ArrayPool.Shared.Return(_buckets, true); + } + + if (_entries.Length > 1) + { + ArrayPool.Shared.Return(_entries, true); + } + + _buckets = HashHelpers.SizeOneIntArray; + _entries = InitialEntries; + _count = 0; + + GC.SuppressFinalize(this); + } + + ~PooledOrderedHashSet() + { + if (_buckets.Length > 1) + { + ArrayPool.Shared.Return(_buckets, true); + } + + if (_entries.Length > 1) + { + ArrayPool.Shared.Return(_entries, true); + } + + _buckets = HashHelpers.SizeOneIntArray; + _entries = InitialEntries; + _count = 0; + } + } +} diff --git a/Projects/Server/Collections/PooledRefQueue.cs b/Projects/Server/Collections/PooledRefQueue.cs index 27fc07ce8..359c42635 100644 --- a/Projects/Server/Collections/PooledRefQueue.cs +++ b/Projects/Server/Collections/PooledRefQueue.cs @@ -1,17 +1,18 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; using System.Buffers; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; -namespace System.Collections.Generic +namespace Server.Collections { // A simple Queue of generic objects. Internally it is implemented as a // circular buffer, so Enqueue can be O(n). Dequeue is O(1). [DebuggerDisplay("Count = {Count}")] - [Serializable] + [System.Serializable] public ref struct PooledRefQueue { private T[] _array; @@ -29,7 +30,7 @@ namespace System.Collections.Generic { _array = capacity switch { - < 0 => throw new ArgumentOutOfRangeException(nameof(capacity), capacity, "Non-negative number required."), + < 0 => throw new ArgumentOutOfRangeException(nameof(capacity), capacity, CollectionThrowStrings.ArgumentOutOfRange_NeedNonNegNum), 0 => Array.Empty(), _ => ArrayPool.Shared.Rent(capacity) }; @@ -74,17 +75,17 @@ namespace System.Collections.Generic { if (array == null) { - throw new ArgumentNullException(nameof(array), "Array cannot be null."); + throw new ArgumentNullException(nameof(array)); } if (arrayIndex < 0 || arrayIndex > array.Length) { - throw new ArgumentOutOfRangeException(nameof(arrayIndex), arrayIndex, "Index was out of range. Must be non-negative and less than the size of the collection."); + throw new ArgumentOutOfRangeException(nameof(arrayIndex), arrayIndex, CollectionThrowStrings.ArgumentOutOfRange_Index); } if (array.Length - arrayIndex < _size) { - throw new ArgumentException("Offset and length were out of bounds for the array or count is greater than the number of elements from index to the end of the source collection."); + throw new ArgumentException(CollectionThrowStrings.Argument_InvalidOffLen); } int numToCopy = _size; @@ -305,16 +306,7 @@ namespace System.Collections.Generic private void ThrowForEmptyQueue() { Debug.Assert(_size == 0); - throw new InvalidOperationException("Queue empty."); - } - - public void TrimExcess() - { - int threshold = (int)(_array.Length * 0.9); - if (_size < threshold) - { - SetCapacity(_size); - } + throw new InvalidOperationException(CollectionThrowStrings.InvalidOperation_EmptyQueue); } /// @@ -325,7 +317,7 @@ namespace System.Collections.Generic { if (capacity < 0) { - throw new ArgumentOutOfRangeException(nameof(capacity), capacity, "Non-negative number required."); + throw new ArgumentOutOfRangeException(nameof(capacity), capacity, CollectionThrowStrings.ArgumentOutOfRange_NeedNonNegNum); } if (_array.Length < capacity) @@ -338,10 +330,8 @@ namespace System.Collections.Generic private void Grow(int capacity) { - Debug.Assert(_array.Length < capacity); - const int GrowFactor = 2; - const int MinimumGrow = 32; + const int MinimumGrow = 4; int newcapacity = GrowFactor * _array.Length; @@ -405,7 +395,7 @@ namespace System.Collections.Generic { if (_version != _q._version) { - throw new InvalidOperationException("Collection was modified after the enumerator was instantiated."); + throw new InvalidOperationException(CollectionThrowStrings.InvalidOperation_EnumFailedVersion); } if (_index == -2) @@ -462,14 +452,14 @@ namespace System.Collections.Generic private void ThrowEnumerationNotStartedOrEnded() { Debug.Assert(_index == -1 || _index == -2); - throw new InvalidOperationException(_index == -1 ? "Enumeration has not started. Call MoveNext." : "Enumeration already finished."); + throw new InvalidOperationException(_index == -1 ? CollectionThrowStrings.InvalidOperation_EnumNotStarted : CollectionThrowStrings.InvalidOperation_EnumEnded); } public void Reset() { if (_version != _q._version) { - throw new InvalidOperationException("Collection was modified after the enumerator was instantiated."); + throw new InvalidOperationException(CollectionThrowStrings.InvalidOperation_EnumFailedVersion); } _index = -1; diff --git a/Projects/Server/Network/Packets/OutgoingGumpPackets.cs b/Projects/Server/Network/Packets/OutgoingGumpPackets.cs index d8eefbd26..8fab20ff2 100644 --- a/Projects/Server/Network/Packets/OutgoingGumpPackets.cs +++ b/Projects/Server/Network/Packets/OutgoingGumpPackets.cs @@ -49,9 +49,10 @@ namespace Server.Network private static readonly byte[] _layoutBuffer = GC.AllocateUninitializedArray(0x20000); private static readonly byte[] _stringsBuffer = GC.AllocateUninitializedArray(0x20000); private static readonly byte[] _packBuffer = GC.AllocateUninitializedArray(0x20000); + private static readonly OrderedHashSet _stringsList = new(32); [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void WritePacked(ReadOnlySpan span, ref SpanWriter writer) + public static void WritePacked(ReadOnlySpan span, ref SpanWriter writer) { var length = span.Length; @@ -127,16 +128,14 @@ namespace Server.Network layoutWriter.Write(Gump.NoResize); } - var stringsList = new OrderedHashSet(11); - foreach (var entry in gump.Entries) { - entry.AppendTo(ref layoutWriter, stringsList, ref entries, ref switches); + entry.AppendTo(ref layoutWriter, _stringsList, ref entries, ref switches); } var stringsWriter = new SpanWriter(_stringsBuffer); - foreach (var str in stringsList) + foreach (var str in _stringsList) { var s = str ?? ""; stringsWriter.Write((ushort)s.Length); @@ -155,7 +154,7 @@ namespace Server.Network maxLength = 23 + layoutWriter.BytesWritten + stringsWriter.BytesWritten; } - var writer = new SpanWriter(stackalloc byte[maxLength]); + var writer = new SpanWriter(maxLength); writer.Write((byte)(packed ? 0xDD : 0xB0)); // Packet ID writer.Seek(2, SeekOrigin.Current); @@ -169,7 +168,7 @@ namespace Server.Network layoutWriter.Write((byte)0); // Layout text terminator WritePacked(layoutWriter.Span, ref writer); - writer.Write(stringsList.Count); + writer.Write(_stringsList.Count); WritePacked(stringsWriter.Span, ref writer); } else @@ -177,18 +176,26 @@ namespace Server.Network writer.Write((ushort)layoutWriter.BytesWritten); writer.Write(layoutWriter.Span); - writer.Write((ushort)stringsList.Count); + writer.Write((ushort)_stringsList.Count); writer.Write(stringsWriter.Span); } writer.WritePacketLength(); ns.Send(writer.Span); + + layoutWriter.Dispose(); // Just in case + stringsWriter.Dispose(); // Just in case + + if (_stringsList.Count > 0) + { + _stringsList.Clear(); + } } public static void SendDisplaySignGump(this NetState ns, Serial serial, int gumpId, string unknown, string caption) { - if (ns == null || !ns.GetSendBuffer(out var buffer)) + if (ns == null) { return; }