Skip to content

Commit

Permalink
Add SpanStringTokenizer and avoid many string allocations in FontColl…
Browse files Browse the repository at this point in the history
…ectionBase (#17745)

* Create ref struct variant of StringTokenizer and reduce string allocations in FontCollectionBase

* Replace constant enum mappings with EnumHelper shim calls

---------

Co-authored-by: Max Katz <[email protected]>
  • Loading branch information
Washi1337 and maxkatz6 authored Dec 15, 2024
1 parent af11240 commit e991d63
Show file tree
Hide file tree
Showing 24 changed files with 427 additions and 104 deletions.
2 changes: 1 addition & 1 deletion src/Avalonia.Base/Animation/KeySpline.cs
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ public static KeySpline Parse(string value, CultureInfo? culture)
{
culture ??= CultureInfo.InvariantCulture;

using var tokenizer = new StringTokenizer(value, culture, exceptionMessage: $"Invalid KeySpline string: \"{value}\".");
using var tokenizer = new SpanStringTokenizer(value, culture, exceptionMessage: $"Invalid KeySpline string: \"{value}\".");
return new KeySpline(tokenizer.ReadDouble(), tokenizer.ReadDouble(), tokenizer.ReadDouble(), tokenizer.ReadDouble());
}

Expand Down
2 changes: 1 addition & 1 deletion src/Avalonia.Base/Animation/Spring.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ public static Spring Parse(string value, CultureInfo? culture)
culture = CultureInfo.InvariantCulture;
}

using var tokenizer = new StringTokenizer(value, culture, exceptionMessage: $"Invalid Spring string: \"{value}\".");
using var tokenizer = new SpanStringTokenizer(value, culture, exceptionMessage: $"Invalid Spring string: \"{value}\".");
return new Spring(tokenizer.ReadDouble(), tokenizer.ReadDouble(), tokenizer.ReadDouble(), tokenizer.ReadDouble());
}

