From 5fbc164ed9f8ce440cfecafaa879ac6341db57ec Mon Sep 17 00:00:00 2001 From: Ladi Prosek Date: Tue, 5 Jan 2021 11:11:33 +0100 Subject: [PATCH 01/14] Add the StringTools library project --- src/StringTools/AssemblyInfo.cs | 13 + src/StringTools/InternableString.Simple.cs | 231 ++++++++++++ src/StringTools/InternableString.cs | 331 ++++++++++++++++++ .../SpanBasedStringBuilder.Simple.cs | 153 ++++++++ src/StringTools/SpanBasedStringBuilder.cs | 261 ++++++++++++++ src/StringTools/StringTools.cs | 108 ++++++ src/StringTools/StringTools.csproj | 39 +++ src/StringTools/StringTools.pkgdef | 7 + src/StringTools/WeakStringCache.Concurrent.cs | 124 +++++++ src/StringTools/WeakStringCache.Locking.cs | 125 +++++++ src/StringTools/WeakStringCache.cs | 141 ++++++++ src/StringTools/WeakStringCacheInterner.cs | 180 ++++++++++ 12 files changed, 1713 insertions(+) create mode 100644 src/StringTools/AssemblyInfo.cs create mode 100644 src/StringTools/InternableString.Simple.cs create mode 100644 src/StringTools/InternableString.cs create mode 100644 src/StringTools/SpanBasedStringBuilder.Simple.cs create mode 100644 src/StringTools/SpanBasedStringBuilder.cs create mode 100644 src/StringTools/StringTools.cs create mode 100644 src/StringTools/StringTools.csproj create mode 100644 src/StringTools/StringTools.pkgdef create mode 100644 src/StringTools/WeakStringCache.Concurrent.cs create mode 100644 src/StringTools/WeakStringCache.Locking.cs create mode 100644 src/StringTools/WeakStringCache.cs create mode 100644 src/StringTools/WeakStringCacheInterner.cs diff --git a/src/StringTools/AssemblyInfo.cs b/src/StringTools/AssemblyInfo.cs new file mode 100644 index 00000000000..0a8c0ee0a72 --- /dev/null +++ b/src/StringTools/AssemblyInfo.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +[assembly: InternalsVisibleTo("Microsoft.NET.StringTools.UnitTests, PublicKey=002400000480000094000000060200000024000052534131000400000100010015c01ae1f50e8cc09ba9eac9147cf8fd9fce2cfe9f8dce4f7301c4132ca9fb50ce8cbf1df4dc18dd4d210e4345c744ecb3365ed327efdbc52603faa5e21daa11234c8c4a73e51f03bf192544581ebe107adee3a34928e39d04e524a9ce729d5090bfd7dad9d10c722c0def9ccc08ff0a03790e48bcd1f9b6c476063e1966a1c4")] +[assembly: InternalsVisibleTo("Microsoft.NET.StringTools.net35.UnitTests, PublicKey=002400000480000094000000060200000024000052534131000400000100010007d1fa57c4aed9f0a32e84aa0faefd0de9e8fd6aec8f87fb03766c834c99921eb23be79ad9d5dcc1dd9ad236132102900b723cf980957fc4e177108fc607774f29e8320e92ea05ece4e821c0a5efe8f1645c4c0c93c1ab99285d622caa652c1dfad63d745d6f2de5f17e5eaf0fc4963d261c8a12436518206dc093344d5ad293")] +[assembly: InternalsVisibleTo("Microsoft.NET.StringTools.Benchmark, PublicKey=002400000480000094000000060200000024000052534131000400000100010007d1fa57c4aed9f0a32e84aa0faefd0de9e8fd6aec8f87fb03766c834c99921eb23be79ad9d5dcc1dd9ad236132102900b723cf980957fc4e177108fc607774f29e8320e92ea05ece4e821c0a5efe8f1645c4c0c93c1ab99285d622caa652c1dfad63d745d6f2de5f17e5eaf0fc4963d261c8a12436518206dc093344d5ad293")] + +[assembly: ComVisible(false)] +[assembly: CLSCompliant(true)] diff --git a/src/StringTools/InternableString.Simple.cs b/src/StringTools/InternableString.Simple.cs new file mode 100644 index 00000000000..88126da5c6f --- /dev/null +++ b/src/StringTools/InternableString.Simple.cs @@ -0,0 +1,231 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq.Expressions; +using System.Text; + +namespace System +{ + /// + /// A bare minimum and inefficient version of MemoryExtensions as provided in System.Memory on .NET 4.5. + /// + public static class MemoryExtensions + { + public static string AsSpan(this T[] array, int start, int length) + { + if (array is char[] charArray) + { + return new string(charArray, start, length); + } + throw new ArgumentException(nameof(array)); + } + } +} + +namespace Microsoft.NET.StringTools +{ + /// + /// Represents a string that can be converted to System.String with interning, i.e. by returning an existing string if it has been seen before + /// and is still tracked in the intern table. + /// + /// + /// This is a simple and inefficient implementation compatible with .NET Framework 3.5. + /// + internal ref struct InternableString + { + /// + /// Enumerator for the top-level struct. Enumerates characters of the string. + /// + public ref struct Enumerator + { + /// + /// The InternableString being enumerated. + /// + private InternableString _string; + + /// + /// Index of the current character, -1 if MoveNext has not been called yet. + /// + private int _charIndex; + + public Enumerator(ref InternableString spanBuilder) + { + _string = spanBuilder; + _charIndex = -1; + } + + /// + /// Returns the current character. + /// + public char Current => (_string._builder == null ? _string.FirstString[_charIndex] : _string._builder[_charIndex]); + + /// + /// Moves to the next character. + /// + /// True if there is another character, false if the enumerator reached the end. + public bool MoveNext() + { + int newIndex = _charIndex + 1; + if (newIndex < _string.Length) + { + _charIndex = newIndex; + return true; + } + return false; + } + } + + /// + /// If this instance wraps a StringBuilder, it uses this backing field. + /// + private StringBuilder? _builder; + + /// + /// If this instance represents one contiguous string, it may be held in this field. + /// + private string? _firstString; + + /// + /// A convenience getter to ensure that we always operate on a non-null string. + /// + private string FirstString => _firstString ?? string.Empty; + + /// + /// Constructs a new InternableString wrapping the given string. + /// + /// The string to wrap, must be non-null. + internal InternableString(string str) + { + if (str == null) + { + throw new ArgumentNullException(nameof(str)); + } + _builder = null; + _firstString = str; + } + + /// + /// Constructs a new InternableString wrapping the given SpanBasedStringBuilder. + /// + internal InternableString(SpanBasedStringBuilder builder) + { + _builder = builder.Builder; + _firstString = null; + } + + /// + /// Gets the length of the string. + /// + public int Length => (_builder == null ? FirstString.Length : _builder.Length); + + /// + /// Creates a new enumerator for enumerating characters in this string. Does not allocate. + /// + /// The enumerator. + public Enumerator GetEnumerator() + { + return new Enumerator(ref this); + } + + /// + /// Returns true if the string is equal to another string by ordinal comparison. + /// + /// Another string. + /// True if this string is equal to . + public bool Equals(string other) + { + if (other.Length != Length) + { + return false; + } + + if (_firstString != null) + { + return _firstString.Equals(other); + } + if (_builder != null) + { + for (int i = 0; i < other.Length; i++) + { + // Note: This indexing into the StringBuilder could be O(N). We prefer it over allocating + // a new string with ToString(). + if (other[i] != _builder[i]) + { + return false; + } + } + } + return true; + } + + /// + /// Returns a System.String representing this string. Allocates memory unless this InternableString was created by wrapping a + /// System.String in which case the original string is returned. + /// + /// The string. + public string ExpensiveConvertToString() + { + // Special case: if we hold just one string, we can directly return it. + if (_firstString != null) + { + return _firstString; + } + return _builder?.ToString() ?? string.Empty; + } + + /// + /// Returns true if this InternableString wraps a System.String and the same System.String is passed as the argument. + /// + /// The string to compare to. + /// True is this instance wraps the given string. + public bool ReferenceEquals(string str) + { + return Object.ReferenceEquals(str, _firstString); + } + + /// + /// Converts this instance to a System.String while first searching for a match in the intern table. + /// + /// + /// May allocate depending on whether the string has already been interned. + /// + public override unsafe string ToString() + { + return WeakStringCacheInterner.Instance.InternableToString(ref this); + } + + /// + /// Implements the simple yet very decently performing djb2 hash function (xor version). + /// + /// A stable hashcode of the string represented by this instance. + public override int GetHashCode() + { + int hashCode = 5381; + + if (_firstString != null) + { + foreach (char ch in _firstString) + { + unchecked + { + hashCode = hashCode * 33 ^ ch; + } + } + } + else if (_builder != null) + { + for (int i = 0; i < _builder.Length; i++) + { + unchecked + { + hashCode = hashCode * 33 ^ _builder[i]; + } + } + } + return hashCode; + } + } +} diff --git a/src/StringTools/InternableString.cs b/src/StringTools/InternableString.cs new file mode 100644 index 00000000000..ca8fa75ef48 --- /dev/null +++ b/src/StringTools/InternableString.cs @@ -0,0 +1,331 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +namespace Microsoft.NET.StringTools +{ + /// + /// Represents a string that can be converted to System.String with interning, i.e. by returning an existing string if it has been seen before + /// and is still tracked in the intern table. + /// + internal ref struct InternableString + { + /// + /// Enumerator for the top-level struct. Enumerates characters of the string. + /// + public ref struct Enumerator + { + /// + /// The InternableString being enumerated. + /// + private InternableString _string; + + /// + /// Index of the current span, -1 represents the inline span. + /// + private int _spanIndex; + + /// + /// Index of the current character in the current span, -1 if MoveNext has not been called yet. + /// + private int _charIndex; + + internal Enumerator(ref InternableString str) + { + _string = str; + _spanIndex = -1; + _charIndex = -1; + } + + /// + /// Returns the current character. + /// + public ref readonly char Current + { + get + { + if (_spanIndex == -1) + { + return ref _string._inlineSpan[_charIndex]; + } + ReadOnlyMemory span = _string._spans![_spanIndex]; + return ref span.Span[_charIndex]; + } + } + + /// + /// Moves to the next character. + /// + /// True if there is another character, false if the enumerator reached the end. + public bool MoveNext() + { + int newCharIndex = _charIndex + 1; + if (_spanIndex == -1) + { + if (newCharIndex < _string._inlineSpan.Length) + { + _charIndex = newCharIndex; + return true; + } + _spanIndex = 0; + newCharIndex = 0; + } + + if (_string._spans != null) + { + while (_spanIndex < _string._spans.Count) + { + if (newCharIndex < _string._spans[_spanIndex].Length) + { + _charIndex = newCharIndex; + return true; + } + _spanIndex++; + newCharIndex = 0; + } + } + return false; + } + } + + /// + /// The span held by this struct, inline to be able to represent . May be empty. + /// + private readonly ReadOnlySpan _inlineSpan; + +#if NETSTANDARD + /// + /// .NET Core does not keep a reference to the containing object in . In particular, + /// it cannot recover the string if the span represents one. We have to hold the reference separately to be able to + /// roundtrip String->InternableString->String without allocating a new String. + /// + private string? _inlineSpanString; +#endif + + /// + /// Additional spans held by this struct. May be null. + /// + private List>? _spans; + + /// + /// Constructs a new InternableString wrapping the given . + /// + /// The span to wrap. + /// + /// When wrapping a span representing an entire System.String, use Internable(string) for optimum performance. + /// + internal InternableString(ReadOnlySpan span) + { + _inlineSpan = span; + _spans = null; + Length = span.Length; +#if NETSTANDARD + _inlineSpanString = null; +#endif + } + + /// + /// Constructs a new InternableString wrapping the given string. + /// + /// The string to wrap, must be non-null. + internal InternableString(string str) + { + if (str == null) + { + throw new ArgumentNullException(nameof(str)); + } + + _inlineSpan = str.AsSpan(); + _spans = null; + Length = str.Length; +#if NETSTANDARD + _inlineSpanString = str; +#endif + } + + /// + /// Constructs a new InternableString wrapping the given SpanBasedStringBuilder. + /// + internal InternableString(SpanBasedStringBuilder stringBuilder) + { + _inlineSpan = default(ReadOnlySpan); + _spans = stringBuilder.Spans; + Length = stringBuilder.Length; +#if NETSTANDARD + _inlineSpanString = null; +#endif + } + + /// + /// Gets the length of the string. + /// + public int Length { get; private set; } + + /// + /// Creates a new enumerator for enumerating characters in this string. Does not allocate. + /// + /// The enumerator. + public Enumerator GetEnumerator() + { + return new Enumerator(ref this); + } + + /// + /// Returns true if the string is equal to another string by ordinal comparison. + /// + /// Another string. + /// True if this string is equal to . + public bool Equals(string other) + { + if (other.Length != Length) + { + return false; + } + + if (_inlineSpan.SequenceCompareTo(other.AsSpan(0, _inlineSpan.Length)) != 0) + { + return false; + } + + if (_spans != null) + { + int otherStart = _inlineSpan.Length; + foreach (ReadOnlyMemory span in _spans) + { + if (span.Span.SequenceCompareTo(other.AsSpan(otherStart, span.Length)) != 0) + { + return false; + } + otherStart += span.Length; + } + } + return true; + } + + /// + /// Returns a System.String representing this string. Allocates memory unless this InternableString was created by wrapping a + /// System.String in which case the original string is returned. + /// + /// The string. + public unsafe string ExpensiveConvertToString() + { + if (Length == 0) + { + return string.Empty; + } + + // Special case: if we hold just one string, we can directly return it. + if (_inlineSpan.Length == Length) + { +#if NETSTANDARD + if (_inlineSpanString != null) + { + return _inlineSpanString; + } +#else + return _inlineSpan.ToString(); +#endif + } + if (_inlineSpan.IsEmpty && _spans?[0].Length == Length) + { + return _spans[0].ToString(); + } + + // In all other cases we create a new string instance and concatenate all spans into it. Note that while technically mutating + // the System.String, the technique is generally considered safe as we are the sole owners of the new object. It is important + // to initialize the string with the '\0' characters as this hits an optimized code path in the runtime. + string result = new string((char)0, Length); + + fixed (char* resultPtr = result) + { + char* destPtr = resultPtr; + if (!_inlineSpan.IsEmpty) + { + fixed (char* sourcePtr = _inlineSpan) + { + Unsafe.CopyBlockUnaligned(destPtr, sourcePtr, 2 * (uint)_inlineSpan.Length); + } + destPtr += _inlineSpan.Length; + } + + if (_spans != null) + { + foreach (ReadOnlyMemory span in _spans) + { + if (!span.IsEmpty) + { + fixed (char* sourcePtr = span.Span) + { + Unsafe.CopyBlockUnaligned(destPtr, sourcePtr, 2 * (uint)span.Length); + } + destPtr += span.Length; + } + } + } + } + return result; + } + + /// + /// Returns true if this InternableString wraps a System.String and the same System.String is passed as the argument. + /// + /// The string to compare to. + /// True is this instance wraps the given string. + public bool ReferenceEquals(string str) + { + if (_inlineSpan.Length == Length) + { + return _inlineSpan == str.AsSpan(); + } + if (_inlineSpan.IsEmpty && _spans?.Count == 1 && _spans[0].Length == Length) + { + return _spans[0].Span == str.AsSpan(); + } + return false; + } + + /// + /// Converts this instance to a System.String while first searching for a match in the intern table. + /// + /// + /// May allocate depending on whether the string has already been interned. + /// + public override string ToString() + { + return WeakStringCacheInterner.Instance.InternableToString(ref this); + } + + /// + /// Implements the simple yet very decently performing djb2 hash function (xor version). + /// + /// A stable hashcode of the string represented by this instance. + public override unsafe int GetHashCode() + { + int hashCode = 5381; + fixed (char* charPtr = _inlineSpan) + { + for (int i = 0; i < _inlineSpan.Length; i++) + { + hashCode = unchecked(hashCode * 33 ^ charPtr[i]); + } + } + if (_spans != null) + { + foreach (ReadOnlyMemory span in _spans) + { + fixed (char* charPtr = span.Span) + { + for (int i = 0; i < span.Length; i++) + { + hashCode = unchecked(hashCode * 33 ^ charPtr[i]); + } + } + } + } + return hashCode; + } + } +} diff --git a/src/StringTools/SpanBasedStringBuilder.Simple.cs b/src/StringTools/SpanBasedStringBuilder.Simple.cs new file mode 100644 index 00000000000..86e38c2907f --- /dev/null +++ b/src/StringTools/SpanBasedStringBuilder.Simple.cs @@ -0,0 +1,153 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Text; + +namespace Microsoft.NET.StringTools +{ + /// + /// A simple version of SpanBasedStringBuilder to be used on .NET Framework 3.5. Wraps a . + /// + public class SpanBasedStringBuilder : IDisposable + { + /// + /// Enumerator for the top-level struct. Enumerates characters of the string. + /// + public struct Enumerator + { + /// + /// The StringBuilder being enumerated. + /// + private StringBuilder _builder; + + /// + /// Index of the current character, -1 if MoveNext has not been called yet. + /// + private int _charIndex; + + public Enumerator(StringBuilder builder) + { + _builder = builder; + _charIndex = -1; + } + + /// + /// Returns the current character. + /// + public char Current => _builder[_charIndex]; + + /// + /// Moves to the next character. + /// + /// True if there is another character, false if the enumerator reached the end. + public bool MoveNext() + { + int newIndex = _charIndex + 1; + if (newIndex < _builder.Length) + { + _charIndex = newIndex; + return true; + } + return false; + } + } + + /// + /// The backing StringBuilder. + /// + private StringBuilder _builder; + + internal StringBuilder Builder => _builder; + + /// + /// Constructs a new SpanBasedStringBuilder containing the given string. + /// + /// The string to wrap, must be non-null. + public SpanBasedStringBuilder(string str) + : this() + { + if (str == null) + { + throw new ArgumentNullException(nameof(str)); + } + Append(str); + } + + /// + /// Constructs a new empty SpanBasedStringBuilder with the given expected number of spans. + /// + public SpanBasedStringBuilder(int capacity = 4) + { + // Since we're using StringBuilder as the backing store in this implementation, our capacity is expressed + // in number of characters rather than number of spans. We use 128 as a reasonable expected multiplier to + // go from one to the other, i.e. by default we'll preallocate a 512-character StringBuilder. + _builder = new StringBuilder(capacity * 128); + } + + /// + /// Gets the length of the string. + /// + public int Length => _builder.Length; + + /// + /// Creates a new enumerator for enumerating characters in this string. Does not allocate. + /// + /// The enumerator. + public Enumerator GetEnumerator() + { + return new Enumerator(_builder); + } + + /// + /// Converts this instance to a System.String while first searching for a match in the intern table. + /// + /// + /// May allocate depending on whether the string has already been interned. + /// + public override string ToString() + { + return new InternableString(this).ToString(); + } + + /// + /// Releases this instance. + /// + public void Dispose() + { + Strings.ReturnSpanBasedStringBuilder(this); + } + + #region Public mutating methods + + /// + /// Appends a string. + /// + /// The string to append. + internal void Append(string value) + { + _builder.Append(value); + } + + /// + /// Appends a substring. + /// + /// The string to append. + /// The start index of the substring within to append. + /// The length of the substring to append. + internal void Append(string value, int startIndex, int count) + { + _builder.Append(value, startIndex, count); + } + + /// + /// Clears this instance making it represent an empty string. + /// + public void Clear() + { + _builder.Length = 0; + } + + #endregion + } +} diff --git a/src/StringTools/SpanBasedStringBuilder.cs b/src/StringTools/SpanBasedStringBuilder.cs new file mode 100644 index 00000000000..2d388641f85 --- /dev/null +++ b/src/StringTools/SpanBasedStringBuilder.cs @@ -0,0 +1,261 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; + +namespace Microsoft.NET.StringTools +{ + /// + /// A StringBuilder replacement that keeps a list of spans making up the intermediate string rather + /// than a copy of its characters. This has positive impact on both memory (no need to allocate space for the intermediate string) + /// and time (no need to copy characters to the intermediate string). + /// + /// + /// The method tries to intern the resulting string without even allocating it if it's already interned. + /// Use to take advantage of pooling to eliminate allocation overhead of this class. + /// + public class SpanBasedStringBuilder : IDisposable + { + /// + /// Enumerator for the top-level class. Enumerates characters of the string. + /// + public struct Enumerator + { + /// + /// The spans being enumerated. + /// + private readonly List> _spans; + + /// + /// Index of the current span. + /// + private int _spanIndex; + + /// + /// Index of the current character in the current span, -1 if MoveNext has not been called yet. + /// + private int _charIndex; + + internal Enumerator(List> spans) + { + _spans = spans; + _spanIndex = 0; + _charIndex = -1; + } + + /// + /// Returns the current character. + /// + public readonly char Current + { + get + { + ReadOnlyMemory span = _spans[_spanIndex]; + return span.Span[_charIndex]; + } + } + + /// + /// Moves to the next character. + /// + /// True if there is another character, false if the enumerator reached the end. + public bool MoveNext() + { + int newCharIndex = _charIndex + 1; + while (_spanIndex < _spans.Count) + { + if (newCharIndex < _spans[_spanIndex].Length) + { + _charIndex = newCharIndex; + return true; + } + _spanIndex++; + newCharIndex = 0; + } + return false; + } + } + + /// + /// Spans making up the rope. + /// + private readonly List> _spans; + + /// + /// Internal getter to get the list of spans out of the SpanBasedStringBuilder. + /// + internal List> Spans => _spans; + + /// + /// Constructs a new SpanBasedStringBuilder containing the given string. + /// + /// The string to wrap, must be non-null. + public SpanBasedStringBuilder(string str) + : this() + { + if (str == null) + { + throw new ArgumentNullException(nameof(str)); + } + Append(str); + } + + /// + /// Constructs a new empty SpanBasedStringBuilder with the given expected number of spans. + /// + public SpanBasedStringBuilder(int capacity = 4) + { + _spans = new List>(capacity); + Length = 0; + } + + /// + /// Gets the length of the string. + /// + public int Length { get; private set; } + + /// + /// Gets the capacity of the SpanBasedStringBuilder in terms of number of spans it can hold without allocating. + /// + public int Capacity => _spans.Capacity; + + /// + /// Creates a new enumerator for enumerating characters in this string. Does not allocate. + /// + /// The enumerator. + public Enumerator GetEnumerator() + { + return new Enumerator(_spans); + } + + /// + /// Converts this instance to a System.String while first searching for a match in the intern table. + /// + /// + /// May allocate depending on whether the string has already been interned. + /// + public override string ToString() + { + return new InternableString(this).ToString(); + } + + /// + /// Releases this instance. + /// + public void Dispose() + { + Strings.ReturnSpanBasedStringBuilder(this); + } + + #region Public mutating methods + + /// + /// Appends a string. + /// + /// The string to append. + public void Append(string value) + { + if (!string.IsNullOrEmpty(value)) + { + _spans.Add(value.AsMemory()); + Length += value.Length; + } + } + + /// + /// Appends a substring. + /// + /// The string to append. + /// The start index of the substring within to append. + /// The length of the substring to append. + public void Append(string value, int startIndex, int count) + { + if (value != null) + { + if (count > 0) + { + _spans.Add(value.AsMemory(startIndex, count)); + Length += count; + } + } + else + { + if (startIndex != 0 || count != 0) + { + throw new ArgumentNullException(nameof(value)); + } + } + } + + /// + /// Removes leading white-space characters from the string. + /// + public void TrimStart() + { + for (int spanIdx = 0; spanIdx < _spans.Count; spanIdx++) + { + ReadOnlySpan span = _spans[spanIdx].Span; + int i = 0; + while (i < span.Length && char.IsWhiteSpace(span[i])) + { + i++; + } + if (i > 0) + { + _spans[spanIdx] = _spans[spanIdx].Slice(i); + Length -= i; + } + if (!_spans[spanIdx].IsEmpty) + { + return; + } + } + } + + /// + /// Removes trailing white-space characters from the string. + /// + public void TrimEnd() + { + for (int spanIdx = _spans.Count - 1; spanIdx >= 0; spanIdx--) + { + ReadOnlySpan span = _spans[spanIdx].Span; + int i = span.Length - 1; + while (i >= 0 && char.IsWhiteSpace(span[i])) + { + i--; + } + if (i + 1 < span.Length) + { + _spans[spanIdx] = _spans[spanIdx].Slice(0, i + 1); + Length -= span.Length - (i + 1); + } + if (!_spans[spanIdx].IsEmpty) + { + return; + } + } + } + + /// + /// Removes leading and trailing white-space characters from the string. + /// + public void Trim() + { + TrimStart(); + TrimEnd(); + } + + /// + /// Clears this instance making it represent an empty string. + /// + public void Clear() + { + _spans.Clear(); + Length = 0; + } + + #endregion + } +} diff --git a/src/StringTools/StringTools.cs b/src/StringTools/StringTools.cs new file mode 100644 index 00000000000..fbe794342b9 --- /dev/null +++ b/src/StringTools/StringTools.cs @@ -0,0 +1,108 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; + +namespace Microsoft.NET.StringTools +{ + public static class Strings + + { + #region Fields + + /// + /// Per-thread instance of the SpanBasedStringBuilder, created lazily. + /// + /// + /// This field serves as a per-thread one-item object pool, which is adequate for most use + /// cases as the builder is not expected to be held for extended periods of time. + /// + [ThreadStatic] + private static SpanBasedStringBuilder? _spanBasedStringBuilder; + + #endregion + + #region Public methods + + /// + /// Interns the given string, keeping only a weak reference to the returned value. + /// + /// The string to intern. + /// A string equal to , could be the same object as . + /// + /// The intern pool does not retain strong references to the strings it's holding so strings are automatically evicted + /// after they become unrooted. This is in contrast to System.String.Intern which holds strings forever. + /// + public static string WeakIntern(string str) + { + InternableString internableString = new InternableString(str); + return WeakStringCacheInterner.Instance.InternableToString(ref internableString); + } + +#if !NET35 + /// + /// Interns the given readonly span of characters, keeping only a weak reference to the returned value. + /// + /// The character span to intern. + /// A string equal to , could be the result of calling ToString() on . + /// + /// The intern pool does not retain strong references to the strings it's holding so strings are automatically evicted + /// after they become unrooted. This is in contrast to System.String.Intern which holds strings forever. + /// + public static string WeakIntern(ReadOnlySpan str) + { + InternableString internableString = new InternableString(str); + return WeakStringCacheInterner.Instance.InternableToString(ref internableString); + } +#endif + + /// + /// Returns a new or recycled . + /// + /// The SpanBasedStringBuilder. + /// + /// Call on the returned instance to recycle it. + /// + public static SpanBasedStringBuilder GetSpanBasedStringBuilder() + { + SpanBasedStringBuilder? stringBuilder = _spanBasedStringBuilder; + if (stringBuilder == null) + { + return new SpanBasedStringBuilder(); + } + else + { + _spanBasedStringBuilder = null; + return stringBuilder; + } + } + + /// + /// Enables diagnostics in the interner. Call to retrieve the diagnostic data. + /// + public static void EnableDiagnostics() + { + WeakStringCacheInterner.Instance.EnableStatistics(); + } + + /// + /// Retrieves the diagnostic data describing the current state of the interner. Make sure to call beforehand. + /// + public static string CreateDiagnosticReport() + { + return WeakStringCacheInterner.Instance.FormatStatistics(); + } + + #endregion + + /// + /// Returns a instance back to the pool if possible. + /// + /// The instance to return. + internal static void ReturnSpanBasedStringBuilder(SpanBasedStringBuilder stringBuilder) + { + stringBuilder.Clear(); + _spanBasedStringBuilder = stringBuilder; + } + } +} diff --git a/src/StringTools/StringTools.csproj b/src/StringTools/StringTools.csproj new file mode 100644 index 00000000000..f3ac3633595 --- /dev/null +++ b/src/StringTools/StringTools.csproj @@ -0,0 +1,39 @@ + + + $(LibraryTargetFrameworks) + $(LibraryTargetFrameworks);net35 + AnyCPU + true + true + 8.0 + Microsoft.NET.StringTools + true + enable + + 1.0.0 + + true + + Microsoft.NET.StringTools + Microsoft.NET.StringTools.net35 + + + + + + + + + + + + + + + + + + + + + diff --git a/src/StringTools/StringTools.pkgdef b/src/StringTools/StringTools.pkgdef new file mode 100644 index 00000000000..4ca09cf37c9 --- /dev/null +++ b/src/StringTools/StringTools.pkgdef @@ -0,0 +1,7 @@ +[$RootKey$\RuntimeConfiguration\dependentAssembly\bindingRedirection\{7FBCE0AF-48AC-46AC-8841-F00D17C63A22}] +"name"="StringTools" +"codeBase"="$BaseInstallDir$\MSBuild\Current\Bin\Microsoft.NET.StringTools.dll" +"publicKeyToken"="b03f5f7f11d50a3a" +"culture"="neutral" +"oldVersion"="0.0.0.0-1.0.0.0" +"newVersion"="1.0.0.0" diff --git a/src/StringTools/WeakStringCache.Concurrent.cs b/src/StringTools/WeakStringCache.Concurrent.cs new file mode 100644 index 00000000000..6110475e946 --- /dev/null +++ b/src/StringTools/WeakStringCache.Concurrent.cs @@ -0,0 +1,124 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Threading; + +namespace Microsoft.NET.StringTools +{ + /// + /// Implements the WeakStringCache functionality on modern .NET versions where ConcurrentDictionary is available. + /// + internal sealed partial class WeakStringCache : IDisposable + { + private readonly ConcurrentDictionary _stringsByHashCode; + + public WeakStringCache() + { + _stringsByHashCode = new ConcurrentDictionary(Environment.ProcessorCount, _initialCapacity); + } + + /// + /// Main entrypoint of this cache. Tries to look up a string that matches the given internable. If it succeeds, returns + /// the string and sets cacheHit to true. If the string is not found, calls ExpensiveConvertToString on the internable, + /// adds the resulting string to the cache, and returns it, setting cacheHit to false. + /// + /// The internable describing the string we're looking for. + /// true if match found in cache, false otherwise. + /// A string matching the given internable. + public string GetOrCreateEntry(ref InternableString internable, out bool cacheHit) + { + int hashCode = internable.GetHashCode(); + + StringWeakHandle handle; + string? result; + + // Get the existing handle from the cache and lock it while we're dereferencing it to prevent a race with the Scavenge + // method running on another thread and freeing the handle from underneath us. + if (_stringsByHashCode.TryGetValue(hashCode, out handle)) + { + lock (handle) + { + result = handle.GetString(ref internable); + if (result != null) + { + cacheHit = true; + return result; + } + + // We have the handle but it's not referencing the right string - create the right string and store it in the handle. + result = internable.ExpensiveConvertToString(); + handle.SetString(result); + + cacheHit = false; + return result; + } + } + + // We don't have the handle in the cache - create the right string, store it in the handle, and add the handle to the cache. + result = internable.ExpensiveConvertToString(); + + handle = new StringWeakHandle(); + handle.SetString(result); + _stringsByHashCode.TryAdd(hashCode, handle); + + // Remove unused handles if our heuristic indicates that it would be productive. + int scavengeThreshold = _scavengeThreshold; + if (_stringsByHashCode.Count >= scavengeThreshold) + { + // Before we start scavenging set _scavengeThreshold to a high value to effectively lock other threads from + // running Scavenge at the same time. + if (Interlocked.CompareExchange(ref _scavengeThreshold, int.MaxValue, scavengeThreshold) == scavengeThreshold) + { + try + { + // Get rid of unused handles. + Scavenge(); + } + finally + { + // And do this again when the number of handles reaches double the current after-scavenge number. + _scavengeThreshold = _stringsByHashCode.Count * 2; + } + } + } + + cacheHit = false; + return result; + } + + /// + /// Iterates over the cache and removes unused GC handles, i.e. handles that don't reference live strings. + /// This is expensive so try to call such that the cost is amortized to O(1) per GetOrCreateEntry() invocation. + /// + public void Scavenge() + { + foreach (KeyValuePair entry in _stringsByHashCode) + { + // We can safely dereference entry.Value as the caller guarantees that Scavenge runs only on one thread. + if (!entry.Value.IsUsed && _stringsByHashCode.TryRemove(entry.Key, out StringWeakHandle removedHandle)) + { + lock (removedHandle) + { + // Note that the removed handle may be different from the one we got from the enumerator so check again + // and try to put it back if it's still in use. + if (!removedHandle.IsUsed || !_stringsByHashCode.TryAdd(entry.Key, removedHandle)) + { + removedHandle.Free(); + } + } + } + } + } + + /// + /// Returns internal debug counters calculated based on the current state of the cache. + /// + public DebugInfo GetDebugInfo() + { + return GetDebugInfoImpl(); + } + } +} diff --git a/src/StringTools/WeakStringCache.Locking.cs b/src/StringTools/WeakStringCache.Locking.cs new file mode 100644 index 00000000000..47daf7ee824 --- /dev/null +++ b/src/StringTools/WeakStringCache.Locking.cs @@ -0,0 +1,125 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; + +namespace Microsoft.NET.StringTools +{ + /// + /// Implements the WeakStringCache functionality on .NET Framework 3.5 where ConcurrentDictionary is not available. + /// + internal sealed partial class WeakStringCache : IDisposable + { + private readonly Dictionary _stringsByHashCode; + + public WeakStringCache() + { + _stringsByHashCode = new Dictionary(_initialCapacity); + } + + /// + /// Main entrypoint of this cache. Tries to look up a string that matches the given internable. If it succeeds, returns + /// the string and sets cacheHit to true. If the string is not found, calls ExpensiveConvertToString on the internable, + /// adds the resulting string to the cache, and returns it, setting cacheHit to false. + /// + /// The internable describing the string we're looking for. + /// A string matching the given internable. + public string GetOrCreateEntry(ref InternableString internable, out bool cacheHit) + { + int hashCode = internable.GetHashCode(); + + StringWeakHandle handle; + string? result; + bool addingNewHandle = false; + + lock (_stringsByHashCode) + { + if (_stringsByHashCode.TryGetValue(hashCode, out handle)) + { + result = handle.GetString(ref internable); + if (result != null) + { + cacheHit = true; + return result; + } + } + else + { + handle = new StringWeakHandle(); + addingNewHandle = true; + } + + // We don't have the string in the cache - create it. + result = internable.ExpensiveConvertToString(); + + // Set the handle to reference the new string. + handle.SetString(result); + + if (addingNewHandle) + { + // Prevent the dictionary from growing forever with GC handles that don't reference live strings anymore. + if (_stringsByHashCode.Count >= _scavengeThreshold) + { + // Get rid of unused handles. + ScavengeNoLock(); + // And do this again when the number of handles reaches double the current after-scavenge number. + _scavengeThreshold = _stringsByHashCode.Count * 2; + } + } + _stringsByHashCode[hashCode] = handle; + } + + cacheHit = false; + return result; + } + + /// + /// Iterates over the cache and removes unused GC handles, i.e. handles that don't reference live strings. + /// This is expensive so try to call such that the cost is amortized to O(1) per GetOrCreateEntry() invocation. + /// Assumes the lock is taken by the caller. + /// + private void ScavengeNoLock() + { + List? keysToRemove = null; + foreach (KeyValuePair entry in _stringsByHashCode) + { + if (!entry.Value.IsUsed) + { + entry.Value.Free(); + keysToRemove ??= new List(); + keysToRemove.Add(entry.Key); + } + } + if (keysToRemove != null) + { + for (int i = 0; i < keysToRemove.Count; i++) + { + _stringsByHashCode.Remove(keysToRemove[i]); + } + } + } + + /// + /// Public version of ScavengeUnderLock() which takes the lock. + /// + public void Scavenge() + { + lock (_stringsByHashCode) + { + ScavengeNoLock(); + } + } + + /// + /// Returns internal debug counters calculated based on the current state of the cache. + /// + public DebugInfo GetDebugInfo() + { + lock (_stringsByHashCode) + { + return GetDebugInfoImpl(); + } + } + } +} diff --git a/src/StringTools/WeakStringCache.cs b/src/StringTools/WeakStringCache.cs new file mode 100644 index 00000000000..cedde724390 --- /dev/null +++ b/src/StringTools/WeakStringCache.cs @@ -0,0 +1,141 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; + +namespace Microsoft.NET.StringTools +{ + /// + /// A cache of weak GC handles pointing to strings. Weak GC handles are functionally equivalent to WeakReference's but have less overhead + /// (they're a struct as opposed to WR which is a finalizable class) at the expense of requiring manual lifetime management. As long as + /// a string has an ordinary strong GC root elsewhere in the process and another string with the same hashcode hasn't reused the entry, + /// the cache has a reference to it and can match it to an internable. When the string is collected, it is also automatically "removed" + /// from the cache by becoming unrecoverable from the GC handle. GC handles that do not reference a live string anymore are freed lazily. + /// + internal sealed partial class WeakStringCache : IDisposable + { + /// + /// Debug stats returned by GetDebugInfo(). + /// + public struct DebugInfo + { + public int LiveStringCount; + public int CollectedStringCount; + } + + /// + /// Holds a weak GC handle to a string. Shared by all strings with the same hash code and referencing the last such string we've seen. + /// + private class StringWeakHandle + { + /// + /// Weak GC handle to the last string of the given hashcode we've seen. + /// + public GCHandle WeakHandle; + + /// + /// Returns true if the string referenced by the handle is still alive. + /// + public bool IsUsed => WeakHandle.Target != null; + + /// + /// Returns the string referenced by this handle if it is equal to the given internable. + /// + /// The internable describing the string we're looking for. + /// The string matching the internable or null if the handle is referencing a collected string or the string is different. + public string? GetString(ref InternableString internable) + { + if (WeakHandle.IsAllocated && WeakHandle.Target is string str) + { + if (internable.Equals(str)) + { + return str; + } + } + return null; + } + + /// + /// Sets the handle to the given string. If the handle is still referencing another live string, that string is effectively forgotten. + /// + /// The string to set. + public void SetString(string str) + { + if (!WeakHandle.IsAllocated) + { + // The handle is not allocated - allocate it. + WeakHandle = GCHandle.Alloc(str, GCHandleType.Weak); + } + else + { + WeakHandle.Target = str; + } + } + + /// + /// Frees the GC handle. + /// + public void Free() + { + WeakHandle.Free(); + } + } + + /// + /// Initial capacity of the underlying dictionary. + /// + private const int _initialCapacity = 503; + + /// + /// The maximum size we let the collection grow before scavenging unused entries. + /// + private int _scavengeThreshold = _initialCapacity; + + /// + /// Frees all GC handles and clears the cache. + /// + private void DisposeImpl() + { + foreach (KeyValuePair entry in _stringsByHashCode) + { + entry.Value.Free(); + } + _stringsByHashCode.Clear(); + } + + public void Dispose() + { + DisposeImpl(); + GC.SuppressFinalize(this); + } + + ~WeakStringCache() + { + DisposeImpl(); + } + + /// + /// Returns internal debug counters calculated based on the current state of the cache. + /// + private DebugInfo GetDebugInfoImpl() + { + DebugInfo debugInfo = new DebugInfo(); + + foreach (KeyValuePair entry in _stringsByHashCode) + { + if (entry.Value.IsUsed) + { + debugInfo.LiveStringCount++; + } + else + { + debugInfo.CollectedStringCount++; + } + } + + return debugInfo; + } + } +} diff --git a/src/StringTools/WeakStringCacheInterner.cs b/src/StringTools/WeakStringCacheInterner.cs new file mode 100644 index 00000000000..34366af3cfc --- /dev/null +++ b/src/StringTools/WeakStringCacheInterner.cs @@ -0,0 +1,180 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; + +namespace Microsoft.NET.StringTools +{ + /// + /// Implements interning based on a WeakStringCache. + /// + internal class WeakStringCacheInterner : IDisposable + { + /// + /// Enumerates the possible interning results. + /// + private enum InternResult + { + FoundInWeakStringCache, + AddedToWeakStringCache, + } + + internal static WeakStringCacheInterner Instance = new WeakStringCacheInterner(); + + /// + /// The cache to keep strings in. + /// + private readonly WeakStringCache _weakStringCache = new WeakStringCache(); + +#region Statistics + /// + /// Number of times the regular interning path found the string in the cache. + /// + private int _regularInternHits; + + /// + /// Number of times the regular interning path added the string to the cache. + /// + private int _regularInternMisses; + + /// + /// Total number of strings eliminated by interning. + /// + private int _internEliminatedStrings; + + /// + /// Total number of chars eliminated across all strings. + /// + private int _internEliminatedChars; + + /// + /// Maps strings that went though the interning path to the number of times they have been + /// seen. The higher the number the better the payoff of interning. Null if statistics + /// gathering has not been enabled. + /// + private Dictionary? _internCallCountsByString; + +#endregion + + /// + /// Try to intern the string. + /// The return value indicates the how the string was interned. + /// + private InternResult Intern(ref InternableString candidate, out string interned) + { + interned = _weakStringCache.GetOrCreateEntry(ref candidate, out bool cacheHit); + return cacheHit ? InternResult.FoundInWeakStringCache : InternResult.AddedToWeakStringCache; + } + + /// + /// WeakIntern the given InternableString. + /// + public string InternableToString(ref InternableString candidate) + { + if (candidate.Length == 0) + { + return string.Empty; + } + + InternResult resultForStatistics = Intern(ref candidate, out string internedString); +#if DEBUG + string expectedString = candidate.ExpensiveConvertToString(); + if (!String.Equals(internedString, expectedString)) + { + throw new InvalidOperationException(String.Format("Interned string {0} should have been {1}", internedString, expectedString)); + } +#endif + + if (_internCallCountsByString != null) + { + lock (_internCallCountsByString) + { + switch (resultForStatistics) + { + case InternResult.FoundInWeakStringCache: + _regularInternHits++; + break; + case InternResult.AddedToWeakStringCache: + _regularInternMisses++; + break; + } + + _internCallCountsByString.TryGetValue(internedString, out int priorCount); + _internCallCountsByString[internedString] = priorCount + 1; + + if (!candidate.ReferenceEquals(internedString)) + { + // Reference changed so 'candidate' is now released and should save memory. + _internEliminatedStrings++; + _internEliminatedChars += candidate.Length; + } + } + } + + return internedString; + } + + /// + /// + /// + public void EnableStatistics() + { + _internCallCountsByString = new Dictionary(); + } + + /// + /// Returns a string with human-readable statistics. + /// + public string FormatStatistics() + { + StringBuilder result = new StringBuilder(1024); + + string title = "Opportunistic Intern"; + + if (_internCallCountsByString != null) + { + result.AppendLine(string.Format("\n{0}{1}{0}", new string('=', 41 - (title.Length / 2)), title)); + result.AppendLine(string.Format("||{0,50}|{1,20:N0}|{2,8}|", "WeakStringCache Hits", _regularInternHits, "hits")); + result.AppendLine(string.Format("||{0,50}|{1,20:N0}|{2,8}|", "WeakStringCache Misses", _regularInternMisses, "misses")); + result.AppendLine(string.Format("||{0,50}|{1,20:N0}|{2,8}|", "Eliminated Strings*", _internEliminatedStrings, "strings")); + result.AppendLine(string.Format("||{0,50}|{1,20:N0}|{2,8}|", "Eliminated Chars", _internEliminatedChars, "chars")); + result.AppendLine(string.Format("||{0,50}|{1,20:N0}|{2,8}|", "Estimated Eliminated Bytes", _internEliminatedChars * 2, "bytes")); + result.AppendLine("Elimination assumes that strings provided were unique objects."); + result.AppendLine("|---------------------------------------------------------------------------------|"); + + IEnumerable topInternedStrings = + _internCallCountsByString + .OrderByDescending(kv => kv.Value * kv.Key.Length) + .Where(kv => kv.Value > 1) + .Take(15) + .Select(kv => string.Format(CultureInfo.InvariantCulture, "({1} instances x each {2} chars)\n{0}", kv.Key, kv.Value, kv.Key.Length)); + + result.AppendLine(string.Format("##########Top Top Interned Strings: \n{0} ", string.Join("\n==============\n", topInternedStrings.ToArray()))); + result.AppendLine(); + + WeakStringCache.DebugInfo debugInfo = _weakStringCache.GetDebugInfo(); + result.AppendLine("WeakStringCache statistics:"); + result.AppendLine(string.Format("String count live/collected/total = {0}/{1}/{2}", debugInfo.LiveStringCount, debugInfo.CollectedStringCount, debugInfo.LiveStringCount + debugInfo.CollectedStringCount)); + } + else + { + result.Append(title); + result.AppendLine(" - EnableStatisticsGathering() has not been called"); + } + + return result.ToString(); + } + + /// + /// Releases all strings from the underlying intern table. + /// + public void Dispose() + { + _weakStringCache.Dispose(); + } + } +} From c319a63df71a815f9862cdaf2237d65525aea429 Mon Sep 17 00:00:00 2001 From: Ladi Prosek Date: Tue, 5 Jan 2021 11:14:09 +0100 Subject: [PATCH 02/14] Add the StringTools.UnitTests project --- src/Directory.Build.targets | 2 +- .../InterningTestData.cs | 68 ++++++ .../SpanBasedStringBuilder_Tests.cs | 154 ++++++++++++++ .../StringTools.UnitTests.csproj | 30 +++ .../StringTools.UnitTests.net35.csproj | 40 ++++ .../StringTools_Tests.cs | 61 ++++++ .../WeakStringCache_Tests.cs | 195 ++++++++++++++++++ 7 files changed, 549 insertions(+), 1 deletion(-) create mode 100644 src/StringTools.UnitTests/InterningTestData.cs create mode 100644 src/StringTools.UnitTests/SpanBasedStringBuilder_Tests.cs create mode 100644 src/StringTools.UnitTests/StringTools.UnitTests.csproj create mode 100644 src/StringTools.UnitTests/StringTools.UnitTests.net35.csproj create mode 100644 src/StringTools.UnitTests/StringTools_Tests.cs create mode 100644 src/StringTools.UnitTests/WeakStringCache_Tests.cs diff --git a/src/Directory.Build.targets b/src/Directory.Build.targets index e90e7f4fbf3..5b62b7ef4e3 100644 --- a/src/Directory.Build.targets +++ b/src/Directory.Build.targets @@ -58,7 +58,7 @@ - + diff --git a/src/StringTools.UnitTests/InterningTestData.cs b/src/StringTools.UnitTests/InterningTestData.cs new file mode 100644 index 00000000000..857e9032850 --- /dev/null +++ b/src/StringTools.UnitTests/InterningTestData.cs @@ -0,0 +1,68 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Generic; + +namespace Microsoft.NET.StringTools.Tests +{ + public static class InterningTestData + { + /// + /// Represents an array of string fragments to initialize an InternableString with. + /// + public class TestDatum + { + private string _string; + public string[] Fragments { get; } + + public int Length => _string.Length; + + public TestDatum(params string[] fragments) + { + Fragments = fragments; + _string = string.Join(string.Empty, Fragments); + } + + public char this[int index] => _string[index]; + + public override string ToString() + { + return _string; + } + } + + public static IEnumerable TestData + { + get + { + yield return new object[] { new TestDatum((string)null) }; + yield return new object[] { new TestDatum("") }; + yield return new object[] { new TestDatum("Test") }; + yield return new object[] { new TestDatum(null, "All") }; + yield return new object[] { new TestDatum("", "All") }; + yield return new object[] { new TestDatum("", "All", "") }; + yield return new object[] { new TestDatum("Test", "All", "The", "Things") }; + } + } + + public static IEnumerable TestDataForTrim + { + get + { + yield return new object[] { new TestDatum((string)null) }; + yield return new object[] { new TestDatum("") }; + yield return new object[] { new TestDatum(" ") }; + yield return new object[] { new TestDatum(" ") }; + yield return new object[] { new TestDatum(null, "") }; + yield return new object[] { new TestDatum(null, " ") }; + yield return new object[] { new TestDatum(" T ") }; + yield return new object[] { new TestDatum(" Test ") }; + yield return new object[] { new TestDatum(null, " Test ") }; + yield return new object[] { new TestDatum(null, " Test All ") }; + yield return new object[] { new TestDatum(" ", " Test", "", "All ", " ") }; + yield return new object[] { new TestDatum("Test", " ", "", " ", " ") }; + yield return new object[] { new TestDatum("Test", " All ", " The ", "Things") }; + } + } + } +} diff --git a/src/StringTools.UnitTests/SpanBasedStringBuilder_Tests.cs b/src/StringTools.UnitTests/SpanBasedStringBuilder_Tests.cs new file mode 100644 index 00000000000..9be63b4b714 --- /dev/null +++ b/src/StringTools.UnitTests/SpanBasedStringBuilder_Tests.cs @@ -0,0 +1,154 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#if NET35_UNITTEST +extern alias StringToolsNet35; +#endif + +using System.Collections.Generic; + +using Shouldly; +using Xunit; + +#if NET35_UNITTEST +using StringToolsNet35::Microsoft.NET.StringTools; +#endif + +namespace Microsoft.NET.StringTools.Tests +{ + public class SpanBasedStringBuilder_Tests + { + private SpanBasedStringBuilder MakeSpanBasedStringBuilder(InterningTestData.TestDatum datum, bool appendSubStrings = false) + { + bool wrapFirstFragment = datum.Fragments.Length > 0 && datum.Fragments[0] != null; + + SpanBasedStringBuilder stringBuilder = wrapFirstFragment + ? new SpanBasedStringBuilder(datum.Fragments[0]) + : new SpanBasedStringBuilder(); + + for (int i = 1; i < datum.Fragments.Length; i++) + { + if (appendSubStrings) + { + int index = datum.Fragments[i].Length / 2; + stringBuilder.Append(datum.Fragments[i], 0, index); + stringBuilder.Append(datum.Fragments[i], index, datum.Fragments[i].Length - index); + } + else + { + stringBuilder.Append(datum.Fragments[i]); + } + } + return stringBuilder; + } + + public static IEnumerable TestData => InterningTestData.TestData; + public static IEnumerable TestDataForTrim => InterningTestData.TestDataForTrim; + + [Theory] + [MemberData(nameof(TestData))] + public void LengthReturnsLength(InterningTestData.TestDatum datum) + { + MakeSpanBasedStringBuilder(datum).Length.ShouldBe(datum.Length); + } + + [Theory] + [MemberData(nameof(TestData))] + public void EnumeratorEnumeratesCharacters(InterningTestData.TestDatum datum) + { + SpanBasedStringBuilder stringBuilder = MakeSpanBasedStringBuilder(datum); + int index = 0; + foreach (char ch in stringBuilder) + { + ch.ShouldBe(datum[index]); + index++; + } + } + + [Theory] + [MemberData(nameof(TestData))] + public void EqualsReturnsExpectedValue(InterningTestData.TestDatum datum) + { + InternableString internableString = new InternableString(MakeSpanBasedStringBuilder(datum)); + internableString.Equals(string.Empty).ShouldBe(internableString.Length == 0); + + string substr = datum.Fragments[0] ?? string.Empty; + internableString.Equals(substr).ShouldBe(substr.Length == internableString.Length); + + if (datum.Fragments.Length > 1) + { + substr += datum.Fragments[1]; + internableString.Equals(substr).ShouldBe(substr.Length == internableString.Length); + + internableString.Equals(datum.ToString()).ShouldBeTrue(); + } + + internableString.Equals("Things").ShouldBeFalse(); + } + + [Fact] + public void ReferenceEqualsReturnsExpectedValue() + { + string str = "Test"; + InternableString internableString = new InternableString(str); + internableString.ReferenceEquals(str).ShouldBeTrue(); + internableString = new InternableString(new string(str.ToCharArray())); + internableString.ReferenceEquals(str).ShouldBeFalse(); + } + + [Theory] + [MemberData(nameof(TestData))] + public void AppendAppendsString(InterningTestData.TestDatum datum) + { + SpanBasedStringBuilder stringBuilder = MakeSpanBasedStringBuilder(datum, false); + new InternableString(stringBuilder).ExpensiveConvertToString().ShouldBe(datum.ToString()); + } + + [Theory] + [MemberData(nameof(TestData))] + public void AppendAppendsSubstring(InterningTestData.TestDatum datum) + { + SpanBasedStringBuilder stringBuilder = MakeSpanBasedStringBuilder(datum, true); + new InternableString(stringBuilder).ExpensiveConvertToString().ShouldBe(datum.ToString()); + } + +#if !NET35_UNITTEST + [Theory] + [MemberData(nameof(TestDataForTrim))] + public void TrimStartRemovesLeadingWhiteSpace(InterningTestData.TestDatum datum) + { + SpanBasedStringBuilder stringBuilder = MakeSpanBasedStringBuilder(datum); + stringBuilder.TrimStart(); + new InternableString(stringBuilder).ExpensiveConvertToString().ShouldBe(datum.ToString().TrimStart()); + } + + [Theory] + [MemberData(nameof(TestDataForTrim))] + public void TrimEndRemovesTrailingWhiteSpace(InterningTestData.TestDatum datum) + { + SpanBasedStringBuilder stringBuilder = MakeSpanBasedStringBuilder(datum); + stringBuilder.TrimEnd(); + new InternableString(stringBuilder).ExpensiveConvertToString().ShouldBe(datum.ToString().TrimEnd()); + } + + [Theory] + [MemberData(nameof(TestDataForTrim))] + public void TrimRemovesLeadingAndTrailingWhiteSpace(InterningTestData.TestDatum datum) + { + SpanBasedStringBuilder stringBuilder = MakeSpanBasedStringBuilder(datum); + stringBuilder.Trim(); + new InternableString(stringBuilder).ExpensiveConvertToString().ShouldBe(datum.ToString().Trim()); + } +#endif + + [Theory] + [MemberData(nameof(TestData))] + public void ClearRemovesAllCharacters(InterningTestData.TestDatum datum) + { + SpanBasedStringBuilder stringBuilder = MakeSpanBasedStringBuilder(datum); + stringBuilder.Clear(); + stringBuilder.Length.ShouldBe(0); + stringBuilder.GetEnumerator().MoveNext().ShouldBeFalse(); + } + } +} diff --git a/src/StringTools.UnitTests/StringTools.UnitTests.csproj b/src/StringTools.UnitTests/StringTools.UnitTests.csproj new file mode 100644 index 00000000000..e11fc1d60ce --- /dev/null +++ b/src/StringTools.UnitTests/StringTools.UnitTests.csproj @@ -0,0 +1,30 @@ + + + $(RuntimeOutputTargetFrameworks) + $(RuntimeOutputPlatformTarget) + + false + + Microsoft.NET.StringTools.UnitTests + true + true + + + + + + + + + + + + + App.config + Designer + + + PreserveNewest + + + diff --git a/src/StringTools.UnitTests/StringTools.UnitTests.net35.csproj b/src/StringTools.UnitTests/StringTools.UnitTests.net35.csproj new file mode 100644 index 00000000000..0c10b4d1f04 --- /dev/null +++ b/src/StringTools.UnitTests/StringTools.UnitTests.net35.csproj @@ -0,0 +1,40 @@ + + + + + + + $(FullFrameworkTFM) + $(RuntimeOutputPlatformTarget) + + false + + Microsoft.NET.StringTools.net35.UnitTests + true + true + $(DefineConstants);NET35_UNITTEST + + + + + + + + + + + TargetFramework=net35 + + + + + + App.config + Designer + + + PreserveNewest + + + diff --git a/src/StringTools.UnitTests/StringTools_Tests.cs b/src/StringTools.UnitTests/StringTools_Tests.cs new file mode 100644 index 00000000000..7f396ab32b6 --- /dev/null +++ b/src/StringTools.UnitTests/StringTools_Tests.cs @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#if NET35_UNITTEST +extern alias StringToolsNet35; +#endif + +using System; + +using Shouldly; +using Xunit; + +#if NET35_UNITTEST +using StringToolsNet35::Microsoft.NET.StringTools; +using Shouldly.Configuration; +#else +using Microsoft.NET.StringTools; +#endif + +namespace Microsoft.NET.StringTools.Tests +{ + public class StringTools_Tests + { + [Theory] + [InlineData("")] + [InlineData("A")] + [InlineData("Hello")] + [InlineData("HelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHello")] + public void InternsStrings(string str) + { + string internedString1 = Strings.WeakIntern(str); + internedString1.Equals(str).ShouldBeTrue(); + string internedString2 = Strings.WeakIntern(str); + internedString1.Equals(str).ShouldBeTrue(); + Object.ReferenceEquals(internedString1, internedString2).ShouldBeTrue(); + +#if !NET35_UNITTEST + ReadOnlySpan span = str.AsSpan(); + internedString1 = Strings.WeakIntern(span); + internedString1.Equals(str).ShouldBeTrue(); + internedString2 = Strings.WeakIntern(span); + internedString1.Equals(str).ShouldBeTrue(); + Object.ReferenceEquals(internedString1, internedString2).ShouldBeTrue(); +#endif + } + + [Fact] + public void CreatesDiagnosticReport() + { + string statisticsNotEnabledString = "EnableStatisticsGathering() has not been called"; + + Strings.CreateDiagnosticReport().ShouldContain(statisticsNotEnabledString); + + Strings.EnableDiagnostics(); + string report = Strings.CreateDiagnosticReport(); + + report.ShouldNotContain(statisticsNotEnabledString); + report.ShouldContain("Eliminated Strings"); + } + } +} diff --git a/src/StringTools.UnitTests/WeakStringCache_Tests.cs b/src/StringTools.UnitTests/WeakStringCache_Tests.cs new file mode 100644 index 00000000000..bddfc60917b --- /dev/null +++ b/src/StringTools.UnitTests/WeakStringCache_Tests.cs @@ -0,0 +1,195 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#if NET35_UNITTEST +extern alias StringToolsNet35; +#endif + +using System; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; + +using Shouldly; +using Xunit; + +#if NET35_UNITTEST +using StringToolsNet35::Microsoft.NET.StringTools; +#endif + +namespace Microsoft.NET.StringTools.Tests +{ + public class WeakStringCache_Tests : IDisposable + { + /// + /// The weak string cache under test. + /// + private WeakStringCache _cache = new WeakStringCache(); + + public void Dispose() + { + _cache.Dispose(); + } + + /// + /// Adds a string to the cache under test. + /// + /// Part one of the string (split to prevent runtime interning and unintended GC roots). + /// Part two of the string (split to prevent runtime interning and unintended GC roots). + /// Callback to be invoked after the string has been added but before the strong GC ref is released. + /// The hash code of the string as calculated by WeakStringCache. + [MethodImpl(MethodImplOptions.NoInlining)] + private int AddString(string strPart1, string strPart2, Action callbackToRunWithTheStringAlive) + { + // Compose the string with SB so it doesn't get interned by the runtime. + string testString = new StringBuilder(strPart1).Append(strPart2).ToString(); + InternableString testStringTarget = new InternableString(testString); + + int hashCode = testStringTarget.GetHashCode(); + + string cachedString = _cache.GetOrCreateEntry(ref testStringTarget, out bool cacheHit); + cacheHit.ShouldBeFalse(); + cachedString.ShouldBeSameAs(testString); + + callbackToRunWithTheStringAlive(cachedString); + + // Verify that the string is really in the cache and the cache returns the interned instance. + string testStringCopy = new StringBuilder(strPart1).Append(strPart2).ToString(); + InternableString testStringCopyTarget = new InternableString(testStringCopy); + cachedString = _cache.GetOrCreateEntry(ref testStringCopyTarget, out cacheHit); + cacheHit.ShouldBeTrue(); + cachedString.ShouldBeSameAs(testString); + + // Trigger full GC and verify that nothing has changed since we're still keeping testString alive. + GC.Collect(); + + callbackToRunWithTheStringAlive(cachedString); + + testStringCopyTarget = new InternableString(testStringCopy); + cachedString = _cache.GetOrCreateEntry(ref testStringCopyTarget, out cacheHit); + cacheHit.ShouldBeTrue(); + cachedString.ShouldBeSameAs(testString); + + return hashCode; + } + + /// + /// Adds strings that are known to have a hash code collision to the cache under test. + /// + [MethodImpl(MethodImplOptions.NoInlining)] + private void AddStringsWithSameHashCode(int numberOfStrings) + { + string[] cachedStrings = new string[numberOfStrings]; + int[] hashCodes = new int[numberOfStrings]; + + for (int i = 0; i < numberOfStrings; i++) + { + string strPart2 = "1" + String.Concat(Enumerable.Repeat("4428939786", i)); + hashCodes[i] = AddString("Random string ", strPart2, (string cachedString) => + { + _cache.GetDebugInfo().ShouldBe(new WeakStringCache.DebugInfo() + { + LiveStringCount = 1, + CollectedStringCount = 0, + }); + cachedStrings[i] = cachedString; + }); + + if (i > 0) + { + // The strings have been carefully constructed to have the same hash code. + hashCodes[i].ShouldBe(hashCodes[i - 1]); + } + } + + // There are no cache hits when iterating over our strings again because the last one always wins and steals the slot. + for (int i = 0; i < numberOfStrings; i++) + { + InternableString stringCopy = new InternableString(new string(cachedStrings[i].ToCharArray())); + string cachedStringFromCache =_cache.GetOrCreateEntry(ref stringCopy, out bool cacheHit); + cacheHit.ShouldBeFalse(); + cachedStringFromCache.ShouldNotBeSameAs(cachedStrings[i]); + } + } + + /// + /// Simple test case to verify that: + /// 1. A string added to the cache stays in the cache as long as it's alive. + /// 2. The string is no longer retrievable after all strong GC refs are gone. + /// 3. The cache completely removes the handle after calling Scavenge on it. + /// + /// + /// Disabled on MacOS Mono because it doesn't play well with conservative GC scanning. + /// https://www.mono-project.com/docs/advanced/garbage-collector/sgen/#precise-stack-marking + /// + [Fact] + [Trait("Category", "mono-osx-failing")] + public void RetainsStringUntilCollected() + { + // Add a string to the cache using a non-inlinable method to make sure it's not reachable from a GC root. + AddString("Random string ", "test", (string cachedString) => + { + _cache.GetDebugInfo().ShouldBe(new WeakStringCache.DebugInfo() + { + LiveStringCount = 1, + CollectedStringCount = 0, + }); + }); + + // Trigger full GC. + GC.Collect(); + + // The handle is still in the cache but it's unused now as the string has been collected. + _cache.GetDebugInfo().ShouldBe(new WeakStringCache.DebugInfo() + { + LiveStringCount = 0, + CollectedStringCount = 1, + }); + + // Ask the cache to get rid of unused handles. + _cache.Scavenge(); + + // The cache should be empty now. + _cache.GetDebugInfo().ShouldBe(new WeakStringCache.DebugInfo() + { + LiveStringCount = 0, + CollectedStringCount = 0, + }); + } + + /// + /// Same as RetainsStringUntilCollected but with multiple strings sharing the same hash code. + /// + /// + /// Disabled on MacOS Mono because it doesn't play well with conservative GC scanning. + /// https://www.mono-project.com/docs/advanced/garbage-collector/sgen/#precise-stack-marking + /// + [Fact] + [Trait("Category", "mono-osx-failing")] + public void RetainsLastStringWithGivenHashCode() + { + // Add 3 strings with the same hash code. + AddStringsWithSameHashCode(3); + + // Trigger full GC. + GC.Collect(); + + // The handle is still in the cache but it's unused now as the strings have been collected. + _cache.GetDebugInfo().ShouldBe(new WeakStringCache.DebugInfo() + { + LiveStringCount = 0, + CollectedStringCount = 1, + }); + + // Ask the cache to get rid of unused handles. + _cache.Scavenge(); + + // The cache should be empty now. + _cache.GetDebugInfo().ShouldBe(new WeakStringCache.DebugInfo() + { + LiveStringCount = 0, + CollectedStringCount = 0, + }); + } + } +} From 1c879601c5fc151f1bbb01b33a162a823588b5e2 Mon Sep 17 00:00:00 2001 From: Ladi Prosek Date: Tue, 5 Jan 2021 11:15:38 +0100 Subject: [PATCH 03/14] Add the StringTools.Benchmark project --- eng/Packages.props | 1 + src/StringTools.Benchmark/Program.cs | 15 ++++ .../SpanBasedStringBuilder_Benchmark.cs | 85 +++++++++++++++++++ .../StringTools.Benchmark.csproj | 22 +++++ 4 files changed, 123 insertions(+) create mode 100644 src/StringTools.Benchmark/Program.cs create mode 100644 src/StringTools.Benchmark/SpanBasedStringBuilder_Benchmark.cs create mode 100644 src/StringTools.Benchmark/StringTools.Benchmark.csproj diff --git a/eng/Packages.props b/eng/Packages.props index 07d71a3583c..30ae007906c 100644 --- a/eng/Packages.props +++ b/eng/Packages.props @@ -1,5 +1,6 @@ + diff --git a/src/StringTools.Benchmark/Program.cs b/src/StringTools.Benchmark/Program.cs new file mode 100644 index 00000000000..7bdd21ed36a --- /dev/null +++ b/src/StringTools.Benchmark/Program.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using BenchmarkDotNet.Running; + +namespace Microsoft.NET.StringTools.Benchmark +{ + public class Program + { + public static void Main(string[] args) + { + BenchmarkRunner.Run(); + } + } +} diff --git a/src/StringTools.Benchmark/SpanBasedStringBuilder_Benchmark.cs b/src/StringTools.Benchmark/SpanBasedStringBuilder_Benchmark.cs new file mode 100644 index 00000000000..03fa15ccfc5 --- /dev/null +++ b/src/StringTools.Benchmark/SpanBasedStringBuilder_Benchmark.cs @@ -0,0 +1,85 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using BenchmarkDotNet.Attributes; +using System.Text; + +namespace Microsoft.NET.StringTools.Benchmark +{ + [MemoryDiagnoser] + public class SpanBasedStringBuilder_Benchmark + { + [Params(1, 2, 4, 8, 16, 256)] + public int NumSubstrings { get; set; } + + [Params(1, 8, 32, 128, 512)] + public int SubstringLengths { get; set; } + + private string[] _subStrings; + + private static SpanBasedStringBuilder _pooledSpanBasedStringBuilder = new SpanBasedStringBuilder(); + private static StringBuilder _pooledStringBuilder = new StringBuilder(); + + private static int _uniqueStringCounter = 0; + + [GlobalSetup] + public void GlobalSetup() + { + _subStrings = new string[NumSubstrings]; + for (int i = 0; i < _subStrings.Length; i++) + { + _subStrings[i] = new string('a', SubstringLengths); + } + } + + [Benchmark] + public void SpanBasedOperations_CacheHit() + { + SpanBasedStringBuilder sbsb = _pooledSpanBasedStringBuilder; + sbsb.Clear(); + foreach (string subString in _subStrings) + { + sbsb.Append(subString); + } + sbsb.ToString(); + } + + [Benchmark] + public void RegularOperations_CacheHit() + { + StringBuilder sb = _pooledStringBuilder; + sb.Clear(); + foreach (string subString in _subStrings) + { + sb.Append(subString); + } + Strings.WeakIntern(sb.ToString()); + } + + [Benchmark] + public void SpanBasedOperations_CacheMiss() + { + SpanBasedStringBuilder sbsb = _pooledSpanBasedStringBuilder; + sbsb.Clear(); + foreach (string subString in _subStrings) + { + sbsb.Append(subString); + } + sbsb.Append(_uniqueStringCounter++.ToString("X8")); + sbsb.ToString(); + } + + [Benchmark] + public void RegularOperations_CacheMiss() + { + StringBuilder sb = _pooledStringBuilder; + sb.Clear(); + foreach (string subString in _subStrings) + { + sb.Append(subString); + } + sb.Append(_uniqueStringCounter++.ToString("X8")); + Strings.WeakIntern(sb.ToString()); + } + } +} diff --git a/src/StringTools.Benchmark/StringTools.Benchmark.csproj b/src/StringTools.Benchmark/StringTools.Benchmark.csproj new file mode 100644 index 00000000000..eb1bf1347f3 --- /dev/null +++ b/src/StringTools.Benchmark/StringTools.Benchmark.csproj @@ -0,0 +1,22 @@ + + + Exe + false + $(RuntimeOutputTargetFrameworks) + $(RuntimeOutputPlatformTarget) + + false + true + + StringTools.Benchmark + Microsoft.NET.StringTools.Benchmark.Program + + + + + + + + + + From 16f2a0f0609f3532291f7df651bd2c2d9c44c8b0 Mon Sep 17 00:00:00 2001 From: Ladi Prosek Date: Tue, 5 Jan 2021 11:22:15 +0100 Subject: [PATCH 04/14] Add StringTools* projects to solution files --- MSBuild.SourceBuild.slnf | 3 +- MSBuild.sln | 130 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 131 insertions(+), 2 deletions(-) diff --git a/MSBuild.SourceBuild.slnf b/MSBuild.SourceBuild.slnf index 48bd4ec0387..d69d4de7ab3 100644 --- a/MSBuild.SourceBuild.slnf +++ b/MSBuild.SourceBuild.slnf @@ -7,7 +7,8 @@ "src\\MSBuild\\MSBuild.csproj", "src\\Package\\Localization\\Localization.csproj", "src\\Tasks\\Microsoft.Build.Tasks.csproj", - "src\\Utilities\\Microsoft.Build.Utilities.csproj" + "src\\Utilities\\Microsoft.Build.Utilities.csproj", + "src\\StringTools\\StringTools.csproj" ] } } \ No newline at end of file diff --git a/MSBuild.sln b/MSBuild.sln index f58cad8b0d6..74de884bad7 100644 --- a/MSBuild.sln +++ b/MSBuild.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 16 -VisualStudioVersion = 16.0.30320.27 +VisualStudioVersion = 16.0.30413.136 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{4900B3B8-4310-4D5B-B1F7-2FDF9199765F}" ProjectSection(SolutionItems) = preProject @@ -65,10 +65,18 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MSBuild.Engine.Corext", "sr EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MSBuild.Bootstrap", "src\MSBuild.Bootstrap\MSBuild.Bootstrap.csproj", "{CEAEE4FE-9298-443B-AFC5-0F72472484B6}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StringTools", "src\StringTools\StringTools.csproj", "{639C178E-368F-4384-869E-7C6D18B4CC1F}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StringTools.UnitTests", "src\StringTools.UnitTests\StringTools.UnitTests.csproj", "{A1FF7E97-F98F-4C5C-AD09-0E1CF4A7A4DB}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StringTools.UnitTests.net35", "src\StringTools.UnitTests\StringTools.UnitTests.net35.csproj", "{D05E5FAF-3E05-48D2-8DEF-FD1A18EB1349}" +EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Build.UnGAC", "src\Package\Microsoft.Build.UnGAC\Microsoft.Build.UnGAC.csproj", "{B60173F0-F9F0-4688-9DF8-9ADDD57BD45F}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ProjectCachePlugin", "src\Samples\ProjectCachePlugin\ProjectCachePlugin.csproj", "{F47E1A0A-7D81-40CF-B8B3-A0F4B5ADE943}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StringTools.Benchmark", "src\StringTools.Benchmark\StringTools.Benchmark.csproj", "{65749C80-47E7-42FE-B441-7A86289D46AA}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -838,6 +846,96 @@ Global {CEAEE4FE-9298-443B-AFC5-0F72472484B6}.Release-MONO|x64.Build.0 = Release-MONO|x64 {CEAEE4FE-9298-443B-AFC5-0F72472484B6}.Release-MONO|x86.ActiveCfg = Release-MONO|Any CPU {CEAEE4FE-9298-443B-AFC5-0F72472484B6}.Release-MONO|x86.Build.0 = Release-MONO|Any CPU + {639C178E-368F-4384-869E-7C6D18B4CC1F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {639C178E-368F-4384-869E-7C6D18B4CC1F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {639C178E-368F-4384-869E-7C6D18B4CC1F}.Debug|x64.ActiveCfg = Debug|x64 + {639C178E-368F-4384-869E-7C6D18B4CC1F}.Debug|x64.Build.0 = Debug|x64 + {639C178E-368F-4384-869E-7C6D18B4CC1F}.Debug|x86.ActiveCfg = Debug|Any CPU + {639C178E-368F-4384-869E-7C6D18B4CC1F}.Debug|x86.Build.0 = Debug|Any CPU + {639C178E-368F-4384-869E-7C6D18B4CC1F}.Debug-MONO|Any CPU.ActiveCfg = Debug-MONO|Any CPU + {639C178E-368F-4384-869E-7C6D18B4CC1F}.Debug-MONO|Any CPU.Build.0 = Debug-MONO|Any CPU + {639C178E-368F-4384-869E-7C6D18B4CC1F}.Debug-MONO|x64.ActiveCfg = Debug-MONO|x64 + {639C178E-368F-4384-869E-7C6D18B4CC1F}.Debug-MONO|x64.Build.0 = Debug-MONO|x64 + {639C178E-368F-4384-869E-7C6D18B4CC1F}.Debug-MONO|x86.ActiveCfg = Debug-MONO|Any CPU + {639C178E-368F-4384-869E-7C6D18B4CC1F}.Debug-MONO|x86.Build.0 = Debug-MONO|Any CPU + {639C178E-368F-4384-869E-7C6D18B4CC1F}.MachineIndependent|Any CPU.ActiveCfg = MachineIndependent|Any CPU + {639C178E-368F-4384-869E-7C6D18B4CC1F}.MachineIndependent|Any CPU.Build.0 = MachineIndependent|Any CPU + {639C178E-368F-4384-869E-7C6D18B4CC1F}.MachineIndependent|x64.ActiveCfg = MachineIndependent|x64 + {639C178E-368F-4384-869E-7C6D18B4CC1F}.MachineIndependent|x64.Build.0 = MachineIndependent|x64 + {639C178E-368F-4384-869E-7C6D18B4CC1F}.MachineIndependent|x86.ActiveCfg = MachineIndependent|Any CPU + {639C178E-368F-4384-869E-7C6D18B4CC1F}.MachineIndependent|x86.Build.0 = MachineIndependent|Any CPU + {639C178E-368F-4384-869E-7C6D18B4CC1F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {639C178E-368F-4384-869E-7C6D18B4CC1F}.Release|Any CPU.Build.0 = Release|Any CPU + {639C178E-368F-4384-869E-7C6D18B4CC1F}.Release|x64.ActiveCfg = Release|x64 + {639C178E-368F-4384-869E-7C6D18B4CC1F}.Release|x64.Build.0 = Release|x64 + {639C178E-368F-4384-869E-7C6D18B4CC1F}.Release|x86.ActiveCfg = Release|Any CPU + {639C178E-368F-4384-869E-7C6D18B4CC1F}.Release|x86.Build.0 = Release|Any CPU + {639C178E-368F-4384-869E-7C6D18B4CC1F}.Release-MONO|Any CPU.ActiveCfg = Release-MONO|Any CPU + {639C178E-368F-4384-869E-7C6D18B4CC1F}.Release-MONO|Any CPU.Build.0 = Release-MONO|Any CPU + {639C178E-368F-4384-869E-7C6D18B4CC1F}.Release-MONO|x64.ActiveCfg = Release-MONO|x64 + {639C178E-368F-4384-869E-7C6D18B4CC1F}.Release-MONO|x64.Build.0 = Release-MONO|x64 + {639C178E-368F-4384-869E-7C6D18B4CC1F}.Release-MONO|x86.ActiveCfg = Release-MONO|Any CPU + {639C178E-368F-4384-869E-7C6D18B4CC1F}.Release-MONO|x86.Build.0 = Release-MONO|Any CPU + {A1FF7E97-F98F-4C5C-AD09-0E1CF4A7A4DB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A1FF7E97-F98F-4C5C-AD09-0E1CF4A7A4DB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A1FF7E97-F98F-4C5C-AD09-0E1CF4A7A4DB}.Debug|x64.ActiveCfg = Debug|x64 + {A1FF7E97-F98F-4C5C-AD09-0E1CF4A7A4DB}.Debug|x64.Build.0 = Debug|x64 + {A1FF7E97-F98F-4C5C-AD09-0E1CF4A7A4DB}.Debug|x86.ActiveCfg = Debug|Any CPU + {A1FF7E97-F98F-4C5C-AD09-0E1CF4A7A4DB}.Debug|x86.Build.0 = Debug|Any CPU + {A1FF7E97-F98F-4C5C-AD09-0E1CF4A7A4DB}.Debug-MONO|Any CPU.ActiveCfg = Debug-MONO|Any CPU + {A1FF7E97-F98F-4C5C-AD09-0E1CF4A7A4DB}.Debug-MONO|Any CPU.Build.0 = Debug-MONO|Any CPU + {A1FF7E97-F98F-4C5C-AD09-0E1CF4A7A4DB}.Debug-MONO|x64.ActiveCfg = Debug-MONO|x64 + {A1FF7E97-F98F-4C5C-AD09-0E1CF4A7A4DB}.Debug-MONO|x64.Build.0 = Debug-MONO|x64 + {A1FF7E97-F98F-4C5C-AD09-0E1CF4A7A4DB}.Debug-MONO|x86.ActiveCfg = Debug-MONO|Any CPU + {A1FF7E97-F98F-4C5C-AD09-0E1CF4A7A4DB}.Debug-MONO|x86.Build.0 = Debug-MONO|Any CPU + {A1FF7E97-F98F-4C5C-AD09-0E1CF4A7A4DB}.MachineIndependent|Any CPU.ActiveCfg = MachineIndependent|Any CPU + {A1FF7E97-F98F-4C5C-AD09-0E1CF4A7A4DB}.MachineIndependent|Any CPU.Build.0 = MachineIndependent|Any CPU + {A1FF7E97-F98F-4C5C-AD09-0E1CF4A7A4DB}.MachineIndependent|x64.ActiveCfg = MachineIndependent|x64 + {A1FF7E97-F98F-4C5C-AD09-0E1CF4A7A4DB}.MachineIndependent|x64.Build.0 = MachineIndependent|x64 + {A1FF7E97-F98F-4C5C-AD09-0E1CF4A7A4DB}.MachineIndependent|x86.ActiveCfg = MachineIndependent|Any CPU + {A1FF7E97-F98F-4C5C-AD09-0E1CF4A7A4DB}.MachineIndependent|x86.Build.0 = MachineIndependent|Any CPU + {A1FF7E97-F98F-4C5C-AD09-0E1CF4A7A4DB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A1FF7E97-F98F-4C5C-AD09-0E1CF4A7A4DB}.Release|Any CPU.Build.0 = Release|Any CPU + {A1FF7E97-F98F-4C5C-AD09-0E1CF4A7A4DB}.Release|x64.ActiveCfg = Release|x64 + {A1FF7E97-F98F-4C5C-AD09-0E1CF4A7A4DB}.Release|x64.Build.0 = Release|x64 + {A1FF7E97-F98F-4C5C-AD09-0E1CF4A7A4DB}.Release|x86.ActiveCfg = Release|Any CPU + {A1FF7E97-F98F-4C5C-AD09-0E1CF4A7A4DB}.Release|x86.Build.0 = Release|Any CPU + {A1FF7E97-F98F-4C5C-AD09-0E1CF4A7A4DB}.Release-MONO|Any CPU.ActiveCfg = Release-MONO|Any CPU + {A1FF7E97-F98F-4C5C-AD09-0E1CF4A7A4DB}.Release-MONO|Any CPU.Build.0 = Release-MONO|Any CPU + {A1FF7E97-F98F-4C5C-AD09-0E1CF4A7A4DB}.Release-MONO|x64.ActiveCfg = Release-MONO|x64 + {A1FF7E97-F98F-4C5C-AD09-0E1CF4A7A4DB}.Release-MONO|x64.Build.0 = Release-MONO|x64 + {A1FF7E97-F98F-4C5C-AD09-0E1CF4A7A4DB}.Release-MONO|x86.ActiveCfg = Release-MONO|Any CPU + {A1FF7E97-F98F-4C5C-AD09-0E1CF4A7A4DB}.Release-MONO|x86.Build.0 = Release-MONO|Any CPU + {D05E5FAF-3E05-48D2-8DEF-FD1A18EB1349}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D05E5FAF-3E05-48D2-8DEF-FD1A18EB1349}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D05E5FAF-3E05-48D2-8DEF-FD1A18EB1349}.Debug|x64.ActiveCfg = Debug|x64 + {D05E5FAF-3E05-48D2-8DEF-FD1A18EB1349}.Debug|x64.Build.0 = Debug|x64 + {D05E5FAF-3E05-48D2-8DEF-FD1A18EB1349}.Debug|x86.ActiveCfg = Debug|Any CPU + {D05E5FAF-3E05-48D2-8DEF-FD1A18EB1349}.Debug|x86.Build.0 = Debug|Any CPU + {D05E5FAF-3E05-48D2-8DEF-FD1A18EB1349}.Debug-MONO|Any CPU.ActiveCfg = Debug-MONO|Any CPU + {D05E5FAF-3E05-48D2-8DEF-FD1A18EB1349}.Debug-MONO|Any CPU.Build.0 = Debug-MONO|Any CPU + {D05E5FAF-3E05-48D2-8DEF-FD1A18EB1349}.Debug-MONO|x64.ActiveCfg = Debug-MONO|x64 + {D05E5FAF-3E05-48D2-8DEF-FD1A18EB1349}.Debug-MONO|x64.Build.0 = Debug-MONO|x64 + {D05E5FAF-3E05-48D2-8DEF-FD1A18EB1349}.Debug-MONO|x86.ActiveCfg = Debug-MONO|Any CPU + {D05E5FAF-3E05-48D2-8DEF-FD1A18EB1349}.Debug-MONO|x86.Build.0 = Debug-MONO|Any CPU + {D05E5FAF-3E05-48D2-8DEF-FD1A18EB1349}.MachineIndependent|Any CPU.ActiveCfg = MachineIndependent|Any CPU + {D05E5FAF-3E05-48D2-8DEF-FD1A18EB1349}.MachineIndependent|Any CPU.Build.0 = MachineIndependent|Any CPU + {D05E5FAF-3E05-48D2-8DEF-FD1A18EB1349}.MachineIndependent|x64.ActiveCfg = MachineIndependent|x64 + {D05E5FAF-3E05-48D2-8DEF-FD1A18EB1349}.MachineIndependent|x64.Build.0 = MachineIndependent|x64 + {D05E5FAF-3E05-48D2-8DEF-FD1A18EB1349}.MachineIndependent|x86.ActiveCfg = MachineIndependent|Any CPU + {D05E5FAF-3E05-48D2-8DEF-FD1A18EB1349}.MachineIndependent|x86.Build.0 = MachineIndependent|Any CPU + {D05E5FAF-3E05-48D2-8DEF-FD1A18EB1349}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D05E5FAF-3E05-48D2-8DEF-FD1A18EB1349}.Release|Any CPU.Build.0 = Release|Any CPU + {D05E5FAF-3E05-48D2-8DEF-FD1A18EB1349}.Release|x64.ActiveCfg = Release|x64 + {D05E5FAF-3E05-48D2-8DEF-FD1A18EB1349}.Release|x64.Build.0 = Release|x64 + {D05E5FAF-3E05-48D2-8DEF-FD1A18EB1349}.Release|x86.ActiveCfg = Release|Any CPU + {D05E5FAF-3E05-48D2-8DEF-FD1A18EB1349}.Release|x86.Build.0 = Release|Any CPU + {D05E5FAF-3E05-48D2-8DEF-FD1A18EB1349}.Release-MONO|Any CPU.ActiveCfg = Release-MONO|Any CPU + {D05E5FAF-3E05-48D2-8DEF-FD1A18EB1349}.Release-MONO|Any CPU.Build.0 = Release-MONO|Any CPU + {D05E5FAF-3E05-48D2-8DEF-FD1A18EB1349}.Release-MONO|x64.ActiveCfg = Release-MONO|x64 + {D05E5FAF-3E05-48D2-8DEF-FD1A18EB1349}.Release-MONO|x64.Build.0 = Release-MONO|x64 + {D05E5FAF-3E05-48D2-8DEF-FD1A18EB1349}.Release-MONO|x86.ActiveCfg = Release-MONO|Any CPU + {D05E5FAF-3E05-48D2-8DEF-FD1A18EB1349}.Release-MONO|x86.Build.0 = Release-MONO|Any CPU {B60173F0-F9F0-4688-9DF8-9ADDD57BD45F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {B60173F0-F9F0-4688-9DF8-9ADDD57BD45F}.Debug|Any CPU.Build.0 = Debug|Any CPU {B60173F0-F9F0-4688-9DF8-9ADDD57BD45F}.Debug|x64.ActiveCfg = Debug|x64 @@ -898,6 +996,36 @@ Global {F47E1A0A-7D81-40CF-B8B3-A0F4B5ADE943}.Release-MONO|x64.Build.0 = Release-MONO|x64 {F47E1A0A-7D81-40CF-B8B3-A0F4B5ADE943}.Release-MONO|x86.ActiveCfg = Release-MONO|Any CPU {F47E1A0A-7D81-40CF-B8B3-A0F4B5ADE943}.Release-MONO|x86.Build.0 = Release-MONO|Any CPU + {65749C80-47E7-42FE-B441-7A86289D46AA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {65749C80-47E7-42FE-B441-7A86289D46AA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {65749C80-47E7-42FE-B441-7A86289D46AA}.Debug|x64.ActiveCfg = Debug|x64 + {65749C80-47E7-42FE-B441-7A86289D46AA}.Debug|x64.Build.0 = Debug|x64 + {65749C80-47E7-42FE-B441-7A86289D46AA}.Debug|x86.ActiveCfg = Debug|Any CPU + {65749C80-47E7-42FE-B441-7A86289D46AA}.Debug|x86.Build.0 = Debug|Any CPU + {65749C80-47E7-42FE-B441-7A86289D46AA}.Debug-MONO|Any CPU.ActiveCfg = Debug-MONO|Any CPU + {65749C80-47E7-42FE-B441-7A86289D46AA}.Debug-MONO|Any CPU.Build.0 = Debug-MONO|Any CPU + {65749C80-47E7-42FE-B441-7A86289D46AA}.Debug-MONO|x64.ActiveCfg = Debug-MONO|x64 + {65749C80-47E7-42FE-B441-7A86289D46AA}.Debug-MONO|x64.Build.0 = Debug-MONO|x64 + {65749C80-47E7-42FE-B441-7A86289D46AA}.Debug-MONO|x86.ActiveCfg = Debug-MONO|Any CPU + {65749C80-47E7-42FE-B441-7A86289D46AA}.Debug-MONO|x86.Build.0 = Debug-MONO|Any CPU + {65749C80-47E7-42FE-B441-7A86289D46AA}.MachineIndependent|Any CPU.ActiveCfg = MachineIndependent|Any CPU + {65749C80-47E7-42FE-B441-7A86289D46AA}.MachineIndependent|Any CPU.Build.0 = MachineIndependent|Any CPU + {65749C80-47E7-42FE-B441-7A86289D46AA}.MachineIndependent|x64.ActiveCfg = MachineIndependent|x64 + {65749C80-47E7-42FE-B441-7A86289D46AA}.MachineIndependent|x64.Build.0 = MachineIndependent|x64 + {65749C80-47E7-42FE-B441-7A86289D46AA}.MachineIndependent|x86.ActiveCfg = MachineIndependent|Any CPU + {65749C80-47E7-42FE-B441-7A86289D46AA}.MachineIndependent|x86.Build.0 = MachineIndependent|Any CPU + {65749C80-47E7-42FE-B441-7A86289D46AA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {65749C80-47E7-42FE-B441-7A86289D46AA}.Release|Any CPU.Build.0 = Release|Any CPU + {65749C80-47E7-42FE-B441-7A86289D46AA}.Release|x64.ActiveCfg = Release|x64 + {65749C80-47E7-42FE-B441-7A86289D46AA}.Release|x64.Build.0 = Release|x64 + {65749C80-47E7-42FE-B441-7A86289D46AA}.Release|x86.ActiveCfg = Release|Any CPU + {65749C80-47E7-42FE-B441-7A86289D46AA}.Release|x86.Build.0 = Release|Any CPU + {65749C80-47E7-42FE-B441-7A86289D46AA}.Release-MONO|Any CPU.ActiveCfg = Release-MONO|Any CPU + {65749C80-47E7-42FE-B441-7A86289D46AA}.Release-MONO|Any CPU.Build.0 = Release-MONO|Any CPU + {65749C80-47E7-42FE-B441-7A86289D46AA}.Release-MONO|x64.ActiveCfg = Release-MONO|x64 + {65749C80-47E7-42FE-B441-7A86289D46AA}.Release-MONO|x64.Build.0 = Release-MONO|x64 + {65749C80-47E7-42FE-B441-7A86289D46AA}.Release-MONO|x86.ActiveCfg = Release-MONO|Any CPU + {65749C80-47E7-42FE-B441-7A86289D46AA}.Release-MONO|x86.Build.0 = Release-MONO|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From df6d15ba3b5f4ec75be156b812e2808949565fa8 Mon Sep 17 00:00:00 2001 From: Ladi Prosek Date: Tue, 5 Jan 2021 12:48:52 +0100 Subject: [PATCH 05/14] Update packaging and deployment files --- scripts/Deploy-MSBuild.ps1 | 1 + src/Package/DevDivPackage/VS.ExternalAPIs.MSBuild.nuspec | 1 + .../MSBuild.Engine.Corext/MsBuild.Engine.Corext.nuspec | 4 ++++ src/Package/MSBuild.VSSetup/files.swr | 5 +++++ 4 files changed, 11 insertions(+) diff --git a/scripts/Deploy-MSBuild.ps1 b/scripts/Deploy-MSBuild.ps1 index 125e9447ca5..47eec2ccff8 100644 --- a/scripts/Deploy-MSBuild.ps1 +++ b/scripts/Deploy-MSBuild.ps1 @@ -58,6 +58,7 @@ $filesToCopyToBin = @( FileToCopy "$bootstrapBinDirectory\Microsoft.Build.Framework.dll" FileToCopy "$bootstrapBinDirectory\Microsoft.Build.Tasks.Core.dll" FileToCopy "$bootstrapBinDirectory\Microsoft.Build.Utilities.Core.dll" + FileToCopy "$bootstrapBinDirectory\Microsoft.NET.StringTools.dll" FileToCopy "$bootstrapBinDirectory\en\Microsoft.Build.resources.dll" "en" FileToCopy "$bootstrapBinDirectory\en\Microsoft.Build.Tasks.Core.resources.dll" "en" diff --git a/src/Package/DevDivPackage/VS.ExternalAPIs.MSBuild.nuspec b/src/Package/DevDivPackage/VS.ExternalAPIs.MSBuild.nuspec index 14bfd426aff..338a4620c27 100644 --- a/src/Package/DevDivPackage/VS.ExternalAPIs.MSBuild.nuspec +++ b/src/Package/DevDivPackage/VS.ExternalAPIs.MSBuild.nuspec @@ -18,6 +18,7 @@ + diff --git a/src/Package/MSBuild.Engine.Corext/MsBuild.Engine.Corext.nuspec b/src/Package/MSBuild.Engine.Corext/MsBuild.Engine.Corext.nuspec index 9482f3faf5d..2918e172a5c 100644 --- a/src/Package/MSBuild.Engine.Corext/MsBuild.Engine.Corext.nuspec +++ b/src/Package/MSBuild.Engine.Corext/MsBuild.Engine.Corext.nuspec @@ -43,6 +43,8 @@ + + @@ -97,6 +99,8 @@ + + diff --git a/src/Package/MSBuild.VSSetup/files.swr b/src/Package/MSBuild.VSSetup/files.swr index 2f87ef174f2..79d3960e612 100644 --- a/src/Package/MSBuild.VSSetup/files.swr +++ b/src/Package/MSBuild.VSSetup/files.swr @@ -48,6 +48,8 @@ folder InstallDir:\MSBuild\Current\Bin file source=$(X86BinPath)System.Resources.Extensions.dll vs.file.ngenArchitecture=all file source=$(X86BinPath)System.Runtime.CompilerServices.Unsafe.dll vs.file.ngenArchitecture=all file source=$(X86BinPath)System.Threading.Tasks.Dataflow.dll vs.file.ngenArchitecture=all vs.file.ngenPriority=1 + file source=$(X86BinPath)Microsoft.NET.StringTools.dll vs.file.ngenArchitecture=all + file source=$(TaskHostBinPath)Microsoft.NET.StringTools.net35.dll vs.file.ngenArchitecture=all file source=$(X86BinPath)System.Collections.Immutable.dll vs.file.ngenApplications="[installDir]\MSBuild\Current\Bin\MSBuild.exe" vs.file.ngenArchitecture=all vs.file.ngenPriority=1 file source=$(X86BinPath)Microsoft.Common.CurrentVersion.targets file source=$(X86BinPath)Microsoft.Common.CrossTargeting.targets @@ -198,6 +200,8 @@ folder InstallDir:\MSBuild\Current\Bin\amd64 file source=$(X86BinPath)System.Runtime.CompilerServices.Unsafe.dll vs.file.ngenArchitecture=all file source=$(X86BinPath)System.Threading.Tasks.Dataflow.dll vs.file.ngenArchitecture=all file source=$(X86BinPath)System.Collections.Immutable.dll vs.file.ngenArchitecture=all + file source=$(X86BinPath)Microsoft.NET.StringTools.dll vs.file.ngenArchitecture=all + file source=$(TaskHostBinPath)Microsoft.NET.StringTools.net35.dll vs.file.ngenArchitecture=all file source=$(X86BinPath)Microsoft.Common.CurrentVersion.targets file source=$(X86BinPath)Microsoft.Common.CrossTargeting.targets file source=$(X86BinPath)Microsoft.Common.overridetasks @@ -321,6 +325,7 @@ folder InstallDir:\Common7\IDE\CommonExtensions\MSBuild file source=$(SourceDir)Build\Microsoft.Build.pkgdef file source=$(SourceDir)Build\System.Text.Encodings.Web.pkgdef file source=$(SourceDir)Build\System.Text.Json.pkgdef + file source=$(SourceDir)StringTools\StringTools.pkgdef file source=$(SourceDir)Tasks\Microsoft.Build.Tasks.Core.pkgdef file source=$(SourceDir)Tasks\System.Resources.Extensions.pkgdef file source=$(SourceDir)Utilities\Microsoft.Build.Utilities.Core.pkgdef From f166d2da51c07db5d51731f222cfd57335042467 Mon Sep 17 00:00:00 2001 From: Ladi Prosek Date: Tue, 5 Jan 2021 12:58:06 +0100 Subject: [PATCH 06/14] Add project references to StringTools --- src/Build/Microsoft.Build.csproj | 1 + src/MSBuildTaskHost/MSBuildTaskHost.csproj | 3 +++ src/Tasks/Microsoft.Build.Tasks.csproj | 1 + src/Utilities/Microsoft.Build.Utilities.csproj | 3 ++- 4 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Build/Microsoft.Build.csproj b/src/Build/Microsoft.Build.csproj index b14c97d1565..1f773903ed9 100644 --- a/src/Build/Microsoft.Build.csproj +++ b/src/Build/Microsoft.Build.csproj @@ -28,6 +28,7 @@ + diff --git a/src/MSBuildTaskHost/MSBuildTaskHost.csproj b/src/MSBuildTaskHost/MSBuildTaskHost.csproj index 615488880be..901c6a53a8e 100644 --- a/src/MSBuildTaskHost/MSBuildTaskHost.csproj +++ b/src/MSBuildTaskHost/MSBuildTaskHost.csproj @@ -210,6 +210,9 @@ + + +