diff --git a/src/SourceGenerators/Uno.UI.SourceGenerators/XamlGenerator/XamlFileGenerator.cs b/src/SourceGenerators/Uno.UI.SourceGenerators/XamlGenerator/XamlFileGenerator.cs index 8f4252dd8506..61a352e5f373 100644 --- a/src/SourceGenerators/Uno.UI.SourceGenerators/XamlGenerator/XamlFileGenerator.cs +++ b/src/SourceGenerators/Uno.UI.SourceGenerators/XamlGenerator/XamlFileGenerator.cs @@ -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; @@ -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(); diff --git a/src/Uno.Foundation/Uno.Core.Extensions/Uno.Core.Extensions/StringExtensions.cs b/src/Uno.Foundation/Uno.Core.Extensions/Uno.Core.Extensions/StringExtensions.cs index 24d3957c918b..485a19afcb66 100644 --- a/src/Uno.Foundation/Uno.Core.Extensions/Uno.Core.Extensions/StringExtensions.cs +++ b/src/Uno.Foundation/Uno.Core.Extensions/Uno.Core.Extensions/StringExtensions.cs @@ -27,6 +27,8 @@ namespace Uno.Extensions { + public delegate void SpanAction(Span span, TArg arg); + internal static partial class StringExtensions { // https://www.meziantou.net/split-a-string-into-lines-without-allocation.htm @@ -153,5 +155,18 @@ public static string TrimEnd(this string source, string trimText) return source; } } + + public static string Create(int length, TState state, SpanAction action) + { + scoped Span 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(); + } } }