Expand Down
2 changes: 1 addition & 1 deletion src/Avalonia.Base/CornerRadius.cs
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ public static CornerRadius Parse(string s)
{
const string exceptionMessage = "Invalid CornerRadius.";

using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage))
using (var tokenizer = new SpanStringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage))
{
if (tokenizer.TryReadDouble(out var a))
{
Expand Down
2 changes: 1 addition & 1 deletion src/Avalonia.Base/Matrix.cs
Original file line number Diff line number Diff line change
Expand Up @@ -504,7 +504,7 @@ public static Matrix Parse(string s)
double v8 = 0;
double v9 = 0;

using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid Matrix."))
using (var tokenizer = new SpanStringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid Matrix."))
{
var v1 = tokenizer.ReadDouble();
var v2 = tokenizer.ReadDouble();
Expand Down
116 changes: 31 additions & 85 deletions src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Text;
using Avalonia.Platform;
using Avalonia.Utilities;

Expand Down Expand Up @@ -181,7 +182,7 @@ internal static bool TryFindWeightFallback(
glyphTypeface = null;
var weight = (int)key.Weight;

//If the target weight given is between 400 and 500 inclusive
//If the target weight given is between 400 and 500 inclusive
if (weight >= 400 && weight <= 500)
{
//Look for available weights between the target and 500, in ascending order.
Expand Down Expand Up @@ -212,7 +213,7 @@ internal static bool TryFindWeightFallback(
}
}

//If a weight less than 400 is given, look for available weights less than the target, in descending order.
//If a weight less than 400 is given, look for available weights less than the target, in descending order.
if (weight < 400)
{
for (var i = 0; weight - i >= 100; i += 50)
Expand Down Expand Up @@ -271,109 +272,54 @@ internal static Typeface GetImplicitTypeface(Typeface typeface, out string norma
var weight = typeface.Weight;
var stretch = typeface.Stretch;

if(TryGetStyle(ref normalizedFamilyName, out var foundStyle))
{
style = foundStyle;
}

if(TryGetWeight(ref normalizedFamilyName, out var foundWeight))
{
weight = foundWeight;
}

if(TryGetStretch(ref normalizedFamilyName, out var foundStretch))
{
stretch = foundStretch;
}

//Preserve old font source
return new Typeface(typeface.FontFamily, style, weight, stretch);

}

internal static bool TryGetWeight(ref string familyName, out FontWeight weight)
{
weight = FontWeight.Normal;
StringBuilder? normalizedFamilyNameBuilder = null;
var totalCharsRemoved = 0;

var tokenizer = new StringTokenizer(familyName, ' ');
var tokenizer = new SpanStringTokenizer(normalizedFamilyName, ' ');

// Skip initial family name.
tokenizer.ReadSpan();

while (tokenizer.TryReadString(out var weightString))
while (tokenizer.TryReadSpan(out var token))
{
if (new StringTokenizer(weightString).TryReadInt32(out _))
// Don't try to match numbers.
if (new SpanStringTokenizer(token).TryReadInt32(out _))
{
continue;
}

if (!Enum.TryParse(weightString, true, out weight))
{
continue;
}

familyName = familyName.Replace(" " + weightString, "").TrimEnd();

return true;
}

return false;
}

internal static bool TryGetStyle(ref string familyName, out FontStyle style)
{
style = FontStyle.Normal;

var tokenizer = new StringTokenizer(familyName, ' ');

tokenizer.ReadSpan();

while (tokenizer.TryReadString(out var styleString))
{
//Do not try to parse an integer
if (new StringTokenizer(styleString).TryReadInt32(out _))
// Try match with font style, weight or stretch and update accordingly.
var match = false;
if (EnumHelper.TryParse<FontStyle>(token, true, out var newStyle))
{
continue;
style = newStyle;
match = true;
}

if (!Enum.TryParse(styleString, true, out style))
else if (EnumHelper.TryParse<FontWeight>(token, true, out var newWeight))
{
continue;
weight = newWeight;
match = true;
}

familyName = familyName.Replace(" " + styleString, "").TrimEnd();

return true;
}

return false;
}

internal static bool TryGetStretch(ref string familyName, out FontStretch stretch)
{
stretch = FontStretch.Normal;

var tokenizer = new StringTokenizer(familyName, ' ');

tokenizer.ReadSpan();

while (tokenizer.TryReadString(out var stretchString))
{
if (new StringTokenizer(stretchString).TryReadInt32(out _))
else if (EnumHelper.TryParse<FontStretch>(token, true, out var newStretch))
{
continue;
stretch = newStretch;
match = true;
}

if (!Enum.TryParse(stretchString, true, out stretch))
if (match)
{
continue;
// Carve out matched word from the normalized name.
normalizedFamilyNameBuilder ??= new StringBuilder(normalizedFamilyName);
normalizedFamilyNameBuilder.Remove(tokenizer.CurrentTokenIndex - totalCharsRemoved, token.Length);
totalCharsRemoved += token.Length;
}

familyName = familyName.Replace(" " + stretchString, "").TrimEnd();

return true;
}

return false;
// Get rid of any trailing spaces.
normalizedFamilyName = (normalizedFamilyNameBuilder?.ToString() ?? normalizedFamilyName).TrimEnd();

//Preserve old font source
return new Typeface(typeface.FontFamily, style, weight, stretch);
}
}
}
2 changes: 1 addition & 1 deletion src/Avalonia.Base/Media/TextDecorationCollection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public static TextDecorationCollection Parse(string s)
{
var locations = new List<TextDecorationLocation>();

using (var tokenizer = new StringTokenizer(s, ',', "Invalid text decoration."))
using (var tokenizer = new SpanStringTokenizer(s, ',', "Invalid text decoration."))
{
while (tokenizer.TryReadSpan(out var name))
{
Expand Down
2 changes: 1 addition & 1 deletion src/Avalonia.Base/PixelPoint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ public static implicit operator PixelVector(PixelPoint p)
/// <returns>The <see cref="PixelPoint"/>.</returns>
public static PixelPoint Parse(string s)
{
using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid PixelPoint."))
using (var tokenizer = new SpanStringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid PixelPoint."))
{
return new PixelPoint(
tokenizer.ReadInt32(),
Expand Down
2 changes: 1 addition & 1 deletion src/Avalonia.Base/PixelRect.cs
Original file line number Diff line number Diff line change
Expand Up @@ -433,7 +433,7 @@ public override string ToString()
/// <returns>The parsed <see cref="PixelRect"/>.</returns>
public static PixelRect Parse(string s)
{
using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid PixelRect."))
using (var tokenizer = new SpanStringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid PixelRect."))
{
return new PixelRect(
tokenizer.ReadInt32(),
Expand Down
2 changes: 1 addition & 1 deletion src/Avalonia.Base/PixelSize.cs
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ public static bool TryParse([NotNullWhen(true)] string? source,
{
return false;
}
using (var tokenizer = new StringTokenizer(source, exceptionMessage: "Invalid PixelSize."))
using (var tokenizer = new SpanStringTokenizer(source, exceptionMessage: "Invalid PixelSize."))
{
if (tokenizer.TryReadInt32(out var w) && tokenizer.TryReadInt32(out var h))
{
Expand Down
2 changes: 1 addition & 1 deletion src/Avalonia.Base/Point.cs
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ public static double Distance(Point value1, Point value2)
/// <returns>The <see cref="Point"/>.</returns>
public static Point Parse(string s)
{
using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid Point."))
using (var tokenizer = new SpanStringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid Point."))
{
return new Point(
tokenizer.ReadDouble(),
Expand Down
2 changes: 1 addition & 1 deletion src/Avalonia.Base/Rect.cs
Original file line number Diff line number Diff line change
Expand Up @@ -592,7 +592,7 @@ public override string ToString()
/// <returns>The parsed <see cref="Rect"/>.</returns>
public static Rect Parse(string s)
{
using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid Rect."))
using (var tokenizer = new SpanStringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid Rect."))
{
return new Rect(
tokenizer.ReadDouble(),
Expand Down
2 changes: 1 addition & 1 deletion src/Avalonia.Base/RelativePoint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ public Point ToPixels(Rect rect)
/// <returns>The parsed <see cref="RelativePoint"/>.</returns>
public static RelativePoint Parse(string s)
{
using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid RelativePoint."))
using (var tokenizer = new SpanStringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid RelativePoint."))
{
var x = tokenizer.ReadString();
var y = tokenizer.ReadString();
Expand Down
2 changes: 1 addition & 1 deletion src/Avalonia.Base/RelativeRect.cs
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ public Rect ToPixels(Rect boundingBox)
/// <returns>The parsed <see cref="RelativeRect"/>.</returns>
public static RelativeRect Parse(string s)
{
using (var tokenizer = new StringTokenizer(s, exceptionMessage: "Invalid RelativeRect."))
using (var tokenizer = new SpanStringTokenizer(s, exceptionMessage: "Invalid RelativeRect."))
{
var x = tokenizer.ReadSpan();
var y = tokenizer.ReadSpan();
Expand Down
2 changes: 1 addition & 1 deletion src/Avalonia.Base/Size.cs
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ public Size(System.Numerics.Vector2 vector2) : this(vector2.X, vector2.Y)
/// <returns>The <see cref="Size"/>.</returns>
public static Size Parse(string s)
{
using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid Size."))
using (var tokenizer = new SpanStringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid Size."))
{
return new Size(
tokenizer.ReadDouble(),
Expand Down
2 changes: 1 addition & 1 deletion src/Avalonia.Base/Thickness.cs
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ public static Thickness Parse(string s)
{
const string exceptionMessage = "Invalid Thickness.";

using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage))
using (var tokenizer = new SpanStringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage))
{
if (tokenizer.TryReadDouble(out var a))
{
Expand Down
15 changes: 15 additions & 0 deletions src/Avalonia.Base/Utilities/EnumHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,26 @@ public static T Parse<T>(ReadOnlySpan<char> key, bool ignoreCase) where T : stru
{
return Enum.Parse<T>(key, ignoreCase);
}

public static bool TryParse<T>(ReadOnlySpan<char> key, bool ignoreCase, out T result) where T : struct
{
return Enum.TryParse(key, ignoreCase, out result);
}
#else
public static T Parse<T>(string key, bool ignoreCase) where T : struct
{
return (T)Enum.Parse(typeof(T), key, ignoreCase);
}

public static bool TryParse<T>(string key, bool ignoreCase, out T result) where T : struct
{
return Enum.TryParse(key, ignoreCase, out result);
}

public static bool TryParse<T>(ReadOnlySpan<char> key, bool ignoreCase, out T result) where T : struct
{
return Enum.TryParse(key.ToString(), ignoreCase, out result);
}
#endif
}
}
Loading

0 comments on commit e991d63

Please sign in to comment.