Skip to content

Commit

Permalink
Cache StringBuilder instances in the .NET JsonTextTokenizer.
Browse files Browse the repository at this point in the history
COPYBARA_INTEGRATE_REVIEW=#15794 from TrayanZapryanov:cache_stringbuilder 596147e
PiperOrigin-RevId: 613251480
  • Loading branch information
TrayanZapryanov authored and Copybara-Service committed Mar 6, 2024
1 parent 3dadd0e commit fac929d
Showing 1 changed file with 63 additions and 7 deletions.
70 changes: 63 additions & 7 deletions csharp/src/Google.Protobuf/JsonTokenizer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,8 @@ private void ValidateState(State validStates, string errorPrefix)
/// </summary>
private string ReadString()
{
var value = new StringBuilder();
//builder will not be released in case of an exception, but this is not a problem and we will create new on next Acquire
var builder = StringBuilderCache.Acquire();
bool haveHighSurrogate = false;
while (true)
{
Expand All @@ -316,7 +317,7 @@ private string ReadString()
{
throw reader.CreateException("Invalid use of surrogate pair code units");
}
return value.ToString();
return StringBuilderCache.GetStringAndRelease(builder);
}
if (c == '\\')
{
Expand All @@ -330,7 +331,7 @@ private string ReadString()
throw reader.CreateException("Invalid use of surrogate pair code units");
}
haveHighSurrogate = char.IsHighSurrogate(c);
value.Append(c);
builder.Append(c);
}
}

Expand Down Expand Up @@ -408,7 +409,8 @@ private void ConsumeLiteral(string text)

private double ReadNumber(char initialCharacter)
{
StringBuilder builder = new StringBuilder();
//builder will not be released in case of an exception, but this is not a problem and we will create new on next Acquire
var builder = StringBuilderCache.Acquire();
if (initialCharacter == '-')
{
builder.Append("-");
Expand Down Expand Up @@ -437,24 +439,25 @@ private double ReadNumber(char initialCharacter)
}

// TODO: What exception should we throw if the value can't be represented as a double?
var builderValue = StringBuilderCache.GetStringAndRelease(builder);
try
{
double result = double.Parse(builder.ToString(),
double result = double.Parse(builderValue,
NumberStyles.AllowLeadingSign | NumberStyles.AllowDecimalPoint | NumberStyles.AllowExponent,
CultureInfo.InvariantCulture);

// .NET Core 3.0 and later returns infinity if the number is too large or small to be represented.
// For compatibility with other Protobuf implementations the tokenizer should still throw.
if (double.IsInfinity(result))
{
throw reader.CreateException("Numeric value out of range: " + builder);
throw reader.CreateException("Numeric value out of range: " + builderValue);
}

return result;
}
catch (OverflowException)
{
throw reader.CreateException("Numeric value out of range: " + builder);
throw reader.CreateException("Numeric value out of range: " + builderValue);
}
}

Expand Down Expand Up @@ -728,6 +731,59 @@ internal InvalidJsonException CreateException(string message)
return new InvalidJsonException(message);
}
}

/// <summary>
/// Provide a cached reusable instance of stringbuilder per thread.
/// Copied from https://github.com/dotnet/runtime/blob/main/src/libraries/Common/src/System/Text/StringBuilderCache.cs
/// </summary>
private static class StringBuilderCache
{
private const int MaxCachedStringBuilderSize = 360;
private const int DefaultStringBuilderCapacity = 16; // == StringBuilder.DefaultCapacity

[ThreadStatic]
private static StringBuilder cachedInstance;

/// <summary>Get a StringBuilder for the specified capacity.</summary>
/// <remarks>If a StringBuilder of an appropriate size is cached, it will be returned and the cache emptied.</remarks>
public static StringBuilder Acquire(int capacity = DefaultStringBuilderCapacity)
{
if (capacity <= MaxCachedStringBuilderSize)
{
StringBuilder sb = cachedInstance;
if (sb != null)
{
// Avoid stringbuilder block fragmentation by getting a new StringBuilder
// when the requested size is larger than the current capacity
if (capacity <= sb.Capacity)
{
cachedInstance = null;
sb.Clear();
return sb;
}
}
}

return new StringBuilder(capacity);
}

/// <summary>Place the specified builder in the cache if it is not too big.</summary>
private static void Release(StringBuilder sb)
{
if (sb.Capacity <= MaxCachedStringBuilderSize)
{
cachedInstance = cachedInstance?.Capacity >= sb.Capacity ? cachedInstance : sb;
}
}

/// <summary>ToString() the stringbuilder, Release it to the cache, and return the resulting string.</summary>
public static string GetStringAndRelease(StringBuilder sb)
{
string result = sb.ToString();
Release(sb);
return result;
}
}
}
}
}

0 comments on commit fac929d

Please sign in to comment.