// 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.Diagnostics.CodeAnalysis; using System.Globalization; using System.Text; using System.Threading; namespace Microsoft.Build.Shared { /// /// A StringBuilder lookalike that reuses its internal storage. /// /// /// This class is being deprecated in favor of SpanBasedStringBuilder in StringTools. Avoid adding more uses. /// internal sealed class ReuseableStringBuilder : IDisposable { /// /// Captured string builder. /// private StringBuilder _borrowedBuilder; /// /// Capacity to initialize the builder with. /// private int _capacity; /// /// Create a new builder, under the covers wrapping a reused one. /// internal ReuseableStringBuilder(int capacity = 16) // StringBuilder default is 16 { _capacity = capacity; // lazy initialization of the builder } /// /// The length of the target. /// public int Length { get { return (_borrowedBuilder == null) ? 0 : _borrowedBuilder.Length; } set { LazyPrepare(); _borrowedBuilder.Length = value; } } /// /// Convert to a string. /// public override string ToString() { if (_borrowedBuilder == null) { return String.Empty; } return _borrowedBuilder.ToString(); } /// /// Dispose, indicating you are done with this builder. /// void IDisposable.Dispose() { if (_borrowedBuilder != null) { ReuseableStringBuilderFactory.Release(_borrowedBuilder); _borrowedBuilder = null; _capacity = -1; } } /// /// Append a character. /// internal ReuseableStringBuilder Append(char value) { LazyPrepare(); _borrowedBuilder.Append(value); return this; } /// /// Append a string. /// internal ReuseableStringBuilder Append(string value) { LazyPrepare(); _borrowedBuilder.Append(value); return this; } /// /// Append a substring. /// internal ReuseableStringBuilder Append(string value, int startIndex, int count) { LazyPrepare(); _borrowedBuilder.Append(value, startIndex, count); return this; } public ReuseableStringBuilder AppendSeparated(char separator, ICollection strings) { LazyPrepare(); var separatorsRemaining = strings.Count - 1; foreach (var s in strings) { _borrowedBuilder.Append(s); if (separatorsRemaining > 0) { _borrowedBuilder.Append(separator); } separatorsRemaining--; } return this; } public ReuseableStringBuilder Clear() { LazyPrepare(); _borrowedBuilder.Clear(); return this; } /// /// Remove a substring. /// internal ReuseableStringBuilder Remove(int startIndex, int length) { LazyPrepare(); _borrowedBuilder.Remove(startIndex, length); return this; } /// /// Grab a backing builder if necessary. /// private void LazyPrepare() { if (_borrowedBuilder == null) { ErrorUtilities.VerifyThrow(_capacity != -1, "Reusing after dispose"); _borrowedBuilder = ReuseableStringBuilderFactory.Get(_capacity); } } /// /// A utility class that mediates access to a shared string builder. /// /// /// If this shared builder is highly contended, this class could add /// a second one and try both in turn. /// private static class ReuseableStringBuilderFactory { /// /// Made up limit beyond which we won't share the builder /// because we could otherwise hold a huge builder indefinitely. /// This size seems reasonable for MSBuild uses (mostly expression expansion) /// private const int MaxBuilderSize = 1024; /// /// The shared builder. /// private static StringBuilder s_sharedBuilder; #if DEBUG /// /// Count of successful reuses /// private static int s_hits = 0; /// /// Count of failed reuses - a new builder was created /// private static int s_misses = 0; /// /// Count of times the builder capacity was raised to satisfy the caller's request /// private static int s_upsizes = 0; /// /// Count of times the returned builder was discarded because it was too large /// private static int s_discards = 0; /// /// Count of times the builder was returned. /// private static int s_accepts = 0; /// /// Aggregate capacity saved (aggregate midpoints of requested and returned) /// private static int s_saved = 0; /// /// Callstacks of those handed out and not returned yet /// private static ConcurrentDictionary s_handouts = new ConcurrentDictionary(); #endif /// /// Obtains a string builder which may or may not already /// have been used. /// Never returns null. /// internal static StringBuilder Get(int capacity) { #if DEBUG bool missed = false; #endif var returned = Interlocked.Exchange(ref s_sharedBuilder, null); if (returned == null) { #if DEBUG missed = true; Interlocked.Increment(ref s_misses); #endif // Currently loaned out so return a new one returned = new StringBuilder(capacity); } else if (returned.Capacity < capacity) { #if DEBUG Interlocked.Increment(ref s_upsizes); #endif // It's essential we guarantee the capacity because this // may be used as a buffer to a PInvoke call. returned.Capacity = capacity; } #if DEBUG Interlocked.Increment(ref s_hits); if (!missed) { Interlocked.Add(ref s_saved, (capacity + returned.Capacity) / 2); } // handouts.TryAdd(returned, Environment.StackTrace); #endif return returned; } /// /// Returns the shared builder for the next caller to use. /// ** CALLERS, DO NOT USE THE BUILDER AFTER RELEASING IT HERE! ** /// internal static void Release(StringBuilder returningBuilder) { // It's possible for someone to cause the builder to // enlarge to such an extent that this static field // would be a leak. To avoid that, only accept // the builder if it's no more than a certain size. // // If some code has a bug and forgets to return their builder // (or we refuse it here because it's too big) the next user will // get given a new one, and then return it soon after. // So the shared builder will be "replaced". if (returningBuilder.Capacity < MaxBuilderSize) { // ErrorUtilities.VerifyThrow(handouts.TryRemove(returningBuilder, out dummy), "returned but not loaned"); returningBuilder.Clear(); // Clear before pooling Interlocked.Exchange(ref s_sharedBuilder, returningBuilder); #if DEBUG Interlocked.Increment(ref s_accepts); } else { Interlocked.Increment(ref s_discards); #endif } } #if DEBUG /// /// Debugging dumping /// [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "Handy helper method that can be used to annotate ReuseableStringBuilder when debugging it, but is not hooked up usually for the sake of perf.")] [SuppressMessage("Microsoft.Usage", "CA1806:DoNotIgnoreMethodResults", MessageId = "System.String.Format(System.IFormatProvider,System.String,System.Object[])", Justification = "Handy string that can be used to annotate ReuseableStringBuilder when debugging it, but is not hooked up usually.")] internal static void DumpUnreturned() { String.Format(CultureInfo.CurrentUICulture, "{0} Hits of which\n {1} Misses (was on loan)\n {2} Upsizes (needed bigger) \n\n{3} Returns=\n{4} Discards (returned too large)+\n {5} Accepts\n\n{6} estimated bytes saved", s_hits, s_misses, s_upsizes, s_discards + s_accepts, s_discards, s_accepts, s_saved); Console.WriteLine("Unreturned string builders were allocated here:"); foreach (var entry in s_handouts.Values) { Console.WriteLine(entry + "\n"); } } #endif } } }