Skip to content

Commit

Permalink
Merge pull request unoplatform#14534 from Youssef1313/xaml-gen-perf
Browse files Browse the repository at this point in the history
  • Loading branch information
MartinZikmund authored Nov 29, 2023
2 parents 8929b90 + 7762480 commit c53a8c9
Show file tree
Hide file tree
Showing 2 changed files with 102 additions and 13 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.RegularExpressions;
using Microsoft.CodeAnalysis;
Expand Down Expand Up @@ -4564,26 +4565,99 @@ private string RewriteNamespaces(string xamlString)
{
foreach (var ns in _fileDefinition.Namespaces)
{
xamlString = ReplaceNamespace(xamlString, ns);
}

return xamlString;

// This is a performance-sensitive code. It used to be using Regex and was improved to avoid Regex.
// It might be possible to improve it further, but that seems to do the job for now.
// We can optimize further if future performance measures shows it being problematic.
// What this method does is:
// Given a xamlString like "muxc:SomeControl", converts it to Microsoft.UI.Xaml.Controls.SomeControl.
// Note that the given xamlString can be a more complex expression involving multiple namespaces.
static string ReplaceNamespace(string xamlString, NamespaceDeclaration ns)
{
// Note: The call xamlString.IndexOf($"{ns.Prefix}:", StringComparison.Ordinal) can be replaced with xamlString.IndexOf(ns.Prefix)
// followed by a separate check for ":" character. This will save a string allocation.
// But for now, we keep it as is. We can always revisit if it shows performance issues.
if (ns.Namespace.StartsWith("using:", StringComparison.Ordinal))
{
// Replace namespaces with their fully qualified namespace.
// Add global:: so that qualified paths can be expluded from binding
// path observation.
xamlString = Regex.Replace(
xamlString,
$@"(^|[^\w])({ns.Prefix}:)",
"$1global::" + ns.Namespace.TrimStart("using:") + ".");
while (xamlString.Length > ns.Prefix.Length)
{
var index = xamlString.IndexOf($"{ns.Prefix}:", StringComparison.Ordinal);
if (index == 0 || (index > 0 && !char.IsLetterOrDigit(xamlString[index - 1])))
{
var nsGlobalized = ns.Namespace.Replace("using:", "global::");

// following is the equivalent of "string.Concat(xamlString.Substring(0, index), nsGlobalized, ".", xamlString.Substring(index + ns.Prefix.Length + 1))"
// but in a way that allocates less memory.
var newLength = xamlString.Length + nsGlobalized.Length - ns.Prefix.Length;
xamlString = StringExtensions.Create(newLength, (xamlString, index, nsGlobalized, ns.Prefix), static (span, state) =>
{
var (xamlString, index, nsGlobalized, nsPrefix) = state;
var copiedLengthSoFar = 0;

xamlString.AsSpan().Slice(0, index).CopyTo(span.Slice(copiedLengthSoFar));
copiedLengthSoFar += index;

nsGlobalized.AsSpan().CopyTo(span.Slice(copiedLengthSoFar));
copiedLengthSoFar += nsGlobalized.Length;

span[copiedLengthSoFar] = '.';
copiedLengthSoFar++;

xamlString.AsSpan().Slice(index + nsPrefix.Length + 1).CopyTo(span.Slice(copiedLengthSoFar));
});

}
else
{
break;
}
}

return xamlString;
}
else if (ns.Namespace == XamlConstants.XamlXmlNamespace)
{
xamlString = Regex.Replace(
xamlString,
$@"(^|[^\w])({ns.Prefix}:)",
"$1global::System.");
while (xamlString.Length > ns.Prefix.Length)
{
var index = xamlString.IndexOf($"{ns.Prefix}:", StringComparison.Ordinal);
if (index == 0 || (index > 0 && !char.IsLetterOrDigit(xamlString[index - 1])))
{
const string nsGlobalized = "global::System.";

// following is the equivalent of "string.Concat(xamlString.Substring(0, index), nsGlobalized, xamlString.Substring(index + ns.Prefix.Length + 1))"
// but in a way that allocates less memory.
var newLength = xamlString.Length + nsGlobalized.Length - ns.Prefix.Length - 1;
xamlString = StringExtensions.Create(newLength, (xamlString, index, ns.Prefix), static (span, state) =>
{
var (xamlString, index, nsPrefix) = state;
var copiedLengthSoFar = 0;

xamlString.AsSpan().Slice(0, index).CopyTo(span.Slice(copiedLengthSoFar));
copiedLengthSoFar += index;

nsGlobalized.AsSpan().CopyTo(span.Slice(copiedLengthSoFar));
copiedLengthSoFar += nsGlobalized.Length;

xamlString.AsSpan().Slice(index + nsPrefix.Length + 1).CopyTo(span.Slice(copiedLengthSoFar));
});
}
else
{
break;
}
}

return xamlString;
}
else
{
return xamlString;
}
}

return xamlString;
}

private string GetDefaultBindMode() => _currentDefaultBindMode.Peek();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@

namespace Uno.Extensions
{
public delegate void SpanAction<T, in TArg>(Span<T> span, TArg arg);

internal static partial class StringExtensions
{
// https://www.meziantou.net/split-a-string-into-lines-without-allocation.htm
Expand Down Expand Up @@ -153,5 +155,18 @@ public static string TrimEnd(this string source, string trimText)
return source;
}
}

public static string Create<TState>(int length, TState state, SpanAction<char, TState> action)
{
scoped Span<char> buffer;

if (length <= 128)
buffer = stackalloc char[128];
else
buffer = new char[length];

action(buffer.Slice(0, length), state);
return buffer.Slice(0, length).ToString();
}
}
}

0 comments on commit c53a8c9

Please sign in to comment.