diff --git a/src/Commands.Http/Commands.Http/Execution/HttpCommandExecutionFactory.cs b/src/Commands.Http/Commands.Http/Execution/HttpCommandExecutionFactory.cs index 6e602f2..991648e 100644 --- a/src/Commands.Http/Commands.Http/Execution/HttpCommandExecutionFactory.cs +++ b/src/Commands.Http/Commands.Http/Execution/HttpCommandExecutionFactory.cs @@ -34,11 +34,8 @@ public Task StartAsync(CancellationToken cancellationToken = default) { Logger?.LogDebug("Configuring HTTP prefix: {Prefix}", prefix.Value); - if (string.IsNullOrWhiteSpace(prefix.Value)) - continue; - - if (!httpListener.Prefixes.Contains(prefix.Value!)) - httpListener.Prefixes.Add(prefix.Value!); + if (!string.IsNullOrWhiteSpace(prefix.Value) && !httpListener.Prefixes.Contains(prefix.Value)) + httpListener.Prefixes.Add(prefix.Value); } } diff --git a/src/Commands/Commands.Parsing/Parsers/ColorParser.cs b/src/Commands/Commands.Parsing/Parsers/ColorParser.cs index e403150..8692988 100644 --- a/src/Commands/Commands.Parsing/Parsers/ColorParser.cs +++ b/src/Commands/Commands.Parsing/Parsers/ColorParser.cs @@ -17,15 +17,9 @@ public ColorParser() { _colors = new(StringComparer.OrdinalIgnoreCase); - var properties = typeof(Color).GetProperties(BindingFlags.Public | BindingFlags.Static); - - foreach (var property in properties) + foreach (var property in typeof(Color).GetProperties(BindingFlags.Public | BindingFlags.Static).Where(static p => p.PropertyType == typeof(Color))) { - if (property.PropertyType == typeof(Color)) - { - var color = (Color)property.GetValue(null)!; - _colors[property.Name] = color; - } + _colors[property.Name] = (Color)property.GetValue(null)!; } } @@ -61,8 +55,6 @@ public override ValueTask Parse(IContext context, ICommandParameter private bool TryParseRgb(string value, out Color result) { - result = new(); - var separation = value.Split(','); if (separation.Length == 3) @@ -76,6 +68,7 @@ private bool TryParseRgb(string value, out Color result) } } + result = new(); return false; } @@ -108,8 +101,6 @@ private bool TryParseHex(string value, out Color result) private bool TryParseUint(string value, out Color result) { - result = new(); - if (uint.TryParse(value, out var rgb)) { var r = (byte)((rgb & 0xFF0000) >> 16); @@ -121,13 +112,12 @@ private bool TryParseUint(string value, out Color result) return true; } + result = new(); return false; } private bool TryParseNamed(string value, out Color result) { - result = new(); - if (_colors.TryGetValue(value, out var color)) { result = color; @@ -135,6 +125,7 @@ private bool TryParseNamed(string value, out Color result) return true; } + result = new(); return false; } } diff --git a/src/Commands/Commands/Assert.cs b/src/Commands/Commands/Assert.cs index d9f3d2c..db3c505 100644 --- a/src/Commands/Commands/Assert.cs +++ b/src/Commands/Commands/Assert.cs @@ -32,21 +32,17 @@ public static void NotNullOrInvalid(IEnumerable values, Regex? regex, st { NotNull(values, nameof(values)); - if (regex != null) - { - var valueCount = 0; - foreach (var value in values) - { - valueCount++; + if (regex == null) return; - NotNullOrEmpty(value, argumentExpression); + if (!values.Any()) + throw new ArgumentException($"The argument '{argumentExpression}' must not be empty.", argumentExpression); - if (!regex.IsMatch(value)) - throw new ArgumentException($"The argument '{argumentExpression}' must match the validation expression '{regex}'", argumentExpression); - } + foreach (var value in values) + { + NotNullOrEmpty(value, argumentExpression); - if (valueCount == 0) - throw new ArgumentException($"The argument '{argumentExpression}' must not be empty.", argumentExpression); + if (!regex.IsMatch(value)) + throw new ArgumentException($"The argument '{argumentExpression}' must match the validation expression '{regex}'", argumentExpression); } } } diff --git a/src/Commands/Commands/Components/Command.cs b/src/Commands/Commands/Components/Command.cs index c17232a..3b31706 100644 --- a/src/Commands/Commands/Components/Command.cs +++ b/src/Commands/Commands/Components/Command.cs @@ -155,12 +155,9 @@ private Command(IActivator activator, ComponentOptions options) (MinLength, MaxLength) = Utilities.GetLength(parameters); - for (var i = 0; i < parameters.Length; i++) + for (int i = parameters.Length - 1; i < -1; i--) { - var parameter = parameters[i]; - - if (parameter.IsRemainder && i != parameters.Length - 1) - throw new ComponentFormatException("Remainder-marked parameters must be the last parameter in the parameter list of a command."); + if (parameters[i].IsRemainder) throw new ComponentFormatException("Remainder-marked parameters must be the last parameter in the parameter list of a command."); } Parameters = parameters; @@ -283,7 +280,7 @@ static TestResult Compare(ITest test, TestResultType targetType, Exception excep /// Defines if the arguments of the command should be included in the output. public string GetFullName(bool includeArguments) { - var sb = new StringBuilder(); + var sb = new ValueStringBuilder(stackalloc char[64]); if (Parent?.Name != null) { @@ -314,18 +311,9 @@ public string GetFullName(bool includeArguments) public string GetFullName() => GetFullName(true); - /// - public float GetScore() - { - var score = 1.0f; - - foreach (var parameter in Parameters) - score += parameter.GetScore(); - - score += Attributes.FirstOrDefault()?.Priority ?? 0; - return score; - } + /// + public float GetScore() => 1 + Parameters.Sum(p => p.GetScore()) + Attributes.FirstOrDefault()?.Priority ?? 0; /// public int CompareTo(IComponent? component) diff --git a/src/Commands/Commands/Components/CommandGroup.cs b/src/Commands/Commands/Components/CommandGroup.cs index 14e54e8..9bfe0c1 100644 --- a/src/Commands/Commands/Components/CommandGroup.cs +++ b/src/Commands/Commands/Components/CommandGroup.cs @@ -165,16 +165,18 @@ public IEnumerable GetConditions() /// public string GetFullName() { - var sb = new StringBuilder(); + var sb = new ValueStringBuilder(stackalloc char[64]); if (Parent?.Name != null) { sb.Append(Parent.GetFullName()); if (Name != null) + { sb.Append(' '); + sb.Append(Name); + } - sb.Append(Name); } else if (Name != null) sb.Append(Name); @@ -236,7 +238,7 @@ public override IComponent[] Find(Arguments args) /// public override string ToString() { - var sb = new StringBuilder(); + var sb = new ValueStringBuilder(stackalloc char[64]); if (Parent != null) { diff --git a/src/Commands/Commands/Components/ComponentSet.cs b/src/Commands/Commands/Components/ComponentSet.cs index 24b6b01..264403d 100644 --- a/src/Commands/Commands/Components/ComponentSet.cs +++ b/src/Commands/Commands/Components/ComponentSet.cs @@ -39,7 +39,7 @@ public IEnumerable GetCommands(Func predicate, bool brow if (component is Command command && predicate(command)) discovered.Add(command); - if (component is CommandGroup grp) + else if (component is CommandGroup grp) discovered.AddRange(grp.GetCommands(predicate, browseNestedComponents)); } @@ -166,35 +166,40 @@ internal SpanStateEnumerator GetSpanEnumerator() // This method is used to remove a range of components from the array of components with low allocation overhead. internal int UnbindRange(IEnumerable components) { + IComponent[] copy = new IComponent[_items.Length]; + lock (_items) { - var mutations = 0; - - var copy = new List(_items); + int mutations = 0; foreach (var component in components) { - Assert.NotNull(component, nameof(component)); - - var output = copy.Remove(component); - - if (output) + for (int i = 0; i < _items.Length; i++) { - mutations += 1; - component.Unbind(); + if (_items[i] == component) + { + component.Unbind(); + mutations++; + break; + } + else + { + copy[i - mutations] = _items[i]; + } } } if (mutations > 0) { + Array.Resize(ref copy, _items.Length - mutations); + _items = copy; + _mutateTree?.Invoke(components, true); - _items = [.. copy]; } return mutations; } } - // This method is used to add a range of components to the array of components with low allocation overhead. internal int BindRange(IEnumerable components, bool extracted) { diff --git a/src/Commands/Commands/Execution/Arguments.cs b/src/Commands/Commands/Execution/Arguments.cs index 5191e16..a9e8903 100644 --- a/src/Commands/Commands/Execution/Arguments.cs +++ b/src/Commands/Commands/Execution/Arguments.cs @@ -1,4 +1,6 @@ -namespace Commands; +using System; + +namespace Commands; /// /// Represents a mechanism for querying arguments while searching or parsing a command. @@ -94,27 +96,21 @@ public Arguments(IEnumerable> args) { //_namedArgs = new(comparer); - var keySet = Array.Empty(); - var flagSet = Array.Empty>(); + int capacityHint = args is ICollection> col ? col.Count / 2 : 2; + + List> flags = new(capacityHint); + List keys = new(capacityHint); foreach (var kvp in args) { - if (kvp.Value == null) - { - Array.Resize(ref keySet, keySet.Length + 1); - - keySet[keySet.Length - 1] = kvp.Key; - } + if (kvp.Value is null) + keys.Add(kvp.Key); else - { - Array.Resize(ref flagSet, flagSet.Length + 1); - - flagSet[flagSet.Length - 1] = kvp; - } + flags.Add(kvp); } - _keys = keySet; - _flaggedKeys = flagSet; + _flaggedKeys = [.. flags]; + _keys = [.. keys]; RemainingLength = _keys.Length + _flaggedKeys.Length; } @@ -162,12 +158,7 @@ internal readonly string TakeRemaining(string parameterName, char separator) #endif internal readonly IEnumerable TakeRemaining(string parameterName) - { - if (TryGetValueInternal(parameterName, out var value)) - return [value!, .. _keys.Skip(_index)]; - - return _keys.Skip(_index); - } + => TryGetValueInternal(parameterName, out var value) ? [value!, .. _keys.Skip(_index)] : _keys.Skip(_index); internal void SetIndex(int index) { @@ -190,145 +181,110 @@ private readonly bool TryGetValueInternal(string parameterName, out object? valu return false; } - private static IEnumerable> ReadInternal(string[] input) + private static List> ReadInternal(string[] input) { - if (input.Length is 0) - yield break; + List> result = []; - if (input.Length is 1) + if (input.Length == 0) + return result; + + if (input.Length == 1) { - yield return new(input[0], null); - yield break; + result.Add(new(input[0], null)); + return result; } - // Reserved for joining arguments. - var openState = 0; - var concatenating = false; - var concatenation = new List(); - - // Reserved for named arguments. + bool concatenating = false; string? name = null; + int openQuotes = 0; + + Span buffer = stackalloc char[256]; + ValueStringBuilder vsb = new(buffer); foreach (var argument in input) { + ReadOnlySpan span = argument.AsSpan(); + if (concatenating) { - if (argument.StartsWith(U0022)) - { - openState++; - - concatenation.Add(argument); + if (span[0] == '"') + openQuotes++; - if (argument.Length > 1) - { -#if NET8_0_OR_GREATER - if (argument[1..].Contains(U0022)) - openState--; -#else - if (argument.Remove(0, 1).Contains(U0022)) - openState--; -#endif - } + if (span.Length > 1 && span.Slice(1).IndexOf('"') != -1) + openQuotes--; - continue; - } + vsb.Append(span); + vsb.Append(' '); - if (argument.EndsWith(U0022)) + if (span[span.Length - 1] == '"') { - if (openState is 0) + openQuotes--; + + if (openQuotes <= 0) { concatenating = false; - - concatenation.Add(argument); + string resultStr = vsb.ToString(); if (name is null) - yield return new(string.Join(U0020, concatenation), null); + result.Add(new(resultStr, null)); else { - yield return new(name, string.Join(U0020, concatenation)); - + result.Add(new(name, resultStr)); name = null; } - concatenation.Clear(); + buffer.Clear(); + vsb = new(buffer); } - else - { - openState--; - - concatenation.Add(argument); - } - - continue; } - concatenation.Add(argument); + continue; } - else - { - if (argument.StartsWith(U002D)) - { - if (argument.Length > 1) - { -#if NET8_0_OR_GREATER - if (argument[1] is U002D) - { - if (name is not null) - yield return new(name, null); - - name = argument[2..]; - - continue; - } - } - - yield return new(argument[1..], null); -#else - if (argument[1] == U002D[0]) - { - if (name is not null) - yield return new(name, null); - - name = argument.Remove(0, 2); - - continue; - } - } - - yield return new(argument.Remove(0, 1), null); -#endif - - continue; - } - if (argument.StartsWith(U0022) && !argument.EndsWith(U0022)) - { - concatenating = true; + if (span[0] == '-' && span[1] == '-') + { + if (name is not null) + result.Add(new(name, null)); - concatenation.Add(argument); + name = span.Slice(2).ToString(); + continue; + } - continue; - } + if (span[0] == '"' && span[span.Length - 1] != '"') + { + concatenating = true; + openQuotes = 1; - if (name is null) - yield return new(argument, null); - else - { - yield return new(name, argument); + vsb.Append(span); + vsb.Append(' '); + continue; + } - name = null; - } + if (name is null) + { + result.Add(new(argument, null)); + } + else + { + result.Add(new(name, argument)); + name = null; } + + buffer.Clear(); + vsb = new(buffer); } - // If concatenation is still filled on escaping the sequence, add as last argument. - if (concatenation.Count != 0) + string final = vsb.ToString(); + + if (final.Length != 0) { if (name is null) - yield return new(string.Join(U0020, concatenation), null); + result.Add(new(final, null)); else - yield return new(name, string.Join(U0020, concatenation)); + result.Add(new(name, final)); } + + return result; } #endregion diff --git a/src/Commands/Commands/Utilities.cs b/src/Commands/Commands/Utilities.cs index e440013..e016830 100644 --- a/src/Commands/Commands/Utilities.cs +++ b/src/Commands/Commands/Utilities.cs @@ -1,4 +1,6 @@ -using Commands.Conditions; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using Commands.Conditions; using Commands.Parsing; namespace Commands; @@ -16,11 +18,7 @@ public static class Utilities /// Determines whether the current iteration of additions is nested or not. /// A new containing all created component groups in the initial collection of types. public static IEnumerable GetComponents(this IEnumerable types, ComponentOptions? options = null, bool isNested = false) - { - options ??= ComponentOptions.Default; - - return GetComponents(options, types, isNested); - } + => GetComponents(options ?? ComponentOptions.Default, types, isNested); #region Internals @@ -37,6 +35,38 @@ public static IEnumerable GetComponents(this IEnumerable typ return default; } +#if NET8_0_OR_GREATER + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void CopyTo(ref T[] array, T item) + { + T[] newArray = GC.AllocateUninitializedArray(array.Length + 1); + + ref T source = ref MemoryMarshal.GetArrayDataReference(array), destination = ref MemoryMarshal.GetArrayDataReference(newArray); + + for (int i = 0; i < array.Length; i++) Unsafe.Add(ref destination, i) = Unsafe.Add(ref source, i); + + Unsafe.Add(ref destination, array.Length) = item; + + array = newArray; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void CopyTo(ref T[] array, T[] items) + { + T[] newArray = GC.AllocateUninitializedArray(array.Length + items.Length); + + ref T sourceA = ref MemoryMarshal.GetArrayDataReference(array), sourceB = ref MemoryMarshal.GetArrayDataReference(items), destination = ref MemoryMarshal.GetArrayDataReference(newArray); + + for (int i = 0; i < array.Length; i++) Unsafe.Add(ref destination, i) = Unsafe.Add(ref sourceA, i); + + for (int i = 0; i < items.Length; i++) Unsafe.Add(ref destination, array.Length + i) = Unsafe.Add(ref sourceB, i); + + array = newArray; + } + +#else + internal static void CopyTo(ref T[] array, T item) { var newArray = new T[array.Length + 1]; @@ -62,6 +92,8 @@ internal static void CopyTo(ref T[] array, T[] items) array = newArray; } +#endif + #endregion #region Execution @@ -302,5 +334,5 @@ internal static IEnumerable GetAttributes(this ICustomAttributeProvid #endregion - #endregion +#endregion } diff --git a/src/Commands/ValueStringBuilder.cs b/src/Commands/ValueStringBuilder.cs new file mode 100644 index 0000000..8725a4f --- /dev/null +++ b/src/Commands/ValueStringBuilder.cs @@ -0,0 +1,93 @@ +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Buffers; + +namespace Commands; + +/// +/// A cheaper single-use alternative to , where the intitial backing store is a of type .
+/// The stack-allocated buffer will be used until it overflows, at which point a larger array will be rented from to minimize allocation.
+/// When is called, the builder will return the rented array to the pool, destroying the builder automatically. +///
+internal ref struct ValueStringBuilder(Span initialBuffer) +{ + private Span _span = initialBuffer; + private int _position; + private char[]? _rentedArray; + + /// + /// Appends a single character to the builder. + /// + /// The character to append. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Append(char c) + { + if (_position >= _span.Length) + Grow(1); + + // Skip bounds check, Grow(1) will always succeed. + Unsafe.Add(ref MemoryMarshal.GetReference(_span), _position++) = c; + } + + /// + /// Appends a string to the builder. + /// + /// The string to append. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Append(string s) => Append(s.AsSpan()); + + /// + /// Appends a span to the builder. + /// + /// The span to append. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Append(ReadOnlySpan s) + { + int length = s.Length; + + if (_position + length > _span.Length) + Grow(length); + + s.CopyTo(_span.Slice(_position)); + + _position += length; + } + + /// + /// Grows the internal buffer to accommodate additional characters. + /// + /// The minimum number of additional characters required. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void Grow(int requested) + { + int newCapacity = _span.Length * 2; + + if (newCapacity < _position + requested) + newCapacity = _position + requested; + + char[] newArray = ArrayPool.Shared.Rent(newCapacity); + + _span.CopyTo(newArray.AsSpan(0, _position)); + + if (_rentedArray != null) + ArrayPool.Shared.Return(_rentedArray); + + _span = _rentedArray = newArray; + } + + /// + /// Converts the current contents of the builder to a string, and destroys the builder by returning the rented array to the pool.
+ /// Only call at the end of the builder's lifetime, as it will no longer be usable after this call.
+ ///
+ /// The string representation of the builder's contents. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override readonly string ToString() + { + string s = _span.Slice(0, _span.IndexOf('\0')).ToString(); + + if (_rentedArray != null) + ArrayPool.Shared.Return(_rentedArray); + + return s; + } +}