From b79adc5bcba186884438b3d328792afe5c124a15 Mon Sep 17 00:00:00 2001 From: Trayan Zapryanov Date: Sat, 1 Apr 2023 02:59:59 +0300 Subject: [PATCH] Improve XmlSerializationWriter.WriteTypedPrimitive (#76436) * Introduce TryFormats for almost all primitive types * Use primitive char buffer in XmlSerializationWriter * Fix char cast * Add tests for different types * Add byte type * Address feedback * Fix tests * remove using * Increase duration char buffer size as it is not enough for TimeSpan.Max/Min * Address feedback * Added assert if we cannot format primitive value to the suppiled buffer * Lazy create primitives buffer * Address new feadback * Resolve feedback * Optimize float and double TryFormat * Replace ArrayPool renting with Interlocked. Fix Debug.Assert * Do not expect concurrency when using primitives buffer --------- Co-authored-by: Traian Zaprianov --- .../src/System/Xml/Schema/XsdDateTime.cs | 18 +- .../src/System/Xml/Schema/XsdDuration.cs | 19 +- .../Serialization/XmlSerializationWriter.cs | 78 +++++--- .../Xml/Serialization/Xmlcustomformatter.cs | 11 ++ .../src/System/Xml/XmlConvert.cs | 168 ++++++++++++++++++ .../XmlSerializerTests.RuntimeOnly.cs | 45 +++++ 6 files changed, 308 insertions(+), 31 deletions(-) diff --git a/src/libraries/System.Private.Xml/src/System/Xml/Schema/XsdDateTime.cs b/src/libraries/System.Private.Xml/src/System/Xml/Schema/XsdDateTime.cs index f82937036093f..4f774a4f98a31 100644 --- a/src/libraries/System.Private.Xml/src/System/Xml/Schema/XsdDateTime.cs +++ b/src/libraries/System.Private.Xml/src/System/Xml/Schema/XsdDateTime.cs @@ -132,6 +132,8 @@ private enum XsdDateTimeKind private static ReadOnlySpan DaysToMonth366 => new int[] { 0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366}; + private const int CharStackBufferSize = 64; + /// /// Constructs an XsdDateTime from a string using specific format. /// @@ -495,7 +497,17 @@ public static implicit operator DateTimeOffset(XsdDateTime xdt) /// public override string ToString() { - var vsb = new ValueStringBuilder(stackalloc char[64]); + Span destination = stackalloc char[CharStackBufferSize]; + bool success = TryFormat(destination, out int charsWritten); + Debug.Assert(success); + + return destination.Slice(0, charsWritten).ToString(); + } + + public bool TryFormat(Span destination, out int charsWritten) + { + var vsb = new ValueStringBuilder(destination); + switch (InternalTypeCode) { case DateTimeTypeCode.DateTime: @@ -534,7 +546,9 @@ public override string ToString() break; } PrintZone(ref vsb); - return vsb.ToString(); + + charsWritten = vsb.Length; + return destination.Length >= vsb.Length; } // Serialize year, month and day diff --git a/src/libraries/System.Private.Xml/src/System/Xml/Schema/XsdDuration.cs b/src/libraries/System.Private.Xml/src/System/Xml/Schema/XsdDuration.cs index 817e01734194e..f4612b2c541f4 100644 --- a/src/libraries/System.Private.Xml/src/System/Xml/Schema/XsdDuration.cs +++ b/src/libraries/System.Private.Xml/src/System/Xml/Schema/XsdDuration.cs @@ -23,6 +23,7 @@ internal struct XsdDuration private uint _nanoseconds; // High bit is used to indicate whether duration is negative private const uint NegativeBit = 0x80000000; + private const int CharStackBufferSize = 32; private enum Parts { @@ -341,7 +342,16 @@ public override string ToString() /// internal string ToString(DurationType durationType) { - var vsb = new ValueStringBuilder(stackalloc char[20]); + Span destination = stackalloc char[CharStackBufferSize]; + bool success = TryFormat(destination, out int charsWritten, durationType); + Debug.Assert(success); + + return destination.Slice(0, charsWritten).ToString(); + } + + public bool TryFormat(Span destination, out int charsWritten, DurationType durationType = DurationType.Duration) + { + var vsb = new ValueStringBuilder(destination); int nanoseconds, digit, zeroIdx, len; if (IsNegative) @@ -411,7 +421,9 @@ internal string ToString(DurationType durationType) } vsb.EnsureCapacity(zeroIdx + 1); - vsb.Append(tmpSpan.Slice(0, zeroIdx - len + 1)); + int nanoSpanLength = zeroIdx - len + 1; + bool successCopy = tmpSpan[..nanoSpanLength].TryCopyTo(vsb.AppendSpan(nanoSpanLength)); + Debug.Assert(successCopy); } vsb.Append('S'); } @@ -428,7 +440,8 @@ internal string ToString(DurationType durationType) vsb.Append("0M"); } - return vsb.ToString(); + charsWritten = vsb.Length; + return destination.Length >= vsb.Length; } internal static Exception? TryParse(string s, out XsdDuration result) diff --git a/src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializationWriter.cs b/src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializationWriter.cs index cfa361fa3f2a0..98ec06f03479a 100644 --- a/src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializationWriter.cs +++ b/src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializationWriter.cs @@ -38,6 +38,9 @@ public abstract class XmlSerializationWriter : XmlSerializationGeneratedCode private bool _soap12; private bool _escapeName = true; + //char buffer for serializing primitive values + private readonly char[] _primitivesBuffer = new char[64]; + // this method must be called before any generated serialization methods are called internal void Init(XmlWriter w, XmlSerializerNamespaces? namespaces, string? encodingStyle, string? idBase) { @@ -120,6 +123,11 @@ protected static string FromDateTime(DateTime value) return XmlCustomFormatter.FromDateTime(value); } + internal static bool TryFormatDateTime(DateTime value, Span destination, out int charsWritten) + { + return XmlCustomFormatter.TryFormatDateTime(value, destination, out charsWritten); + } + protected static string FromDate(DateTime value) { return XmlCustomFormatter.FromDate(value); @@ -246,13 +254,15 @@ private XmlQualifiedName GetPrimitiveTypeName(Type type) [RequiresUnreferencedCode(XmlSerializer.TrimSerializationWarning)] protected void WriteTypedPrimitive(string? name, string? ns, object o, bool xsiType) { - string? value; + string? value = null; string type; string typeNs = XmlSchema.Namespace; bool writeRaw = true; bool writeDirect = false; Type t = o.GetType(); bool wroteStartElement = false; + bool? tryFormatResult = null; + int charsWritten = -1; switch (Type.GetTypeCode(t)) { @@ -262,60 +272,60 @@ protected void WriteTypedPrimitive(string? name, string? ns, object o, bool xsiT writeRaw = false; break; case TypeCode.Int32: - value = XmlConvert.ToString((int)o); + tryFormatResult = XmlConvert.TryFormat((int)o, _primitivesBuffer, out charsWritten); type = "int"; break; case TypeCode.Boolean: - value = XmlConvert.ToString((bool)o); + tryFormatResult = XmlConvert.TryFormat((bool)o, _primitivesBuffer, out charsWritten); type = "boolean"; break; case TypeCode.Int16: - value = XmlConvert.ToString((short)o); + tryFormatResult = XmlConvert.TryFormat((short)o, _primitivesBuffer, out charsWritten); type = "short"; break; case TypeCode.Int64: - value = XmlConvert.ToString((long)o); + tryFormatResult = XmlConvert.TryFormat((long)o, _primitivesBuffer, out charsWritten); type = "long"; break; case TypeCode.Single: - value = XmlConvert.ToString((float)o); + tryFormatResult = XmlConvert.TryFormat((float)o, _primitivesBuffer, out charsWritten); type = "float"; break; case TypeCode.Double: - value = XmlConvert.ToString((double)o); + tryFormatResult = XmlConvert.TryFormat((double)o, _primitivesBuffer, out charsWritten); type = "double"; break; case TypeCode.Decimal: - value = XmlConvert.ToString((decimal)o); + tryFormatResult = XmlConvert.TryFormat((decimal)o, _primitivesBuffer, out charsWritten); type = "decimal"; break; case TypeCode.DateTime: - value = FromDateTime((DateTime)o); + tryFormatResult = TryFormatDateTime((DateTime)o, _primitivesBuffer, out charsWritten); type = "dateTime"; break; case TypeCode.Char: - value = FromChar((char)o); + tryFormatResult = XmlConvert.TryFormat((ushort)(char)o, _primitivesBuffer, out charsWritten); type = "char"; typeNs = UrtTypes.Namespace; break; case TypeCode.Byte: - value = XmlConvert.ToString((byte)o); + tryFormatResult = XmlConvert.TryFormat((byte)o, _primitivesBuffer, out charsWritten); type = "unsignedByte"; break; case TypeCode.SByte: - value = XmlConvert.ToString((sbyte)o); + tryFormatResult = XmlConvert.TryFormat((sbyte)o, _primitivesBuffer, out charsWritten); type = "byte"; break; case TypeCode.UInt16: - value = XmlConvert.ToString((ushort)o); + tryFormatResult = XmlConvert.TryFormat((ushort)o, _primitivesBuffer, out charsWritten); type = "unsignedShort"; break; case TypeCode.UInt32: - value = XmlConvert.ToString((uint)o); + tryFormatResult = XmlConvert.TryFormat((uint)o, _primitivesBuffer, out charsWritten); type = "unsignedInt"; break; case TypeCode.UInt64: - value = XmlConvert.ToString((ulong)o); + tryFormatResult = XmlConvert.TryFormat((ulong)o, _primitivesBuffer, out charsWritten); type = "unsignedLong"; break; @@ -340,19 +350,19 @@ protected void WriteTypedPrimitive(string? name, string? ns, object o, bool xsiT } else if (t == typeof(Guid)) { - value = XmlConvert.ToString((Guid)o); + tryFormatResult = XmlConvert.TryFormat((Guid)o, _primitivesBuffer, out charsWritten); type = "guid"; typeNs = UrtTypes.Namespace; } else if (t == typeof(TimeSpan)) { - value = XmlConvert.ToString((TimeSpan)o); + tryFormatResult = XmlConvert.TryFormat((TimeSpan)o, _primitivesBuffer, out charsWritten); type = "TimeSpan"; typeNs = UrtTypes.Namespace; } else if (t == typeof(DateTimeOffset)) { - value = XmlConvert.ToString((DateTimeOffset)o); + tryFormatResult = XmlConvert.TryFormat((DateTimeOffset)o, _primitivesBuffer, out charsWritten); type = "dateTimeOffset"; typeNs = UrtTypes.Namespace; } @@ -374,7 +384,10 @@ protected void WriteTypedPrimitive(string? name, string? ns, object o, bool xsiT return; } else + { throw CreateUnknownTypeException(t); + } + break; } if (!wroteStartElement) @@ -387,21 +400,34 @@ protected void WriteTypedPrimitive(string? name, string? ns, object o, bool xsiT if (xsiType) WriteXsiType(type, typeNs); - if (value == null) - { - _w.WriteAttributeString("nil", XmlSchema.InstanceNamespace, "true"); - } - else if (writeDirect) + if (writeDirect) { // only one type currently writes directly to XML stream XmlCustomFormatter.WriteArrayBase64(_w, (byte[])o, 0, ((byte[])o).Length); } - else if (writeRaw) + else if (tryFormatResult != null) { - _w.WriteRaw(value); + Debug.Assert(tryFormatResult.Value, "Something goes wrong with formatting primitives to the buffer."); +#if DEBUG + const string escapeChars = "<>\"'&"; + ReadOnlySpan span = _primitivesBuffer; + Debug.Assert(span.Slice(0, charsWritten).IndexOfAny(escapeChars) == -1, "Primitive value contains illegal xml char."); +#endif + //all the primitive types except string and XmlQualifiedName writes to the buffer + _w.WriteRaw(_primitivesBuffer, 0, charsWritten); } else - _w.WriteString(value); + { + if (value == null) + _w.WriteAttributeString("nil", XmlSchema.InstanceNamespace, "true"); + else if (writeRaw) + { + _w.WriteRaw(value); + } + else + _w.WriteString(value); + } + _w.WriteEndElement(); } diff --git a/src/libraries/System.Private.Xml/src/System/Xml/Serialization/Xmlcustomformatter.cs b/src/libraries/System.Private.Xml/src/System/Xml/Serialization/Xmlcustomformatter.cs index 2338d8d46e949..77a2bb1863992 100644 --- a/src/libraries/System.Private.Xml/src/System/Xml/Serialization/Xmlcustomformatter.cs +++ b/src/libraries/System.Private.Xml/src/System/Xml/Serialization/Xmlcustomformatter.cs @@ -105,6 +105,17 @@ internal static string FromDateTime(DateTime value) } } + internal static bool TryFormatDateTime(DateTime value, Span destination, out int charsWritten) + { + if (Mode == DateTimeSerializationSection.DateTimeSerializationMode.Local) + { + return XmlConvert.TryFormat(value, "yyyy-MM-ddTHH:mm:ss.fffffffzzzzzz", destination, out charsWritten); + } + + // for mode DateTimeSerializationMode.Roundtrip and DateTimeSerializationMode.Default + return XmlConvert.TryFormat(value, XmlDateTimeSerializationMode.RoundtripKind, destination, out charsWritten); + } + internal static string FromChar(char value) { return XmlConvert.ToString((ushort)value); diff --git a/src/libraries/System.Private.Xml/src/System/Xml/XmlConvert.cs b/src/libraries/System.Private.Xml/src/System/Xml/XmlConvert.cs index c465eb96094f0..07b6bb93691a7 100644 --- a/src/libraries/System.Private.Xml/src/System/Xml/XmlConvert.cs +++ b/src/libraries/System.Private.Xml/src/System/Xml/XmlConvert.cs @@ -1645,5 +1645,173 @@ internal static Exception CreateInvalidNameCharException(string name, int index, { return CreateException(index == 0 ? SR.Xml_BadStartNameChar : SR.Xml_BadNameChar, XmlException.BuildCharExceptionArgs(name, index), exceptionType, 0, index + 1); } + + internal static bool TryFormat(bool value, Span destination, out int charsWritten) + { + string valueAsString = value ? "true" : "false"; + + charsWritten = valueAsString.Length; + return valueAsString.TryCopyTo(destination); + } + + internal static bool TryFormat(char value, Span destination, out int charsWritten) + { + charsWritten = 1; + if (destination.Length < 1) return false; + + destination[0] = value; + return true; + } + + internal static bool TryFormat(decimal value, Span destination, out int charsWritten) + { + return value.TryFormat(destination, out charsWritten, default, NumberFormatInfo.InvariantInfo); + } + + internal static bool TryFormat(sbyte value, Span destination, out int charsWritten) + { + return value.TryFormat(destination, out charsWritten, default, CultureInfo.InvariantCulture); + } + + internal static bool TryFormat(short value, Span destination, out int charsWritten) + { + return value.TryFormat(destination, out charsWritten, default, CultureInfo.InvariantCulture); + } + + internal static bool TryFormat(int value, Span destination, out int charsWritten) + { + return value.TryFormat(destination, out charsWritten, default, CultureInfo.InvariantCulture); + } + + internal static bool TryFormat(long value, Span destination, out int charsWritten) + { + return value.TryFormat(destination, out charsWritten, default, CultureInfo.InvariantCulture); + } + + internal static bool TryFormat(byte value, Span destination, out int charsWritten) + { + return value.TryFormat(destination, out charsWritten, default, CultureInfo.InvariantCulture); + } + + internal static bool TryFormat(ushort value, Span destination, out int charsWritten) + { + return value.TryFormat(destination, out charsWritten, default, CultureInfo.InvariantCulture); + } + + internal static bool TryFormat(uint value, Span destination, out int charsWritten) + { + return value.TryFormat(destination, out charsWritten, default, CultureInfo.InvariantCulture); + } + + internal static bool TryFormat(ulong value, Span destination, out int charsWritten) + { + return value.TryFormat(destination, out charsWritten, default, CultureInfo.InvariantCulture); + } + + internal static bool TryFormat(float value, Span destination, out int charsWritten) + { + ReadOnlySpan valueSpan; + + if (!float.IsFinite(value)) + { + if (float.IsNaN(value)) + valueSpan = "NaN"; + else + valueSpan = float.IsNegative(value) ? "-INF" : "INF"; + } + else if (IsNegativeZero((double)value)) + { + valueSpan = "-0"; + } + else + { + return value.TryFormat(destination, out charsWritten, "R", NumberFormatInfo.InvariantInfo); + } + + charsWritten = valueSpan.Length; + return valueSpan.TryCopyTo(destination); + } + + internal static bool TryFormat(double value, Span destination, out int charsWritten) + { + ReadOnlySpan valueSpan; + + if (!double.IsFinite(value)) + { + if (double.IsNaN(value)) + valueSpan = "NaN"; + else + valueSpan = double.IsNegative(value) ? "-INF" : "INF"; + } + else if (IsNegativeZero(value)) + { + valueSpan = "-0"; + } + else + { + return value.TryFormat(destination, out charsWritten, "R", NumberFormatInfo.InvariantInfo); + } + + charsWritten = valueSpan.Length; + return valueSpan.TryCopyTo(destination); + } + + internal static bool TryFormat(TimeSpan value, Span destination, out int charsWritten) + { + return new XsdDuration(value).TryFormat(destination, out charsWritten); + } + + internal static bool TryFormat(DateTime value, [StringSyntax(StringSyntaxAttribute.DateTimeFormat)] string format, Span destination, out int charsWritten) + { + return value.TryFormat(destination, out charsWritten, format, DateTimeFormatInfo.InvariantInfo); + } + + internal static bool TryFormat(DateTime value, Span destination, out int charsWritten) + { + return TryFormat(value, XmlDateTimeSerializationMode.RoundtripKind, destination, out charsWritten); + } + + internal static bool TryFormat(DateTime value, XmlDateTimeSerializationMode dateTimeOption, Span destination, out int charsWritten) + { + switch (dateTimeOption) + { + case XmlDateTimeSerializationMode.Local: + value = SwitchToLocalTime(value); + break; + + case XmlDateTimeSerializationMode.Utc: + value = SwitchToUtcTime(value); + break; + + case XmlDateTimeSerializationMode.Unspecified: + value = new DateTime(value.Ticks, DateTimeKind.Unspecified); + break; + + case XmlDateTimeSerializationMode.RoundtripKind: + break; + + default: + throw new ArgumentException(SR.Format(SR.Sch_InvalidDateTimeOption, dateTimeOption, nameof(dateTimeOption))); + } + + XsdDateTime xsdDateTime = new XsdDateTime(value, XsdDateTimeFlags.DateTime); + return xsdDateTime.TryFormat(destination, out charsWritten); + } + + internal static bool TryFormat(DateTimeOffset value, Span destination, out int charsWritten) + { + XsdDateTime xsdDateTime = new XsdDateTime(value); + return xsdDateTime.TryFormat(destination, out charsWritten); + } + + internal static bool TryFormat(DateTimeOffset value, [StringSyntax(StringSyntaxAttribute.DateTimeFormat)] string format, Span destination, out int charsWritten) + { + return value.TryFormat(destination, out charsWritten, format, DateTimeFormatInfo.InvariantInfo); + } + + internal static bool TryFormat(Guid value, Span destination, out int charsWritten) + { + return value.TryFormat(destination, out charsWritten); + } } } diff --git a/src/libraries/System.Private.Xml/tests/XmlSerializer/XmlSerializerTests.RuntimeOnly.cs b/src/libraries/System.Private.Xml/tests/XmlSerializer/XmlSerializerTests.RuntimeOnly.cs index 29a7880968a5f..9fa615c142d72 100644 --- a/src/libraries/System.Private.Xml/tests/XmlSerializer/XmlSerializerTests.RuntimeOnly.cs +++ b/src/libraries/System.Private.Xml/tests/XmlSerializer/XmlSerializerTests.RuntimeOnly.cs @@ -411,6 +411,51 @@ public static void Xml_CollectionRoot() Assert.True((int)y[1] == 45); } + [Fact] + public static void Xml_CollectionRoot_MorePrimitiveTypes() + { + DateTime now = new DateTime(2022, 9, 30, 9, 4, 15, DateTimeKind.Utc); + DateTimeOffset dtoNow = now.AddDays(1); + TimeSpan ts = new TimeSpan(1, 2, 3, 4, 5); + MyCollection x = new MyCollection(123.45m, now, ts, dtoNow, (short)55, 2345324L, (sbyte)11, (ushort)34, (uint)4564, (ulong)456734767, + new byte[] { 33, 44, 55 }, (byte)67); + MyCollection y = SerializeAndDeserialize(x, +@" + + 123.45 + 2022-09-30T09:04:15Z + P1DT2H3M4.005S + 2022-10-01T09:04:15Z + 55 + 2345324 + 11 + 34 + 4564 + 456734767 + ISw3 + 67 +"); + + Assert.NotNull(y); + Assert.True(y.Count == 12); + Assert.True((decimal)y[0] == 123.45m); + Assert.True((DateTime)y[1] == now); + Assert.True((TimeSpan)y[2] == ts); + Assert.True((DateTimeOffset)y[3] == dtoNow); + Assert.True((short)y[4] == 55); + Assert.True((long)y[5] == 2345324L); + Assert.True((sbyte)y[6] == 11); + Assert.True((ushort)y[7] == 34); + Assert.True((uint)y[8] == 4564); + Assert.True((ulong)y[9] == 456734767); + Assert.True(y[10] is byte[]); + Assert.Equal(3, ((byte[])y[10]).Length); + Assert.Equal(33, ((byte[])y[10])[0]); + Assert.Equal(44, ((byte[])y[10])[1]); + Assert.Equal(55, ((byte[])y[10])[2]); + Assert.True((byte)y[11] == 67); + } + [Fact] public static void Xml_EnumerableRoot() {