diff --git a/src/Microsoft.Android.Build.BaseTasks/Files.cs b/src/Microsoft.Android.Build.BaseTasks/Files.cs index adccff2f4fb..514448df475 100644 --- a/src/Microsoft.Android.Build.BaseTasks/Files.cs +++ b/src/Microsoft.Android.Build.BaseTasks/Files.cs @@ -595,21 +595,9 @@ public static string ToHexString (byte[] hash) public static string ToHexString (ReadOnlySpan hash) { - const int MaxStackCharLength = 128; - int charLength = hash.Length * 2; - Span chars = charLength <= MaxStackCharLength - ? stackalloc char[charLength] - : new char[charLength]; - for (int i = 0, j = 0; i < hash.Length; i += 1, j += 2) { - byte b = hash [i]; - chars [j] = GetHexValue (b / 16); - chars [j + 1] = GetHexValue (b % 16); - } - return ((ReadOnlySpan) chars).ToString (); + return HexUtilities.ToHexString (hash); } - static char GetHexValue (int i) => (char) (i < 10 ? i + 48 : i - 10 + 65); - public static void DeleteFile (string filename, object log) { try { diff --git a/src/Microsoft.Android.Build.BaseTasks/HexUtilities.cs b/src/Microsoft.Android.Build.BaseTasks/HexUtilities.cs new file mode 100644 index 00000000000..ea69f86dfeb --- /dev/null +++ b/src/Microsoft.Android.Build.BaseTasks/HexUtilities.cs @@ -0,0 +1,86 @@ +#nullable enable +using System; +using System.Diagnostics; +using System.IO; + +namespace Microsoft.Android.Build.Tasks +{ + /// + /// Allocation-free helpers for rendering bytes as hexadecimal. + /// + /// + /// This file is also linked into Microsoft.Android.Sdk.TrimmableTypeMap, which + /// deliberately does not reference Microsoft.Android.Build.BaseTasks (that would drag + /// Microsoft.Build.*, LibZipSharp, K4os.LZ4 and Mono.Unix into it). Only the copy compiled + /// into Microsoft.Android.Build.BaseTasks is public; the linked copy stays + /// internal, otherwise Xamarin.Android.Build.Tasks — which references both + /// assemblies — fails with CS0433. + /// +#if MICROSOFT_ANDROID_BUILD_BASETASKS + public +#endif + static class HexUtilities + { + /// + /// Convert a value in the 0..15 range to its hexadecimal digit. + /// + /// + /// Values outside 0..15 produce meaningless characters. + /// + public static char GetHexValue (int value, bool upperCase = true) + { + Debug.Assert ((uint) value < 16, $"Value must be in the 0..15 range, was {value}."); + + if (value < 10) + return (char) (value + '0'); + return (char) (value - 10 + (upperCase ? 'A' : 'a')); + } + + /// + /// Write into as exactly two + /// hexadecimal digits. + /// + /// + /// is shorter than two characters. + /// + public static void WriteHex (Span destination, byte value, bool upperCase = true) + { + if (destination.Length < 2) + throw new ArgumentException ("Destination must be at least 2 characters long.", nameof (destination)); + + destination [0] = GetHexValue (value >> 4, upperCase); + destination [1] = GetHexValue (value & 0x0f, upperCase); + } + + /// + /// Write to as exactly two hexadecimal + /// digits, without allocating. + /// + public static void WriteHex (TextWriter writer, byte value, bool upperCase = true) + { + if (writer == null) + throw new ArgumentNullException (nameof (writer)); + + writer.Write (GetHexValue (value >> 4, upperCase)); + writer.Write (GetHexValue (value & 0x0f, upperCase)); + } + + /// + /// Convert to a hexadecimal string, without allocating + /// intermediate strings. + /// + public static string ToHexString (ReadOnlySpan bytes, bool upperCase = true) + { + const int MaxStackCharLength = 128; + + int charLength = bytes.Length * 2; + Span chars = charLength <= MaxStackCharLength + ? stackalloc char [charLength] + : new char [charLength]; + for (int i = 0, j = 0; i < bytes.Length; i += 1, j += 2) { + WriteHex (chars.Slice (j, 2), bytes [i], upperCase); + } + return ((ReadOnlySpan) chars).ToString (); + } + } +} diff --git a/src/Microsoft.Android.Build.BaseTasks/Microsoft.Android.Build.BaseTasks.csproj b/src/Microsoft.Android.Build.BaseTasks/Microsoft.Android.Build.BaseTasks.csproj index 90aa46272ec..8f3f3b7be8e 100644 --- a/src/Microsoft.Android.Build.BaseTasks/Microsoft.Android.Build.BaseTasks.csproj +++ b/src/Microsoft.Android.Build.BaseTasks/Microsoft.Android.Build.BaseTasks.csproj @@ -16,6 +16,8 @@ latest enable nullable + + $(DefineConstants);MICROSOFT_ANDROID_BUILD_BASETASKS diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Microsoft.Android.Sdk.TrimmableTypeMap.csproj b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Microsoft.Android.Sdk.TrimmableTypeMap.csproj index 99399656317..2a08e9c073f 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Microsoft.Android.Sdk.TrimmableTypeMap.csproj +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Microsoft.Android.Sdk.TrimmableTypeMap.csproj @@ -19,6 +19,7 @@ + diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/ScannerHashingHelper.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/ScannerHashingHelper.cs index fe3aa9aaed0..4ac2731d306 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/ScannerHashingHelper.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/ScannerHashingHelper.cs @@ -2,6 +2,7 @@ using System.Buffers; using System.Buffers.Binary; using Java.Interop.Tools.JavaCallableWrappers; +using Microsoft.Android.Build.Tasks; namespace Microsoft.Android.Sdk.TrimmableTypeMap; @@ -18,7 +19,7 @@ internal static string ToLegacyCrc64 (string ns, string assemblyName) Crc64Helper.HashCore (rented, 0, bytesWritten, ref crc, ref length); Span hash = stackalloc byte [8]; BinaryPrimitives.WriteUInt64LittleEndian (hash, crc ^ length); - return ToHexString (hash); + return HexUtilities.ToHexString (hash, upperCase: false); } finally { ArrayPool.Shared.Return (rented); } @@ -37,7 +38,7 @@ internal static string ToCrc64 (string ns, string assemblyName) System.IO.Hashing.Crc64.Hash (utf8Buffer.Slice (0, bytesWritten), hash); ulong hashValue = BinaryPrimitives.ReadUInt64LittleEndian (hash); BinaryPrimitives.WriteUInt64LittleEndian (hash, hashValue ^ (ulong) bytesWritten); - return ToHexString (hash); + return HexUtilities.ToHexString (hash, upperCase: false); } static int GetNamespaceAssemblyUtf8ByteCount (string ns, string assemblyName) @@ -62,23 +63,4 @@ static unsafe int GetNamespaceAssemblyUtf8Bytes (string ns, string assemblyName, return bytesWritten; } - - static string ToHexString (ReadOnlySpan hash) - { - const int maxStackCharLength = 128; - int charLength = hash.Length * 2; - Span chars = charLength <= maxStackCharLength - ? stackalloc char [charLength] - : new char [charLength]; - - for (int i = 0, j = 0; i < hash.Length; i += 1, j += 2) { - byte b = hash [i]; - chars [j] = GetHexValue (b / 16); - chars [j + 1] = GetHexValue (b % 16); - } - - return ((ReadOnlySpan) chars).ToString (); - } - - static char GetHexValue (int value) => (char) (value < 10 ? value + '0' : value - 10 + 'a'); } diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrGenerator.cs b/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrGenerator.cs index dc3d7c37678..2ea987a2ef6 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrGenerator.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrGenerator.cs @@ -9,6 +9,7 @@ using System.Reflection; using System.Text; +using Microsoft.Android.Build.Tasks; using Xamarin.Android.Tools; namespace Xamarin.Android.Tasks.LLVMIR @@ -470,6 +471,23 @@ void WriteStructureType (GeneratorWriteContext context, StructureInstance si, ou void WriteType (GeneratorWriteContext context, Type type, object? value, out LlvmTypeInfo typeInfo, LlvmIrGlobalVariable? globalVariable = null) { + // Fast path: a basic scalar type (`byte`, `uint`, ...) can never be a structure instance, + // an array or a string blob, so skip the reflection-heavy probing below. Arrays and string + // blobs write one element at a time, so this runs millions of times for a typical + // application and `Type.IsPrimitive`/`Type.IsArray` dominate the generator otherwise. + if (basicTypeMap.ContainsKey (type)) { + string basicIRType = GetIRType (context, type, out ulong basicSize, out bool basicIsPointer); + typeInfo = new LlvmTypeInfo ( + isPointer: basicIsPointer, + isAggregate: false, + isStructure: false, + size: basicSize, + maxFieldAlignment: basicSize + ); + context.Output.Write (basicIRType); + return; + } + if (IsStructureInstance (type)) { if (value == null) { throw new ArgumentException ($"must not be null for structure instances ({type})", nameof (value)); @@ -947,10 +965,12 @@ void WriteCommaWithStride (ulong counter) void WriteByteTypeAndValue (byte v) { - WriteType (context, elementType, v, out _); - - context.Output.Write (' '); - WriteValue (context, elementType, v); + // This is by far the hottest path in the generator: a hello world MAUI app writes + // ~3.6 million bytes here. WriteType()/WriteValue() would box the byte and allocate a + // handful of strings per element, so write the (always identical) type and the two hex + // digits directly. + context.Output.Write ("i8 u0x"); + HexUtilities.WriteHex (context.Output, v, upperCase: false); } } diff --git a/tests/Microsoft.Android.Build.BaseTasks-Tests/HexUtilitiesTests.cs b/tests/Microsoft.Android.Build.BaseTasks-Tests/HexUtilitiesTests.cs new file mode 100644 index 00000000000..d9c99d7806b --- /dev/null +++ b/tests/Microsoft.Android.Build.BaseTasks-Tests/HexUtilitiesTests.cs @@ -0,0 +1,166 @@ +using System; +using System.IO; +using System.Linq; +using Microsoft.Android.Build.Tasks; +using NUnit.Framework; + +namespace Microsoft.Android.Build.BaseTasks.Tests +{ + [TestFixture] + public class HexUtilitiesTests + { + [Test] + public void GetHexValue_UpperCase () + { + const string expected = "0123456789ABCDEF"; + for (int i = 0; i < expected.Length; i++) { + Assert.AreEqual (expected [i], HexUtilities.GetHexValue (i), $"Mismatch at {i}."); + } + } + + [Test] + public void GetHexValue_LowerCase () + { + const string expected = "0123456789abcdef"; + for (int i = 0; i < expected.Length; i++) { + Assert.AreEqual (expected [i], HexUtilities.GetHexValue (i, upperCase: false), $"Mismatch at {i}."); + } + } + + [Test] + public void GetHexValue_DefaultsToUpperCase () + { + Assert.AreEqual ('A', HexUtilities.GetHexValue (10)); + } + + [Test] + public void WriteHex_Span_UpperCase () + { + Span chars = stackalloc char [2]; + HexUtilities.WriteHex (chars, 0xab); + Assert.AreEqual ("AB", chars.ToString ()); + } + + [Test] + public void WriteHex_Span_LowerCase () + { + Span chars = stackalloc char [2]; + HexUtilities.WriteHex (chars, 0xab, upperCase: false); + Assert.AreEqual ("ab", chars.ToString ()); + } + + [Test] + public void WriteHex_Span_WritesExactlyTwoChars () + { + Span chars = stackalloc char [4]; + chars.Fill ('_'); + HexUtilities.WriteHex (chars.Slice (1, 2), 0x0f); + Assert.AreEqual ("_0F_", chars.ToString ()); + } + + [Test] + public void WriteHex_Span_EveryByteValue () + { + Span chars = stackalloc char [2]; + for (int i = 0; i < 256; i++) { + HexUtilities.WriteHex (chars, (byte) i); + Assert.AreEqual (i.ToString ("X2"), chars.ToString (), $"Mismatch at {i}."); + } + } + + [Test] + public void WriteHex_TextWriter_UpperCase () + { + var writer = new StringWriter (); + HexUtilities.WriteHex (writer, 0x00); + HexUtilities.WriteHex (writer, 0x0f); + HexUtilities.WriteHex (writer, 0xff); + Assert.AreEqual ("000FFF", writer.ToString ()); + } + + [Test] + public void WriteHex_TextWriter_LowerCase () + { + var writer = new StringWriter (); + HexUtilities.WriteHex (writer, 0x00, upperCase: false); + HexUtilities.WriteHex (writer, 0x0f, upperCase: false); + HexUtilities.WriteHex (writer, 0xff, upperCase: false); + Assert.AreEqual ("000fff", writer.ToString ()); + } + + [Test] + public void WriteHex_TextWriter_EveryByteValue () + { + var writer = new StringWriter (); + for (int i = 0; i < 256; i++) { + HexUtilities.WriteHex (writer, (byte) i); + } + var expected = string.Concat (Enumerable.Range (0, 256).Select (i => i.ToString ("X2"))); + Assert.AreEqual (expected, writer.ToString ()); + } + + [Test] + public void WriteHex_TextWriter_NullThrows () + { + Assert.Throws (() => HexUtilities.WriteHex (writer: null, value: 0x00)); + } + + [TestCase (0)] + [TestCase (1)] + public void WriteHex_Span_TooShortThrows (int length) + { + var destination = new char [length]; + Assert.Throws (() => HexUtilities.WriteHex (destination.AsSpan (), 0xAB)); + } + + [Test] + public void WriteHex_Span_LongerThanTwoOnlyWritesTwo () + { + var destination = new char [] { 'x', 'x', 'x', 'x' }; + HexUtilities.WriteHex (destination.AsSpan (), 0xAB); + Assert.AreEqual ("ABxx", new string (destination)); + } + + [Test] + public void ToHexString_Empty () + { + Assert.AreEqual ("", HexUtilities.ToHexString (ReadOnlySpan.Empty)); + } + + [Test] + public void ToHexString_UpperCase () + { + var bytes = new byte [] { 0x00, 0x01, 0x0f, 0x10, 0x7f, 0x80, 0xab, 0xff }; + Assert.AreEqual ("00010F107F80ABFF", HexUtilities.ToHexString (bytes)); + } + + [Test] + public void ToHexString_LowerCase () + { + var bytes = new byte [] { 0x00, 0x01, 0x0f, 0x10, 0x7f, 0x80, 0xab, 0xff }; + Assert.AreEqual ("00010f107f80abff", HexUtilities.ToHexString (bytes, upperCase: false)); + } + + [Test] + public void ToHexString_EveryByteValue () + { + var bytes = Enumerable.Range (0, 256).Select (i => (byte) i).ToArray (); + var expected = BitConverter.ToString (bytes).Replace ("-", ""); + Assert.AreEqual (expected, HexUtilities.ToHexString (bytes)); + Assert.AreEqual (expected.ToLowerInvariant (), HexUtilities.ToHexString (bytes, upperCase: false)); + } + + // The implementation stackallocs up to 128 chars (64 bytes) and heap allocates beyond that + [TestCase (63)] + [TestCase (64)] + [TestCase (65)] + [TestCase (1024)] + public void ToHexString_CrossesStackallocThreshold (int length) + { + var bytes = new byte [length]; + new Random (42).NextBytes (bytes); + var expected = BitConverter.ToString (bytes).Replace ("-", ""); + Assert.AreEqual (expected, HexUtilities.ToHexString (bytes)); + } + } +}