diff --git a/.azurepipelines/test.yml b/.azurepipelines/test.yml index 089b20227d..9a285c0951 100644 --- a/.azurepipelines/test.yml +++ b/.azurepipelines/test.yml @@ -22,10 +22,11 @@ jobs: inputs: targetType: filePath filePath: ./.azurepipelines/get-matrix.ps1 - arguments: -FileName azure-pipelines.yml -AgentTable ${{ parameters.agents }} + arguments: -FileName *.Tests.csproj -AgentTable ${{ parameters.agents }} - job: testall${{ parameters.jobnamesuffix }} displayName: Tests (${{ parameters.framework }}) dependsOn: testprep${{ parameters.jobnamesuffix }} + timeoutInMinutes: 120 strategy: matrix: $[dependencies.testprep${{ parameters.jobnamesuffix }}.outputs['testmatrix.jobMatrix'] ] variables: @@ -62,12 +63,12 @@ jobs: displayName: Restore ${{ parameters.configuration }} inputs: command: restore - projects: '**/*.Tests.csproj' + projects: $(file) arguments: '${{ variables.DotCliCommandline }} --configuration ${{ parameters.configuration }}' - task: DotNetCoreCLI@2 displayName: Test ${{ parameters.configuration }} - timeoutInMinutes: 60 + timeoutInMinutes: 30 inputs: command: test - projects: '**/*.Tests.csproj' + projects: $(file) arguments: '--no-restore ${{ variables.DotCliCommandline }} --configuration ${{ parameters.configuration }}' diff --git a/.editorconfig b/.editorconfig index 12e1f79235..1db323f25b 100644 --- a/.editorconfig +++ b/.editorconfig @@ -336,6 +336,12 @@ dotnet_diagnostic.IDE1005.severity = # CA1305: Specify IFormatProvider dotnet_diagnostic.CA1305.severity = warning +# CA1819: Properties should not return arrays +dotnet_diagnostic.CA1819.severity = silent + +# CA1721: The property name is confusing given the existence of another method with the same name. +dotnet_diagnostic.CA1721.severity = silent + # exclude generated code [**/Generated/*.cs] generated_code = true diff --git a/Stack/Opc.Ua.Core/Stack/Bindings/ArraySegmentStream.cs b/Stack/Opc.Ua.Core/Stack/Bindings/ArraySegmentStream.cs index 93d61973ef..8c4a53147d 100644 --- a/Stack/Opc.Ua.Core/Stack/Bindings/ArraySegmentStream.cs +++ b/Stack/Opc.Ua.Core/Stack/Bindings/ArraySegmentStream.cs @@ -10,7 +10,12 @@ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ +#if NETSTANDARD2_1_OR_GREATER || NET5_0_OR_GREATER +#define STREAM_WITH_SPAN_SUPPORT +#endif + using System; +using System.Buffers; using System.IO; using System.Runtime.CompilerServices; @@ -63,6 +68,37 @@ namespace Opc.Ua.Bindings SetCurrentBuffer(0); } + + /// + /// Creates a writeable stream that creates buffers as necessary. + /// + /// The buffer manager. + public ArraySegmentStream(BufferManager bufferManager) + : this(bufferManager, bufferManager.MaxSuggestedBufferSize, 0, bufferManager.MaxSuggestedBufferSize) + { + } + #endregion + + #region IDisposable + /// + protected override void Dispose(bool disposing) + { + if (disposing) + { + if (m_buffers != null && m_bufferManager != null) + { + for (int ii = 0; ii < m_buffers.Count; ii++) + { + m_bufferManager.ReturnBuffer(m_buffers[ii].Array, "ArraySegmentStream.Dispose"); + } + m_buffers.Clear(); + m_buffers = null; + } + m_bufferManager = null; + } + + base.Dispose(disposing); + } #endregion #region Public Methods @@ -81,44 +117,77 @@ namespace Opc.Ua.Bindings buffers.Add(new ArraySegment(m_buffers[ii].Array, m_buffers[ii].Offset, GetBufferCount(ii))); } - m_buffers.Clear(); + ClearBuffers(); return buffers; } + + /// + /// Returns sequence of the buffers stored in the stream. + /// + /// + /// The buffers ownership is transferred to the sequence, + /// the stream can be disposed. + /// The new owner is responisble to dispose the sequence after use. + /// + public BufferSequence GetSequence(string owner) + { + if (m_buffers.Count == 0) + { + return new BufferSequence(m_bufferManager, owner, null, ReadOnlySequence.Empty); + } + + int endIndex = GetBufferCount(0); + var firstSegment = new BufferSegment(m_buffers[0].Array, m_buffers[0].Offset, endIndex); + m_bufferManager.TransferBuffer(m_buffers[0].Array, owner); + BufferSegment nextSegment = firstSegment; + for (int ii = 1; ii < m_buffers.Count; ii++) + { + m_bufferManager.TransferBuffer(m_buffers[ii].Array, owner); + endIndex = GetBufferCount(ii); + nextSegment = nextSegment.Append(m_buffers[ii].Array, m_buffers[ii].Offset, endIndex); + } + + var sequence = new ReadOnlySequence(firstSegment, 0, nextSegment, endIndex); + + ClearBuffers(); + + return new BufferSequence(m_bufferManager, owner, firstSegment, sequence); + } #endregion #region Overridden Methods - /// + /// public override bool CanRead { - get { return true; } + get { return m_buffers != null; } } - /// + /// public override bool CanSeek { - get { return true; } + get { return m_buffers != null; } } - /// + /// public override bool CanWrite { - get { return true; } + get { return m_buffers != null; } } - /// + /// public override void Flush() { // nothing to do. } - /// + /// public override long Length { get { return GetAbsoluteLength(); } } - /// + /// public override long Position { get @@ -132,7 +201,7 @@ namespace Opc.Ua.Bindings } } - /// + /// public override int ReadByte() { do @@ -148,7 +217,11 @@ namespace Opc.Ua.Bindings // copy the bytes requested. if (bytesLeft > 0) { +#if STREAM_WITH_SPAN_SUPPORT + return m_currentBuffer[m_currentPosition++]; +#else return m_currentBuffer.Array[m_currentBuffer.Offset + m_currentPosition++]; +#endif } // move to next buffer. @@ -157,7 +230,54 @@ namespace Opc.Ua.Bindings } while (true); } - /// +#if STREAM_WITH_SPAN_SUPPORT + /// + /// Helper to benchmark the performance of the stream. + /// + internal int ReadMemoryStream(Span buffer) => base.Read(buffer); + + /// + public override int Read(Span buffer) + { + int count = buffer.Length; + int offset = 0; + int bytesRead = 0; + + while (count > 0) + { + // check for end of stream. + if (m_currentBuffer.Array == null) + { + return bytesRead; + } + + int bytesLeft = GetBufferCount(m_bufferIndex) - m_currentPosition; + + // copy the bytes requested. + if (bytesLeft > count) + { + m_currentBuffer.AsSpan(m_currentPosition, count).CopyTo(buffer.Slice(offset)); + bytesRead += count; + m_currentPosition += count; + return bytesRead; + } + + // copy the bytes available and move to next buffer. + m_currentBuffer.AsSpan(m_currentPosition, bytesLeft).CopyTo(buffer.Slice(offset)); + bytesRead += bytesLeft; + + offset += bytesLeft; + count -= bytesLeft; + + // move to next buffer. + SetCurrentBuffer(m_bufferIndex + 1); + } + + return bytesRead; + } +#endif + + /// public override int Read(byte[] buffer, int offset, int count) { int bytesRead = 0; @@ -195,7 +315,7 @@ namespace Opc.Ua.Bindings return bytesRead; } - /// + /// public override long Seek(long offset, SeekOrigin origin) { switch (origin) @@ -223,6 +343,13 @@ namespace Opc.Ua.Bindings throw new IOException("Cannot seek beyond the beginning of the stream."); } + // special case + if (offset == 0) + { + SetCurrentBuffer(0); + return 0; + } + int position = (int)offset; if (position >= GetAbsolutePosition()) @@ -247,13 +374,13 @@ namespace Opc.Ua.Bindings throw new IOException("Cannot seek beyond the end of the stream."); } - /// + /// public override void SetLength(long value) { throw new NotSupportedException(); } - /// + /// public override void WriteByte(byte value) { do @@ -266,16 +393,12 @@ namespace Opc.Ua.Bindings // copy the byte requested. if (bytesLeft >= 1) { +#if STREAM_WITH_SPAN_SUPPORT + m_currentBuffer[m_currentPosition] = value; +#else m_currentBuffer.Array[m_currentBuffer.Offset + m_currentPosition] = value; - m_currentPosition++; - - if (m_bufferIndex == m_buffers.Count - 1) - { - if (m_endOfLastBuffer < m_currentPosition) - { - m_endOfLastBuffer = m_currentPosition; - } - } +#endif + UpdateCurrentPosition(1); return; } @@ -286,7 +409,47 @@ namespace Opc.Ua.Bindings } while (true); } - /// +#if STREAM_WITH_SPAN_SUPPORT + /// + /// Helper to benchmark the performance of the stream. + /// + internal void WriteMemoryStream(ReadOnlySpan buffer) => base.Write(buffer); + + /// + public override void Write(ReadOnlySpan buffer) + { + int count = buffer.Length; + int offset = 0; + while (count > 0) + { + // check for end of stream. + CheckEndOfStream(); + + int bytesLeft = m_currentBuffer.Count - m_currentPosition; + + // copy the bytes requested. + if (bytesLeft >= count) + { + buffer.Slice(offset, count).CopyTo(m_currentBuffer.AsSpan(m_currentPosition)); + + UpdateCurrentPosition(count); + + return; + } + + // copy the bytes available and move to next buffer. + buffer.Slice(offset, bytesLeft).CopyTo(m_currentBuffer.AsSpan(m_currentPosition)); + + offset += bytesLeft; + count -= bytesLeft; + + // move to next buffer. + SetCurrentBuffer(m_bufferIndex + 1); + } + } +#endif + + /// public override void Write(byte[] buffer, int offset, int count) { while (count > 0) @@ -301,15 +464,7 @@ namespace Opc.Ua.Bindings { Array.Copy(buffer, offset, m_currentBuffer.Array, m_currentPosition + m_currentBuffer.Offset, count); - m_currentPosition += count; - - if (m_bufferIndex == m_buffers.Count - 1) - { - if (m_endOfLastBuffer < m_currentPosition) - { - m_endOfLastBuffer = m_currentPosition; - } - } + UpdateCurrentPosition(count); return; } @@ -325,9 +480,14 @@ namespace Opc.Ua.Bindings } } - /// + /// public override byte[] ToArray() { + if (m_buffers == null) + { + throw new ObjectDisposedException(nameof(ArraySegmentStream)); + } + int absoluteLength = GetAbsoluteLength(); if (absoluteLength == 0) { @@ -353,6 +513,23 @@ namespace Opc.Ua.Bindings #endregion #region Private Methods + /// + /// Update the current buffer count. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void UpdateCurrentPosition(int count) + { + m_currentPosition += count; + + if (m_bufferIndex == m_buffers.Count - 1) + { + if (m_endOfLastBuffer < m_currentPosition) + { + m_endOfLastBuffer = m_currentPosition; + } + } + } + /// /// Sets the current buffer. /// @@ -446,6 +623,17 @@ namespace Opc.Ua.Bindings SetCurrentBuffer(m_buffers.Count - 1); } } + + /// + /// Clears the buffers and resets the state variables. + /// + private void ClearBuffers() + { + m_buffers.Clear(); + m_bufferIndex = 0; + m_endOfLastBuffer = 0; + SetCurrentBuffer(0); + } #endregion #region Private Fields diff --git a/Stack/Opc.Ua.Core/Stack/Bindings/BufferManager.cs b/Stack/Opc.Ua.Core/Stack/Bindings/BufferManager.cs index 1e3a50502f..730b2b708d 100644 --- a/Stack/Opc.Ua.Core/Stack/Bindings/BufferManager.cs +++ b/Stack/Opc.Ua.Core/Stack/Bindings/BufferManager.cs @@ -125,6 +125,7 @@ namespace Opc.Ua.Bindings ? ArrayPool.Shared : ArrayPool.Create(maxBufferSize + kCookieLength, 4); m_maxBufferSize = maxBufferSize; + m_maxSuggestedBufferSize = DetermineSuggestedBufferSize(maxBufferSize); } #endregion @@ -336,11 +337,54 @@ namespace Opc.Ua.Bindings #endif m_arrayPool.Return(buffer); } + + /// + /// Returns the suggested max rent size for data in the buffers. + /// + /// The max buffer size configured. + private int DetermineSuggestedBufferSize(int maxBufferSize) + { + int bufferArrayPoolSize = RoundUpToPowerOfTwo(maxBufferSize); + int maxDataRentSize = RoundUpToPowerOfTwo(maxBufferSize + kCookieLength); + if (bufferArrayPoolSize != maxDataRentSize) + { + Utils.LogWarning("BufferManager: Max buffer size {0} + cookie length {1} may waste memory because it allocates buffers in the next bucket!", maxBufferSize, kCookieLength); + return bufferArrayPoolSize - kCookieLength; + } + return maxBufferSize; + } + + /// + /// Helper to round up to the next power of two. + /// + private int RoundUpToPowerOfTwo(int value) + { + int result = 1; + + while (result < value && result != 0) + { + result <<= 1; + } + + return result; + } + + /// + /// Returns the max size of data in the buffers. + /// + /// + /// Due to the underlying implementation of the ArrayPool, + /// the actual buffer size may be larger than this value. + /// To avoid memory waste, use this value as a guideline + /// for the maximum buffer size when taking buffers. + /// + public int MaxSuggestedBufferSize => m_maxSuggestedBufferSize; #endregion #region Private Fields private readonly string m_name; private readonly int m_maxBufferSize; + private readonly int m_maxSuggestedBufferSize; #if TRACE_MEMORY private int m_buffersTaken = 0; #endif @@ -348,7 +392,7 @@ namespace Opc.Ua.Bindings private const byte kCookieLocked = 0xa5; private const byte kCookieUnlocked = 0x5a; #if TRACK_MEMORY - const byte kCookieLength = 5; + private const byte kCookieLength = 5; class Allocation { public int Id; diff --git a/Stack/Opc.Ua.Core/Stack/Bindings/BufferSegment.cs b/Stack/Opc.Ua.Core/Stack/Bindings/BufferSegment.cs new file mode 100644 index 0000000000..06750fb5db --- /dev/null +++ b/Stack/Opc.Ua.Core/Stack/Bindings/BufferSegment.cs @@ -0,0 +1,70 @@ +/* ======================================================================== + * Copyright (c) 2005-2024 The OPC Foundation, Inc. All rights reserved. + * + * OPC Foundation MIT License 1.00 + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + * + * The complete license agreement can be found here: + * http://opcfoundation.org/License/MIT/1.00/ + * ======================================================================*/ + +using System; +using System.Buffers; + +namespace Opc.Ua.Bindings +{ + /// + /// Helper to build a ReadOnlySequence from a set of buffers. + /// + public sealed class BufferSegment : ReadOnlySequenceSegment + { + /// + /// Returns the base array of the buffer. + /// + public byte[] Array() => m_array; + + /// + /// Constructor for a buffer segment. + /// + public BufferSegment(byte[] array, int offset, int length) + { + Memory = new ReadOnlyMemory(array, offset, length); + m_array = array; + } + + /// + /// Appends a buffer to the sequence. + /// + public BufferSegment Append(byte[] array, int offset, int length) + { + var segment = new BufferSegment(array, offset, length) { + RunningIndex = RunningIndex + Memory.Length + }; + Next = segment; + return segment; + } + + #region Private Fields + private byte[] m_array; + #endregion + } +} diff --git a/Stack/Opc.Ua.Core/Stack/Bindings/BufferSequence.cs b/Stack/Opc.Ua.Core/Stack/Bindings/BufferSequence.cs new file mode 100644 index 0000000000..81a6471013 --- /dev/null +++ b/Stack/Opc.Ua.Core/Stack/Bindings/BufferSequence.cs @@ -0,0 +1,77 @@ +/* ======================================================================== + * Copyright (c) 2005-2024 The OPC Foundation, Inc. All rights reserved. + * + * OPC Foundation MIT License 1.00 + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + * + * The complete license agreement can be found here: + * http://opcfoundation.org/License/MIT/1.00/ + * ======================================================================*/ + +using System; +using System.Buffers; + +namespace Opc.Ua.Bindings +{ + /// + /// A class to hold a sequence of buffers until disposed. + /// + public sealed class BufferSequence : IDisposable + { + /// + /// The constructor to create the sequence of buffers. + /// + public BufferSequence(BufferManager bufferManager, string owner, BufferSegment firstSegment, ReadOnlySequence sequence) + { + m_bufferManager = bufferManager; + m_owner = owner; + m_firstSegment = firstSegment; + m_sequence = sequence; + } + + /// + public void Dispose() + { + BufferSegment segment = m_firstSegment; + while (segment != null) + { + m_bufferManager.ReturnBuffer(segment.Array(), m_owner); + segment = (BufferSegment)segment.Next; + } + m_sequence = ReadOnlySequence.Empty; + m_firstSegment = null; + GC.SuppressFinalize(this); + } + + /// + /// Returns a sequence which can be used to access the buffers. + /// + public ReadOnlySequence Sequence => m_sequence; + + #region Private + private BufferManager m_bufferManager; + private BufferSegment m_firstSegment; + private ReadOnlySequence m_sequence; + private string m_owner; + #endregion + } +} diff --git a/Stack/Opc.Ua.Core/Types/Encoders/JsonEncoder.cs b/Stack/Opc.Ua.Core/Types/Encoders/JsonEncoder.cs index a36c061448..cc7fc6f675 100644 --- a/Stack/Opc.Ua.Core/Types/Encoders/JsonEncoder.cs +++ b/Stack/Opc.Ua.Core/Types/Encoders/JsonEncoder.cs @@ -283,41 +283,31 @@ namespace Opc.Ua /// public string CloseAndReturnText() { - Close(); - if (m_memoryStream == null) + try { - if (m_stream is MemoryStream memoryStream) + InternalClose(false); + if (m_memoryStream == null) { - return Encoding.UTF8.GetString(memoryStream.ToArray()); + if (m_stream is MemoryStream memoryStream) + { + return Encoding.UTF8.GetString(memoryStream.ToArray()); + } + throw new NotSupportedException("Cannot get text from external stream. Use Close or MemoryStream instead."); } - throw new NotSupportedException("Cannot get text from external stream. Use Close or MemoryStream instead."); + return Encoding.UTF8.GetString(m_memoryStream.ToArray()); + } + finally + { + m_writer?.Dispose(); + m_writer = null; } - return Encoding.UTF8.GetString(m_memoryStream.ToArray()); } /// /// Completes writing and returns the text length. + /// The StreamWriter is disposed. /// - public int Close() - { - if (!m_dontWriteClosing) - { - if (m_topLevelIsArray) - { - m_writer.Write(s_rightSquareBracket); - } - else - { - m_writer.Write(s_rightCurlyBrace); - } - } - - m_writer.Flush(); - int length = (int)m_writer.BaseStream.Position; - m_writer.Dispose(); - m_writer = null; - return length; - } + public int Close() => InternalClose(true); #endregion #region IDisposable Members @@ -339,7 +329,7 @@ namespace Opc.Ua { if (m_writer != null) { - Close(); + InternalClose(true); m_writer = null; } @@ -2580,6 +2570,33 @@ namespace Opc.Ua #endregion #region Private Methods + /// + /// Completes writing and returns the text length. + /// + private int InternalClose(bool dispose) + { + if (!m_dontWriteClosing) + { + if (m_topLevelIsArray) + { + m_writer.Write(s_rightSquareBracket); + } + else + { + m_writer.Write(s_rightCurlyBrace); + } + } + + m_writer.Flush(); + int length = (int)m_writer.BaseStream.Position; + if (dispose) + { + m_writer.Dispose(); + m_writer = null; + } + return length; + } + /// /// Writes a DiagnosticInfo to the stream. /// Ignores InnerDiagnosticInfo field if the nesting level diff --git a/Tests/Opc.Ua.Client.ComplexTypes.Tests/Types/ComplexTypesCommon.cs b/Tests/Opc.Ua.Client.ComplexTypes.Tests/Types/ComplexTypesCommon.cs index 7cf43a05c7..75562af6fb 100644 --- a/Tests/Opc.Ua.Client.ComplexTypes.Tests/Types/ComplexTypesCommon.cs +++ b/Tests/Opc.Ua.Client.ComplexTypes.Tests/Types/ComplexTypesCommon.cs @@ -276,6 +276,7 @@ namespace Opc.Ua.Client.ComplexTypes.Tests.Types /// protected void EncodeDecodeComplexType( IServiceMessageContext encoderContext, + MemoryStreamType memoryStreamType, EncodingType encoderType, StructureType structureType, ExpandedNodeId nodeId, @@ -291,7 +292,7 @@ namespace Opc.Ua.Client.ComplexTypes.Tests.Types TestContext.Out.WriteLine(expected); byte[] buffer; - using (var encoderStream = new MemoryStream()) + using (var encoderStream = CreateEncoderMemoryStream(memoryStreamType)) { using (IEncoder encoder = CreateEncoder(encoderType, encoderContext, encoderStream, typeof(DataValue))) { diff --git a/Tests/Opc.Ua.Client.ComplexTypes.Tests/Types/EncoderTests.cs b/Tests/Opc.Ua.Client.ComplexTypes.Tests/Types/EncoderTests.cs index acdc606508..916e27fd3d 100644 --- a/Tests/Opc.Ua.Client.ComplexTypes.Tests/Types/EncoderTests.cs +++ b/Tests/Opc.Ua.Client.ComplexTypes.Tests/Types/EncoderTests.cs @@ -30,6 +30,7 @@ using System; using System.Collections.Generic; using NUnit.Framework; +using Opc.Ua.Core.Tests.Types.Encoders; namespace Opc.Ua.Client.ComplexTypes.Tests.Types { @@ -86,6 +87,7 @@ namespace Opc.Ua.Client.ComplexTypes.Tests.Types [Theory] [Category("ComplexTypes")] public void ReEncodeComplexType( + MemoryStreamType memoryStreamType, EncodingType encoderType, StructureType structureType ) @@ -96,7 +98,7 @@ namespace Opc.Ua.Client.ComplexTypes.Tests.Types object emittedType = Activator.CreateInstance(complexType); var baseType = emittedType as BaseComplexType; FillStructWithValues(baseType, true); - EncodeDecodeComplexType(EncoderContext, encoderType, structureType, nodeId, emittedType); + EncodeDecodeComplexType(EncoderContext, memoryStreamType, encoderType, structureType, nodeId, emittedType); } /// @@ -106,6 +108,7 @@ namespace Opc.Ua.Client.ComplexTypes.Tests.Types [Theory] [Category("ComplexTypes")] public void ReEncodeStructureWithOptionalFieldsComplexType( + MemoryStreamType memoryStreamType, EncodingType encoderType, StructureFieldParameter structureFieldParameter ) @@ -118,17 +121,17 @@ namespace Opc.Ua.Client.ComplexTypes.Tests.Types var builtInType = structureFieldParameter.BuiltInType; TestContext.Out.WriteLine($"Optional Field: {structureFieldParameter.BuiltInType} is the only value."); baseType[structureFieldParameter.Name] = DataGenerator.GetRandom(builtInType); - EncodeDecodeComplexType(EncoderContext, encoderType, StructureType.StructureWithOptionalFields, nodeId, emittedType); + EncodeDecodeComplexType(EncoderContext, memoryStreamType, encoderType, StructureType.StructureWithOptionalFields, nodeId, emittedType); TestContext.Out.WriteLine($"Optional Field: {structureFieldParameter.BuiltInType} is null."); baseType[structureFieldParameter.Name] = null; - EncodeDecodeComplexType(EncoderContext, encoderType, StructureType.StructureWithOptionalFields, nodeId, emittedType); + EncodeDecodeComplexType(EncoderContext, memoryStreamType, encoderType, StructureType.StructureWithOptionalFields, nodeId, emittedType); TestContext.Out.WriteLine($"Optional Field: {structureFieldParameter.BuiltInType} is null, all other fields have random values."); FillStructWithValues(baseType, true); baseType[structureFieldParameter.Name] = null; - EncodeDecodeComplexType(EncoderContext, encoderType, StructureType.StructureWithOptionalFields, nodeId, emittedType); + EncodeDecodeComplexType(EncoderContext, memoryStreamType, encoderType, StructureType.StructureWithOptionalFields, nodeId, emittedType); TestContext.Out.WriteLine($"Optional Field: {structureFieldParameter.BuiltInType} has random value."); baseType[structureFieldParameter.Name] = DataGenerator.GetRandom(builtInType); - EncodeDecodeComplexType(EncoderContext, encoderType, StructureType.StructureWithOptionalFields, nodeId, emittedType); + EncodeDecodeComplexType(EncoderContext, memoryStreamType, encoderType, StructureType.StructureWithOptionalFields, nodeId, emittedType); } /// @@ -138,6 +141,7 @@ namespace Opc.Ua.Client.ComplexTypes.Tests.Types [Theory] [Category("ComplexTypes")] public void ReEncodeUnionComplexType( + MemoryStreamType memoryStreamType, EncodingType encoderType, StructureFieldParameter structureFieldParameter ) @@ -150,10 +154,10 @@ namespace Opc.Ua.Client.ComplexTypes.Tests.Types var builtInType = structureFieldParameter.BuiltInType; TestContext.Out.WriteLine($"Union Field: {structureFieldParameter.BuiltInType} is random."); baseType[structureFieldParameter.Name] = DataGenerator.GetRandom(builtInType); - EncodeDecodeComplexType(EncoderContext, encoderType, StructureType.Union, nodeId, emittedType); + EncodeDecodeComplexType(EncoderContext, memoryStreamType, encoderType, StructureType.Union, nodeId, emittedType); TestContext.Out.WriteLine($"Union Field: {structureFieldParameter.BuiltInType} is null."); baseType[structureFieldParameter.Name] = null; - EncodeDecodeComplexType(EncoderContext, encoderType, StructureType.Union, nodeId, emittedType); + EncodeDecodeComplexType(EncoderContext, memoryStreamType, encoderType, StructureType.Union, nodeId, emittedType); } #endregion Test Methods } diff --git a/Tests/Opc.Ua.Client.ComplexTypes.Tests/Types/JsonEncoderTests.cs b/Tests/Opc.Ua.Client.ComplexTypes.Tests/Types/JsonEncoderTests.cs index b73e471d16..5f12fea60c 100644 --- a/Tests/Opc.Ua.Client.ComplexTypes.Tests/Types/JsonEncoderTests.cs +++ b/Tests/Opc.Ua.Client.ComplexTypes.Tests/Types/JsonEncoderTests.cs @@ -120,7 +120,8 @@ namespace Opc.Ua.Client.ComplexTypes.Tests.Types /// [Theory] public void JsonEncodeStructureRev( - JsonValidationData jsonValidationData) + JsonValidationData jsonValidationData + ) { ExpandedNodeId nodeId; Type complexType; @@ -131,6 +132,7 @@ namespace Opc.Ua.Client.ComplexTypes.Tests.Types ExtensionObject extensionObject = CreateExtensionObject(StructureType.Structure, nodeId, emittedType); EncodeJsonComplexTypeVerifyResult( jsonValidationData.BuiltInType, + MemoryStreamType.MemoryStream, extensionObject, true, jsonValidationData.ExpectedNonReversible ?? jsonValidationData.ExpectedReversible, @@ -142,7 +144,8 @@ namespace Opc.Ua.Client.ComplexTypes.Tests.Types /// [Theory] public void JsonEncodeStructureNonRev( - JsonValidationData jsonValidationData) + JsonValidationData jsonValidationData + ) { ExpandedNodeId nodeId; Type complexType; @@ -153,6 +156,7 @@ namespace Opc.Ua.Client.ComplexTypes.Tests.Types ExtensionObject extensionObject = CreateExtensionObject(StructureType.Structure, nodeId, emittedType); EncodeJsonComplexTypeVerifyResult( jsonValidationData.BuiltInType, + MemoryStreamType.ArraySegmentStream, extensionObject, false, jsonValidationData.ExpectedNonReversible ?? jsonValidationData.ExpectedReversible, @@ -165,7 +169,8 @@ namespace Opc.Ua.Client.ComplexTypes.Tests.Types /// [Theory] public void JsonEncodeOptionalFieldsRev( - JsonValidationData jsonValidationData) + JsonValidationData jsonValidationData + ) { ExpandedNodeId nodeId; Type complexType; @@ -176,6 +181,7 @@ namespace Opc.Ua.Client.ComplexTypes.Tests.Types ExtensionObject extensionObject = CreateExtensionObject(StructureType.StructureWithOptionalFields, nodeId, emittedType); EncodeJsonComplexTypeVerifyResult( jsonValidationData.BuiltInType, + MemoryStreamType.ArraySegmentStream, extensionObject, true, jsonValidationData.ExpectedNonReversible ?? jsonValidationData.ExpectedReversible, @@ -199,6 +205,7 @@ namespace Opc.Ua.Client.ComplexTypes.Tests.Types ExtensionObject extensionObject = CreateExtensionObject(StructureType.StructureWithOptionalFields, nodeId, emittedType); EncodeJsonComplexTypeVerifyResult( jsonValidationData.BuiltInType, + MemoryStreamType.RecyclableMemoryStream, extensionObject, false, jsonValidationData.ExpectedNonReversible ?? jsonValidationData.ExpectedReversible, @@ -210,7 +217,8 @@ namespace Opc.Ua.Client.ComplexTypes.Tests.Types /// [Theory] public void JsonEncodeUnionRev( - JsonValidationData jsonValidationData) + JsonValidationData jsonValidationData + ) { ExpandedNodeId nodeId; Type complexType; @@ -221,6 +229,7 @@ namespace Opc.Ua.Client.ComplexTypes.Tests.Types ExtensionObject extensionObject = CreateExtensionObject(StructureType.Union, nodeId, emittedType); EncodeJsonComplexTypeVerifyResult( jsonValidationData.BuiltInType, + MemoryStreamType.ArraySegmentStream, extensionObject, true, jsonValidationData.ExpectedNonReversible ?? jsonValidationData.ExpectedReversible, @@ -232,7 +241,8 @@ namespace Opc.Ua.Client.ComplexTypes.Tests.Types /// [Theory] public void JsonEncodeUnionNonRev( - JsonValidationData jsonValidationData) + JsonValidationData jsonValidationData + ) { ExpandedNodeId nodeId; Type complexType; @@ -243,6 +253,7 @@ namespace Opc.Ua.Client.ComplexTypes.Tests.Types ExtensionObject extensionObject = CreateExtensionObject(StructureType.Union, nodeId, emittedType); EncodeJsonComplexTypeVerifyResult( jsonValidationData.BuiltInType, + MemoryStreamType.ArraySegmentStream, extensionObject, false, jsonValidationData.ExpectedNonReversible ?? jsonValidationData.ExpectedReversible, @@ -253,6 +264,7 @@ namespace Opc.Ua.Client.ComplexTypes.Tests.Types #region Private Methods protected void EncodeJsonComplexTypeVerifyResult( BuiltInType builtInType, + MemoryStreamType memoryStreamType, ExtensionObject data, bool useReversibleEncoding, string expected, @@ -269,7 +281,7 @@ namespace Opc.Ua.Client.ComplexTypes.Tests.Types _ = PrettifyAndValidateJson(expected); byte[] buffer; - using (var encoderStream = new MemoryStream()) + using (var encoderStream = CreateEncoderMemoryStream(memoryStreamType)) { using (IEncoder encoder = CreateEncoder( EncodingType.Json, EncoderContext, encoderStream, diff --git a/Tests/Opc.Ua.Client.ComplexTypes.Tests/Types/MockResolverTests.cs b/Tests/Opc.Ua.Client.ComplexTypes.Tests/Types/MockResolverTests.cs index 9bc2e73c05..3b822c94ed 100644 --- a/Tests/Opc.Ua.Client.ComplexTypes.Tests/Types/MockResolverTests.cs +++ b/Tests/Opc.Ua.Client.ComplexTypes.Tests/Types/MockResolverTests.cs @@ -36,6 +36,7 @@ using System.Runtime.Serialization; using System.Text; using System.Threading.Tasks; using NUnit.Framework; +using Opc.Ua.Core.Tests.Types.Encoders; namespace Opc.Ua.Client.ComplexTypes.Tests.Types { @@ -148,7 +149,7 @@ namespace Opc.Ua.Client.ComplexTypes.Tests.Types /// Test the functionality to create a custom complex type. /// [Theory] - public async Task CreateMockTypeAsync(EncodingType encodingType) + public async Task CreateMockTypeAsync(EncodingType encodingType, MemoryStreamType memoryStreamType) { var mockResolver = new MockResolver(); @@ -255,7 +256,7 @@ namespace Opc.Ua.Client.ComplexTypes.Tests.Types }; byte[] buffer; - using (var encoderStream = new MemoryStream()) + using (var encoderStream = CreateEncoderMemoryStream(memoryStreamType)) { using (IEncoder encoder = CreateEncoder(EncodingType.Json, encoderContext, encoderStream, carType)) { @@ -267,7 +268,7 @@ namespace Opc.Ua.Client.ComplexTypes.Tests.Types _ = PrettifyAndValidateJson(Encoding.UTF8.GetString(buffer)); // test encoder/decoder - EncodeDecodeComplexType(encoderContext, encodingType, StructureType.Structure, nodeId, car); + EncodeDecodeComplexType(encoderContext, memoryStreamType, encodingType, StructureType.Structure, nodeId, car); // Test extracting type definition @@ -281,7 +282,7 @@ namespace Opc.Ua.Client.ComplexTypes.Tests.Types /// Test the functionality to create a custom complex type. /// [Theory] - public async Task CreateMockArrayTypeAsync(EncodingType encodingType) + public async Task CreateMockArrayTypeAsync(EncodingType encodingType, MemoryStreamType memoryStreamType) { var mockResolver = new MockResolver(); @@ -430,7 +431,7 @@ namespace Opc.Ua.Client.ComplexTypes.Tests.Types }; byte[] buffer; - using (var encoderStream = new MemoryStream()) + using (var encoderStream = CreateEncoderMemoryStream(memoryStreamType)) { using (IEncoder encoder = CreateEncoder(EncodingType.Json, encoderContext, encoderStream, arraysTypes, false)) { @@ -442,7 +443,7 @@ namespace Opc.Ua.Client.ComplexTypes.Tests.Types _ = PrettifyAndValidateJson(Encoding.UTF8.GetString(buffer)); // test encoder/decoder - EncodeDecodeComplexType(encoderContext, encodingType, StructureType.Structure, dataTypeNode.NodeId, arrays); + EncodeDecodeComplexType(encoderContext, memoryStreamType, encodingType, StructureType.Structure, dataTypeNode.NodeId, arrays); // Test extracting type definition @@ -456,7 +457,7 @@ namespace Opc.Ua.Client.ComplexTypes.Tests.Types /// Create a complex type with a single scalar or array type, with default and random values . /// [Theory] - public async Task CreateMockSingleTypeAsync(EncodingType encodingType, TestType typeDescription, bool randomValues, TestRanks testRank) + public async Task CreateMockSingleTypeAsync(EncodingType encodingType, MemoryStreamType memoryStreamType, TestType typeDescription, bool randomValues, TestRanks testRank) { SetRepeatedRandomSeed(); @@ -611,7 +612,7 @@ namespace Opc.Ua.Client.ComplexTypes.Tests.Types }; byte [] buffer; - using (var encoderStream = new MemoryStream()) + using (var encoderStream = CreateEncoderMemoryStream(memoryStreamType)) { using (IEncoder encoder = CreateEncoder(EncodingType.Json, encoderContext, encoderStream, arraysTypes, false)) { @@ -623,7 +624,7 @@ namespace Opc.Ua.Client.ComplexTypes.Tests.Types _ = PrettifyAndValidateJson(Encoding.UTF8.GetString(buffer)); // test encoder/decoder - EncodeDecodeComplexType(encoderContext, encodingType, StructureType.Structure, dataTypeNode.NodeId, testType); + EncodeDecodeComplexType(encoderContext, memoryStreamType, encodingType, StructureType.Structure, dataTypeNode.NodeId, testType); // Test extracting type definition diff --git a/Tests/Opc.Ua.Client.Tests/ClientTest.cs b/Tests/Opc.Ua.Client.Tests/ClientTest.cs index 34d4405751..3cd7719848 100644 --- a/Tests/Opc.Ua.Client.Tests/ClientTest.cs +++ b/Tests/Opc.Ua.Client.Tests/ClientTest.cs @@ -614,13 +614,16 @@ namespace Opc.Ua.Client.Tests sre = Assert.Throws(() => session1.ReadValue(VariableIds.Server_ServerStatus, typeof(ServerStatusDataType))); // TODO: Both channel should return BadSecureChannelClosed - if (endpoint.EndpointUrl.ToString().StartsWith(Utils.UriSchemeOpcTcp, StringComparison.Ordinal)) + if (!(StatusCodes.BadSecureChannelClosed == sre.StatusCode)) { - Assert.AreEqual(StatusCodes.BadSessionIdInvalid, sre.StatusCode, sre.Message); - } - else - { - Assert.AreEqual(StatusCodes.BadUnknownResponse, sre.StatusCode, sre.Message); + if (endpoint.EndpointUrl.ToString().StartsWith(Utils.UriSchemeOpcTcp, StringComparison.Ordinal)) + { + Assert.AreEqual(StatusCodes.BadSessionIdInvalid, sre.StatusCode, sre.Message); + } + else + { + Assert.AreEqual(StatusCodes.BadUnknownResponse, sre.StatusCode, sre.Message); + } } } diff --git a/Tests/Opc.Ua.Core.Tests/Types/Bindings/BufferManager.cs b/Tests/Opc.Ua.Core.Tests/Types/Bindings/BufferManagerBenchmarks.cs similarity index 98% rename from Tests/Opc.Ua.Core.Tests/Types/Bindings/BufferManager.cs rename to Tests/Opc.Ua.Core.Tests/Types/Bindings/BufferManagerBenchmarks.cs index 719b4d9bca..998f4d7ea1 100644 --- a/Tests/Opc.Ua.Core.Tests/Types/Bindings/BufferManager.cs +++ b/Tests/Opc.Ua.Core.Tests/Types/Bindings/BufferManagerBenchmarks.cs @@ -43,7 +43,7 @@ namespace Opc.Ua.Core.Tests.Stack.Bindings public class BufferManagerBenchmarks { //[Params(8192, 65535, 1024 * 1024 - 1)] - public int BufferSize { get; set; } = 65535; + public int BufferSize { get; set; } = TcpMessageLimits.DefaultMaxBufferSize; //[Params( /*8,*/ 64, 256, 1024)] public int Allocations { get; set; } = 256; diff --git a/Tests/Opc.Ua.Core.Tests/Types/Encoders/BinaryDecoderBenchmarks.cs b/Tests/Opc.Ua.Core.Tests/Types/Encoders/BinaryDecoderBenchmarks.cs new file mode 100644 index 0000000000..f792af20f0 --- /dev/null +++ b/Tests/Opc.Ua.Core.Tests/Types/Encoders/BinaryDecoderBenchmarks.cs @@ -0,0 +1,239 @@ +/* ======================================================================== + * Copyright (c) 2005-2024 The OPC Foundation, Inc. All rights reserved. + * + * OPC Foundation MIT License 1.00 + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + * + * The complete license agreement can be found here: + * http://opcfoundation.org/License/MIT/1.00/ + * ======================================================================*/ + +using System.IO; +using BenchmarkDotNet.Attributes; +using NUnit.Framework; +using Opc.Ua.Bindings; + +namespace Opc.Ua.Core.Tests.Types.Encoders +{ + [TestFixture, Category("BinaryEncoder")] + [SetCulture("en-us"), SetUICulture("en-us")] + [NonParallelizable] + [MemoryDiagnoser] + [DisassemblyDiagnoser] + public class BinaryDecoderBenchmarks : EncoderBenchmarks + { + /// + /// Test decoding with internal memory stream. + /// + [Test] + public void BinaryDecoderInternalMemoryStreamTest() + { + using (var binaryDecoder = new BinaryDecoder(m_encodedByteArray, m_context)) + { + TestDecoding(binaryDecoder); + } + } + + /// + /// Test decoding with ArrayPool memory stream. + /// + [Test] + public void BinaryDecoderArraySegmentStreamTest() + { + using (var memoryStream = new ArraySegmentStream(m_encodedBufferList)) + using (var binaryDecoder = new BinaryDecoder(memoryStream, m_context)) + { + TestDecoding(binaryDecoder); + } + } + + /// + /// Test decoding with ArrayPool memory stream that has no span support. + /// + [Test] + public void BinaryDecoderArraySegmentStreamNoSpanTest() + { +#if NET6_0_OR_GREATER + using (var arraySegmentStream = new ArraySegmentStreamNoSpan(m_encodedBufferList)) +#else + using (var arraySegmentStream = new ArraySegmentStream(m_encodedBufferList)) +#endif + using (var binaryDecoder = new BinaryDecoder(arraySegmentStream, m_context)) + { + TestDecoding(binaryDecoder); + } + } + + /// + /// Benchmark decoding with memory stream. + /// + [Benchmark(Baseline = true)] + [Test] + public void BinaryDecoderMemoryStream() + { + using (var memoryStream = new MemoryStream(m_encodedByteArray)) + { + BinaryDecoder_Stream(memoryStream); + } + } + + /// + /// Benchmark decoding with array segment memory stream. + /// + [Benchmark] + [Test] + public void BinaryDecoderArraySegmentStream() + { + using (var arraySegmentStream = new ArraySegmentStream(m_encodedBufferList)) + { + BinaryDecoder_Stream(arraySegmentStream); + } + } + + /// + /// Benchmark decoding with array segment memory stream without span support. + /// + [Benchmark] + [Test] + public void BinaryDecoderArraySegmentStreamNoSpan() + { +#if NET6_0_OR_GREATER + using (var arraySegmentStream = new ArraySegmentStreamNoSpan(m_encodedBufferList)) +#else + using (var arraySegmentStream = new ArraySegmentStream(m_encodedBufferList)) +#endif + { + BinaryDecoder_Stream(arraySegmentStream); + } + } + + #region Private Methods + private void TestStreamDecode(MemoryStream memoryStream) + { + using (var binaryDecoder = new BinaryDecoder(memoryStream, m_context)) + { + TestDecoding(binaryDecoder); + TestDecoding(binaryDecoder); + binaryDecoder.Close(); + } + } + + private void BinaryDecoder_Stream(MemoryStream memoryStream) + { + using (var binaryDecoder = new BinaryDecoder(memoryStream, m_context)) + { + TestDecoding(binaryDecoder); + TestDecoding(binaryDecoder); + binaryDecoder.Close(); + } + } + #endregion + + #region Test Setup + [OneTimeSetUp] + public new void OneTimeSetUp() + { + base.OneTimeSetUp(); + InitializeEncodedTestData(); + } + + [OneTimeTearDown] + public new void OneTimeTearDown() + { + base.OneTimeTearDown(); + } + #endregion + + #region Benchmark Setup + /// + /// Set up some variables for benchmarks. + /// + [GlobalSetup] + public new void GlobalSetup() + { + base.GlobalSetup(); + InitializeEncodedBenchmarkData(); + } + + /// + /// Tear down benchmark variables. + /// + [GlobalCleanup] + public new void GlobalCleanup() + { + base.GlobalCleanup(); + } + #endregion + + #region Private Methods + /// + /// Initialize encoded data. + /// + private void InitializeEncodedTestData() + { + using (var memoryStream = new MemoryStream(StreamBufferSize)) + using (var binaryEncoder = new BinaryEncoder(memoryStream, m_context, true)) + { + TestEncoding(binaryEncoder); + TestEncoding(binaryEncoder); + m_encodedByteArray = binaryEncoder.CloseAndReturnBuffer(); + } + + using (var memoryStream = new ArraySegmentStream(m_bufferManager)) + using (var binaryEncoder = new BinaryEncoder(memoryStream, m_context, true)) + { + TestEncoding(binaryEncoder); + TestEncoding(binaryEncoder); + binaryEncoder.Close(); + m_encodedBufferList = memoryStream.GetBuffers("writer"); + } + } + + /// + /// Initialize encoded data. + /// + private void InitializeEncodedBenchmarkData() + { + using (var memoryStream = new MemoryStream(StreamBufferSize)) + using (var binaryEncoder = new BinaryEncoder(memoryStream, m_context, true)) + { + TestEncoding(binaryEncoder); + TestEncoding(binaryEncoder); + m_encodedByteArray = binaryEncoder.CloseAndReturnBuffer(); + } + + using (var memoryStream = new ArraySegmentStream(m_bufferManager)) + using (var binaryEncoder = new BinaryEncoder(memoryStream, m_context, true)) + { + TestEncoding(binaryEncoder); + TestEncoding(binaryEncoder); + binaryEncoder.Close(); + m_encodedBufferList = memoryStream.GetBuffers("writer"); + } + } + #endregion + + private byte[] m_encodedByteArray; + private BufferCollection m_encodedBufferList; + + } +} diff --git a/Tests/Opc.Ua.Core.Tests/Types/Encoders/BinaryEncoderBenchmarks.cs b/Tests/Opc.Ua.Core.Tests/Types/Encoders/BinaryEncoderBenchmarks.cs index ce10178cd6..1dda74441d 100644 --- a/Tests/Opc.Ua.Core.Tests/Types/Encoders/BinaryEncoderBenchmarks.cs +++ b/Tests/Opc.Ua.Core.Tests/Types/Encoders/BinaryEncoderBenchmarks.cs @@ -1,5 +1,5 @@ /* ======================================================================== - * Copyright (c) 2005-2023 The OPC Foundation, Inc. All rights reserved. + * Copyright (c) 2005-2024 The OPC Foundation, Inc. All rights reserved. * * OPC Foundation MIT License 1.00 * @@ -27,11 +27,10 @@ * http://opcfoundation.org/License/MIT/1.00/ * ======================================================================*/ -using System; -using System.Collections.Generic; using System.IO; using System.Text; using BenchmarkDotNet.Attributes; +using Microsoft.IO; using NUnit.Framework; using Opc.Ua.Bindings; @@ -42,167 +41,206 @@ namespace Opc.Ua.Core.Tests.Types.Encoders [NonParallelizable] [MemoryDiagnoser] [DisassemblyDiagnoser] - public class BinaryEncoderBenchmarks + public class BinaryEncoderBenchmarks : EncoderBenchmarks { - const int kBufferSize = 4096; - - [Params(1, 2, 8, 64)] - public int PayLoadSize { get; set; } = 64; - /// - /// Benchmark encoding with internal memory stream. + /// Test encoding with internal memory stream. /// - [Benchmark] [Test] - public void BinaryEncoderConstructor2() + public void BinaryEncoderInternalMemoryStreamTest() { using (var binaryEncoder = new BinaryEncoder(m_context)) { TestEncoding(binaryEncoder); - _ = binaryEncoder.CloseAndReturnBuffer(); + var result = binaryEncoder.CloseAndReturnBuffer(); + Assert.NotNull(result); + } + } + + + /// + /// Test encoding with internal memory stream, + /// uses reflection to get array from memory stream. + /// + [Test] + public void BinaryEncoderConstructorStreamwriterReflection2() + { + using (var memoryStream = new MemoryStream(StreamBufferSize)) + using (var binaryEncoder = new BinaryEncoder(memoryStream, m_context, true)) + { + TestEncoding(binaryEncoder); + var result = binaryEncoder.CloseAndReturnBuffer(); + Assert.NotNull(result); } } /// - /// Benchmark encoding with ArrayPool memory stream. + /// Test encoding with ArrayPool memory stream. /// - [Benchmark] - [Test] - public void BinaryEncoderConstructor3() + [Theory] + public void BinaryEncoderArraySegmentStreamTest(bool toArray) { - using (var stream = new ArraySegmentStream(m_bufferManager, kBufferSize, 0, kBufferSize)) + using (var memoryStream = new ArraySegmentStream(m_bufferManager)) { - using (var binaryEncoder = new BinaryEncoder(stream, m_context, false)) - { - TestEncoding(binaryEncoder); - _ = binaryEncoder.CloseAndReturnBuffer(); - } + TestStreamEncode(memoryStream, toArray); + } + } + + /// + /// Test encoding with memory stream kept open. + /// + [Theory] + public void BinaryEncoderMemoryStreamTest(bool toArray) + { + using (var memoryStream = new MemoryStream(StreamBufferSize)) + { + TestStreamEncode(memoryStream, toArray); + } + } + + /// + /// Test encoding with recyclable memory stream kept open. + /// + [Theory] + public void BinaryEncoderRecyclableMemoryStream(bool toArray) + { + using (var memoryStream = new RecyclableMemoryStream(m_memoryManager)) + { + TestStreamEncode(memoryStream, toArray); } } /// /// Benchmark encoding with memory stream kept open. /// - [Benchmark] + [Benchmark(Baseline = true)] [Test] - public void BinaryEncoderConstructorStreamwriter2() + public void BinaryEncoderMemoryStream() { - using (IEncoder binaryEncoder = new BinaryEncoder(m_memoryStream, m_context, true)) + using (var memoryStream = new MemoryStream(StreamBufferSize)) { - TestEncoding(binaryEncoder); - int length = binaryEncoder.Close(); - var result = Encoding.UTF8.GetString(m_memoryStream.ToArray()); + BinaryEncoder_StreamLeaveOpen(memoryStream); + // get buffer for write + _ = memoryStream.ToArray(); } } - /// - /// Encoding test with memory stream kept open. - /// - [Benchmark] - [Test] - public void BinaryEncoderStreamLeaveOpenMemoryStream() - { - BinaryEncoder_StreamLeaveOpen(m_memoryStream); - } - /// /// Benchmark encoding with recyclable memory stream kept open. /// [Benchmark] [Test] - public void BinaryEncoderStreamLeaveOpenRecyclableMemoryStream() + public void BinaryEncoderRecyclableMemoryStream() { - BinaryEncoder_StreamLeaveOpen(m_recyclableMemoryStream); - } - - /// - /// Encoding test with memory stream kept open. - /// - [Benchmark] - [Test] - public void BinaryEncoderStreamLeaveOpenArraySegmentStream() - { - BinaryEncoder_StreamLeaveOpen(m_arraySegmentStream); - } - - /// - /// Benchmark encoding with memory stream kept open, - /// use internal reflection to get string from memory stream. - /// - [Benchmark] - [Test] - public void BinaryEncoderConstructorStreamwriterReflection2() - { - using (var binaryEncoder = new BinaryEncoder(m_memoryStream, m_context, true)) + using (var recyclableMemoryStream = new RecyclableMemoryStream(m_memoryManager)) { - TestEncoding(binaryEncoder); - var result = binaryEncoder.CloseAndReturnBuffer(); + BinaryEncoder_StreamLeaveOpen(recyclableMemoryStream); + // get buffers for write + _ = recyclableMemoryStream.GetReadOnlySequence(); + } + } + + /// + /// Benchmark encoding with array segment memory stream kept open. + /// + [Benchmark] + [Test] + public void BinaryEncoderArraySegmentStream() + { + using (var arraySegmentStream = new ArraySegmentStream(m_bufferManager)) + { + BinaryEncoder_StreamLeaveOpen(arraySegmentStream); + // get buffers and return them to buffer manager + var buffers = arraySegmentStream.GetBuffers("writer"); + foreach (var buffer in buffers) + { + m_bufferManager.ReturnBuffer(buffer.Array, "testreturn"); + } + } + } + + /// + /// Benchmark encoding with array segment memory stream kept open, + /// to compare the version without span support. + /// + [Benchmark] + [Test] + public void BinaryEncoderArraySegmentStreamNoSpan() + { +#if NET6_0_OR_GREATER + using (var arraySegmentStream = new ArraySegmentStreamNoSpan(m_bufferManager)) +#else + using (var arraySegmentStream = new ArraySegmentStream(m_bufferManager)) +#endif + { + BinaryEncoder_StreamLeaveOpen(arraySegmentStream); + // get buffers and return them to buffer manager + var buffers = arraySegmentStream.GetBuffers("writer"); + foreach (var buffer in buffers) + { + m_bufferManager.ReturnBuffer(buffer.Array, "testreturn"); + } } } #region Private Methods - private void BinaryEncoder_StreamLeaveOpen(MemoryStream stream) + private void TestStreamEncode(MemoryStream memoryStream, bool toArray) + { + using (var binaryEncoder = new BinaryEncoder(memoryStream, m_context, true)) + { + TestEncoding(binaryEncoder); + _ = binaryEncoder.Close(); + } + using (var binaryEncoder = new BinaryEncoder(memoryStream, m_context, true)) + { + TestEncoding(binaryEncoder); + if (toArray) + { + int length = binaryEncoder.Close(); + Assert.AreEqual(length, memoryStream.Position); + var result = memoryStream.ToArray(); + Assert.NotNull(result); + Assert.AreEqual(length, result.Length); + } + else + { + var result = binaryEncoder.CloseAndReturnBuffer(); + Assert.NotNull(result); + } + } + } + + private int BinaryEncoder_StreamLeaveOpen(MemoryStream memoryStream, bool testResult = false) { int length1; int length2; - m_memoryStream.Position = 0; - using (IEncoder encoder = new BinaryEncoder(stream, m_context, true)) + using (var encoder = new BinaryEncoder(memoryStream, m_context, true)) { TestEncoding(encoder); length1 = encoder.Close(); } - using (IEncoder encoder = new BinaryEncoder(stream, m_context, true)) + using (var encoder = new BinaryEncoder(memoryStream, m_context, true)) { TestEncoding(encoder); length2 = encoder.Close(); } - Assert.AreEqual(length1 * 2, length2); - var result = Encoding.UTF8.GetString(stream.ToArray()); - Assert.NotNull(result); - Assert.AreEqual(length2, stream.Position); - } - - private void TestEncoding(IEncoder encoder) - { - int payLoadSize = PayLoadSize; - encoder.WriteByte("Byte", 0); - while (--payLoadSize > 0) + if (testResult) { - encoder.WriteBoolean("Boolean", true); - encoder.WriteUInt64("UInt64", 1234566890); - encoder.WriteString("String", "The quick brown fox..."); - encoder.WriteNodeId("NodeId", s_nodeId); - encoder.WriteInt32Array("Array", s_list); + Assert.AreEqual(length1 * 2, length2); + var result = Encoding.UTF8.GetString(memoryStream.ToArray()); + Assert.NotNull(result); + Assert.AreEqual(length2, memoryStream.Position); } + return length1 + length2; } #endregion #region Test Setup [OneTimeSetUp] - public void OneTimeSetUp() - { - // for validating benchmark tests - m_context = new ServiceMessageContext(); - m_memoryStream = new MemoryStream(); - m_memoryManager = new Microsoft.IO.RecyclableMemoryStreamManager(); - m_recyclableMemoryStream = new Microsoft.IO.RecyclableMemoryStream(m_memoryManager); - m_bufferManager = new BufferManager(nameof(BinaryEncoder), kBufferSize); - m_arraySegmentStream = new ArraySegmentStream(m_bufferManager, kBufferSize, 0, kBufferSize); - } + public new void OneTimeSetUp() => base.OneTimeSetUp(); [OneTimeTearDown] - public void OneTimeTearDown() - { - m_context = null; - m_memoryStream.Dispose(); - m_memoryStream = null; - m_recyclableMemoryStream.Dispose(); - m_recyclableMemoryStream = null; - m_memoryManager = null; - m_bufferManager = null; - m_arraySegmentStream.Dispose(); - m_arraySegmentStream = null; - } + public new void OneTimeTearDown() => base.OneTimeTearDown(); #endregion #region Benchmark Setup @@ -210,44 +248,13 @@ namespace Opc.Ua.Core.Tests.Types.Encoders /// Set up some variables for benchmarks. /// [GlobalSetup] - public void GlobalSetup() - { - // for validating benchmark tests - m_context = new ServiceMessageContext(); - m_memoryStream = new MemoryStream(); - m_memoryManager = new Microsoft.IO.RecyclableMemoryStreamManager(); - m_recyclableMemoryStream = new Microsoft.IO.RecyclableMemoryStream(m_memoryManager); - m_bufferManager = new BufferManager(nameof(BinaryEncoder), kBufferSize); - m_arraySegmentStream = new ArraySegmentStream(m_bufferManager, kBufferSize, 0, kBufferSize); - } + public new void GlobalSetup() => base.GlobalSetup(); /// /// Tear down benchmark variables. /// [GlobalCleanup] - public void GlobalCleanup() - { - m_context = null; - m_memoryStream.Dispose(); - m_memoryStream = null; - m_recyclableMemoryStream.Dispose(); - m_recyclableMemoryStream = null; - m_memoryManager = null; - m_bufferManager = null; - m_arraySegmentStream.Dispose(); - m_arraySegmentStream = null; - } - #endregion - - #region Private Fields - private static NodeId s_nodeId = new NodeId(1234); - private static IList s_list = new List() { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; - private IServiceMessageContext m_context; - private MemoryStream m_memoryStream; - private Microsoft.IO.RecyclableMemoryStreamManager m_memoryManager; - private Microsoft.IO.RecyclableMemoryStream m_recyclableMemoryStream; - private BufferManager m_bufferManager; - private ArraySegmentStream m_arraySegmentStream; + public new void GlobalCleanup() => base.GlobalCleanup(); #endregion } } diff --git a/Tests/Opc.Ua.Core.Tests/Types/Encoders/EncodeableTests.cs b/Tests/Opc.Ua.Core.Tests/Types/Encoders/EncodeableTests.cs index b615578560..9adbfdee23 100644 --- a/Tests/Opc.Ua.Core.Tests/Types/Encoders/EncodeableTests.cs +++ b/Tests/Opc.Ua.Core.Tests/Types/Encoders/EncodeableTests.cs @@ -57,6 +57,7 @@ namespace Opc.Ua.Core.Tests.Types.Encoders [Category("EncodeableTypes")] public void ActivateEncodeableType( EncodingType encoderType, + MemoryStreamType memoryStreamType, Type systemType ) { @@ -68,13 +69,14 @@ namespace Opc.Ua.Core.Tests.Types.Encoders Assert.AreNotEqual(testObject.TypeId, testObject.BinaryEncodingId); Assert.AreNotEqual(testObject.TypeId, testObject.XmlEncodingId); Assert.AreNotEqual(testObject.BinaryEncodingId, testObject.XmlEncodingId); - EncodeDecode(encoderType, BuiltInType.ExtensionObject, new ExtensionObject(testObject.TypeId, testObject)); + EncodeDecode(encoderType, BuiltInType.ExtensionObject, memoryStreamType, new ExtensionObject(testObject.TypeId, testObject)); } [Theory] [Category("EncodeableTypes")] public void ActivateEncodeableTypeArray( EncodingType encoderType, + MemoryStreamType memoryStreamType, Type systemType ) { @@ -95,7 +97,7 @@ namespace Opc.Ua.Core.Tests.Types.Encoders BuiltInType builtInType = BuiltInType.Variant; byte[] buffer; - using (var encoderStream = new MemoryStream()) + using (var encoderStream = CreateEncoderMemoryStream(memoryStreamType)) { using (IEncoder encoder = CreateEncoder(encoderType, Context, encoderStream, systemType)) { @@ -140,6 +142,7 @@ namespace Opc.Ua.Core.Tests.Types.Encoders [Category("EncodeableTypes")] public void ActivateEncodeableTypeMatrix( EncodingType encoderType, + MemoryStreamType memoryStreamType, bool encodeAsMatrix, Type systemType ) @@ -167,7 +170,7 @@ namespace Opc.Ua.Core.Tests.Types.Encoders Matrix matrix = new Matrix(array, builtInType, dimensions); byte[] buffer; - using (var encoderStream = new MemoryStream()) + using (var encoderStream = CreateEncoderMemoryStream(memoryStreamType)) { using (IEncoder encoder = CreateEncoder(encoderType, Context, encoderStream, systemType)) { diff --git a/Tests/Opc.Ua.Core.Tests/Types/Encoders/EncoderBenchmarks.cs b/Tests/Opc.Ua.Core.Tests/Types/Encoders/EncoderBenchmarks.cs new file mode 100644 index 0000000000..ff9a60ae47 --- /dev/null +++ b/Tests/Opc.Ua.Core.Tests/Types/Encoders/EncoderBenchmarks.cs @@ -0,0 +1,201 @@ +/* ======================================================================== + * Copyright (c) 2005-2024 The OPC Foundation, Inc. All rights reserved. + * + * OPC Foundation MIT License 1.00 + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + * + * The complete license agreement can be found here: + * http://opcfoundation.org/License/MIT/1.00/ + * ======================================================================*/ + +#pragma warning disable CA5394 // Do not use insecure randomness + +using System; +using System.Collections.Generic; +using BenchmarkDotNet.Attributes; +using Microsoft.IO; +using Opc.Ua.Bindings; + +namespace Opc.Ua.Core.Tests.Types.Encoders +{ + /// + /// Common code for encoder/decoder benchmarks. + /// + public class EncoderBenchmarks + { + public const int StreamBufferSize = 4096; + public const int DataValueCount = 10; + + public EncoderBenchmarks() + { + m_random = new Random(0x62541); + m_nodeId = new NodeId((uint)m_random.Next(50000)); + m_list = new List(); + for (int i = 0; i < DataValueCount; i++) + { + m_list.Add(m_random.Next()); + } + m_values = new List(); + DateTime now = new DateTime(2024, 03, 01, 06, 05, 59, DateTimeKind.Utc); + now += TimeSpan.FromTicks(456789); + for (int i = 0; i < DataValueCount; i++) + { + m_values.Add(new DataValue(new Variant((m_random.NextDouble() - 0.5) * 1000.0), m_random.NextDouble() > 0.1 ? StatusCodes.Good : StatusCodes.BadDataLost, now, now)); + } + } + + [Params(64, 1024)] + public int PayLoadSize { get; set; } = 64; + + #region Private Methods + protected void TestEncoding(IEncoder encoder) + { + var now = DateTime.UtcNow; + int payLoadSize = PayLoadSize; + encoder.WriteInt32("PayloadSize", payLoadSize); + while (payLoadSize-- > 0) + { + encoder.WriteBoolean("Boolean", true); + encoder.WriteByte("Byte", 123); + encoder.WriteUInt16("UInt16", 1234); + encoder.WriteUInt32("UInt32", 123456); + encoder.WriteUInt64("UInt64", 1234566890); + encoder.WriteSByte("Int8", -123); + encoder.WriteInt16("Int16", -1234); + encoder.WriteInt32("Int32", -123456); + encoder.WriteInt64("Int64", -1234566890); + encoder.WriteFloat("Float", 123.456f); + encoder.WriteDouble("Double", 123456.789); + encoder.WriteDateTime("DateTime", now); + encoder.WriteString("String", "The quick brown fox jumps over the lazy dog."); + encoder.WriteNodeId("NodeId", m_nodeId); + encoder.WriteNodeId("ExpandedNodeId", m_nodeId); + encoder.WriteInt32Array("Array", m_list); + encoder.WriteDataValueArray("DataValues", m_values); + } + } + + protected void TestDecoding(IDecoder decoder) + { + var payLoadSize = decoder.ReadInt32("PayloadSize"); + while (payLoadSize-- > 0) + { + _ = decoder.ReadBoolean("Boolean"); + _ = decoder.ReadByte("Byte"); + _ = decoder.ReadUInt16("UInt16"); + _ = decoder.ReadUInt32("UInt32"); + _ = decoder.ReadUInt64("UInt64"); + _ = decoder.ReadSByte("Int8"); + _ = decoder.ReadInt16("Int16"); + _ = decoder.ReadInt32("Int32"); + _ = decoder.ReadInt64("Int64"); + _ = decoder.ReadFloat("Float"); + _ = decoder.ReadDouble("Double"); + _ = decoder.ReadDateTime("DateTime"); + _ = decoder.ReadString("String"); + _ = decoder.ReadNodeId("NodeId"); + _ = decoder.ReadNodeId("ExpandedNodeId"); + _ = decoder.ReadInt32Array("Array"); + _ = decoder.ReadDataValueArray("DataValues"); + } + } + #endregion + + #region Test Setup + public void OneTimeSetUp() + { + // for validating benchmark tests + m_context = new ServiceMessageContext(); + m_memoryManager = new RecyclableMemoryStreamManager(new RecyclableMemoryStreamManager.Options { BlockSize = StreamBufferSize }); + m_bufferManager = new BufferManager(nameof(BinaryEncoder), StreamBufferSize); + } + + public void OneTimeTearDown() + { + m_context = null; + m_memoryManager = null; + m_bufferManager = null; + } + #endregion + + #region Benchmark Setup + /// + /// Set up some variables for benchmarks. + /// + public void GlobalSetup() + { + // for validating benchmark tests + m_context = new ServiceMessageContext(); + m_memoryManager = new RecyclableMemoryStreamManager(new RecyclableMemoryStreamManager.Options { BlockSize = StreamBufferSize }); + m_bufferManager = new BufferManager(nameof(BinaryEncoder), StreamBufferSize); + } + + /// + /// Tear down benchmark variables. + /// + public void GlobalCleanup() + { + m_context = null; + m_memoryManager = null; + m_bufferManager = null; + } + #endregion + + #region Protected Fields + protected Random m_random; + protected NodeId m_nodeId = new NodeId(1234); + protected List m_list; + protected List m_values; + protected IServiceMessageContext m_context; + protected RecyclableMemoryStreamManager m_memoryManager; + protected BufferManager m_bufferManager; + #endregion + } + +#if NET6_0_OR_GREATER + /// + /// Helper class to test ArraySegmentStream without Span support. + /// + public class ArraySegmentStreamNoSpan : ArraySegmentStream + { + public ArraySegmentStreamNoSpan(BufferManager bufferManager) + : base(bufferManager) + { + } + + public ArraySegmentStreamNoSpan(BufferCollection buffers) + : base(buffers) + { + } + + public override int Read(Span buffer) + { + return base.ReadMemoryStream(buffer); + } + + public override void Write(ReadOnlySpan buffer) + { + base.WriteMemoryStream(buffer); + } + } +#endif +} diff --git a/Tests/Opc.Ua.Core.Tests/Types/Encoders/EncoderCommon.cs b/Tests/Opc.Ua.Core.Tests/Types/Encoders/EncoderCommon.cs index c50b979081..3ef4f09e1d 100644 --- a/Tests/Opc.Ua.Core.Tests/Types/Encoders/EncoderCommon.cs +++ b/Tests/Opc.Ua.Core.Tests/Types/Encoders/EncoderCommon.cs @@ -37,13 +37,25 @@ using System.Runtime.Serialization; using System.Text; using System.Threading; using System.Xml; +using Microsoft.IO; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using NUnit.Framework; +using Opc.Ua.Bindings; using Opc.Ua.Test; namespace Opc.Ua.Core.Tests.Types.Encoders { + /// + /// Supported memory stream types. + /// + public enum MemoryStreamType + { + MemoryStream, + ArraySegmentStream, + RecyclableMemoryStream + } + /// /// Base class for the encoder tests. /// @@ -55,12 +67,16 @@ namespace Opc.Ua.Core.Tests.Types.Encoders protected const int kRandomStart = 4840; protected const int kRandomRepeats = 100; protected const int kMaxArrayLength = 1024 * 64; + protected const int kTestBlockSize = 0x1000; protected const string kApplicationUri = "uri:localhost:opcfoundation.org:EncoderCommon"; protected RandomSource RandomSource { get; private set; } protected DataGenerator DataGenerator { get; private set; } protected IServiceMessageContext Context { get; private set; } protected NamespaceTable NameSpaceUris { get; private set; } protected StringTable ServerUris { get; private set; } + protected BufferManager BufferManager { get; private set; } + protected RecyclableMemoryStreamManager RecyclableMemoryManager { get; private set; } + #region Test Setup [OneTimeSetUp] @@ -74,6 +90,8 @@ namespace Opc.Ua.Core.Tests.Types.Encoders NameSpaceUris.GetIndexOrAppend(kApplicationUri); NameSpaceUris.GetIndexOrAppend(Namespaces.OpcUaGds); ServerUris = new StringTable(); + BufferManager = new BufferManager(nameof(EncoderCommon), kTestBlockSize); + RecyclableMemoryManager = new RecyclableMemoryStreamManager(new RecyclableMemoryStreamManager.Options { BlockSize = kTestBlockSize }); } [OneTimeTearDown] @@ -135,6 +153,7 @@ namespace Opc.Ua.Core.Tests.Types.Encoders protected string EncodeDataValue( EncodingType encoderType, BuiltInType builtInType, + MemoryStreamType memoryStreamType, object data, bool useReversibleEncoding = true ) @@ -146,7 +165,7 @@ namespace Opc.Ua.Core.Tests.Types.Encoders TestContext.Out.WriteLine("Expected:"); TestContext.Out.WriteLine(expected); Assert.IsNotNull(expected, "Expected DataValue is Null, " + encodeInfo); - using (var encoderStream = new MemoryStream()) + using (var encoderStream = CreateEncoderMemoryStream(memoryStreamType)) { using (IEncoder encoder = CreateEncoder(encoderType, Context, encoderStream, typeof(DataValue), useReversibleEncoding)) { @@ -164,6 +183,7 @@ namespace Opc.Ua.Core.Tests.Types.Encoders protected void EncodeDecodeDataValue( EncodingType encoderType, BuiltInType builtInType, + MemoryStreamType memoryStreamType, object data ) { @@ -176,7 +196,7 @@ namespace Opc.Ua.Core.Tests.Types.Encoders TestContext.Out.WriteLine(expected); byte[] buffer; - using (var encoderStream = new MemoryStream()) + using (var encoderStream = CreateEncoderMemoryStream(memoryStreamType)) { using (IEncoder encoder = CreateEncoder(encoderType, Context, encoderStream, typeof(DataValue))) { @@ -217,6 +237,7 @@ namespace Opc.Ua.Core.Tests.Types.Encoders protected void EncodeDecode( EncodingType encoderType, BuiltInType builtInType, + MemoryStreamType memoryStreamType, object expected ) { @@ -227,7 +248,7 @@ namespace Opc.Ua.Core.Tests.Types.Encoders TestContext.Out.WriteLine(expected); byte[] buffer; - using (var encoderStream = new MemoryStream()) + using (var encoderStream = CreateEncoderMemoryStream(memoryStreamType)) { using (IEncoder encoder = CreateEncoder(encoderType, Context, encoderStream, type)) { @@ -272,6 +293,7 @@ namespace Opc.Ua.Core.Tests.Types.Encoders /// protected void EncodeJsonVerifyResult( BuiltInType builtInType, + MemoryStreamType memoryStreamType, object data, bool useReversibleEncoding, string expected, @@ -300,7 +322,7 @@ namespace Opc.Ua.Core.Tests.Types.Encoders bool includeDefaultNumbers = isNumber ? includeDefaults : true; byte[] buffer; - using (var encoderStream = new MemoryStream()) + using (var encoderStream = CreateEncoderMemoryStream(memoryStreamType)) { using (IEncoder encoder = CreateEncoder(EncodingType.Json, Context, encoderStream, typeof(DataValue), useReversibleEncoding, topLevelIsArray, includeDefaultValues, includeDefaultNumbers)) @@ -379,16 +401,18 @@ namespace Opc.Ua.Core.Tests.Types.Encoders using (var stringWriter = new StringWriter()) using (var stringReader = new StringReader(json)) { - var jsonReader = new JsonTextReader(stringReader); - var jsonWriter = new JsonTextWriter(stringWriter) { + using (var jsonReader = new JsonTextReader(stringReader)) + using (var jsonWriter = new JsonTextWriter(stringWriter) { FloatFormatHandling = FloatFormatHandling.String, Formatting = Newtonsoft.Json.Formatting.Indented, Culture = System.Globalization.CultureInfo.InvariantCulture - }; - jsonWriter.WriteToken(jsonReader); - string formattedJson = stringWriter.ToString(); - TestContext.Out.WriteLine(formattedJson); - return formattedJson; + }) + { + jsonWriter.WriteToken(jsonReader); + string formattedJson = stringWriter.ToString(); + TestContext.Out.WriteLine(formattedJson); + return formattedJson; + } } } catch (Exception ex) @@ -399,6 +423,27 @@ namespace Opc.Ua.Core.Tests.Types.Encoders return json; } + /// + /// Returns various implementations of a memory stream. + /// + /// + /// A MemoryStream + /// + protected MemoryStream CreateEncoderMemoryStream(MemoryStreamType memoryStreamType) + { + switch (memoryStreamType) + { + case MemoryStreamType.MemoryStream: + return new MemoryStream(kTestBlockSize); + case MemoryStreamType.ArraySegmentStream: + return new ArraySegmentStream(BufferManager); + case MemoryStreamType.RecyclableMemoryStream: + return new RecyclableMemoryStream(RecyclableMemoryManager); + default: + throw new ArgumentOutOfRangeException(nameof(memoryStreamType), memoryStreamType, "Invalid MemoryStreamType specified."); + } + } + /// /// Encoder factory for all encoding types. /// @@ -418,13 +463,13 @@ namespace Opc.Ua.Core.Tests.Types.Encoders { case EncodingType.Binary: Assume.That(useReversibleEncoding, "Binary encoding only supports reversible option."); - return new BinaryEncoder(stream, context, false); + return new BinaryEncoder(stream, context, true); case EncodingType.Xml: Assume.That(useReversibleEncoding, "Xml encoding only supports reversible option."); var xmlWriter = XmlWriter.Create(stream); return new XmlEncoder(systemType, xmlWriter, context); case EncodingType.Json: - return new JsonEncoder(context, useReversibleEncoding, topLevelIsArray, stream) { + return new JsonEncoder(context, useReversibleEncoding, topLevelIsArray, stream, true) { IncludeDefaultValues = includeDefaultValues, IncludeDefaultNumberValues = includeDefaultNumbers }; diff --git a/Tests/Opc.Ua.Core.Tests/Types/Encoders/EncoderTests.cs b/Tests/Opc.Ua.Core.Tests/Types/Encoders/EncoderTests.cs index 840b4fced3..94fd0518b9 100644 --- a/Tests/Opc.Ua.Core.Tests/Types/Encoders/EncoderTests.cs +++ b/Tests/Opc.Ua.Core.Tests/Types/Encoders/EncoderTests.cs @@ -60,7 +60,7 @@ namespace Opc.Ua.Core.Tests.Types.Encoders ) { object defaultValue = TypeInfo.GetDefaultValue(builtInType); - EncodeDecodeDataValue(encoderType, builtInType, defaultValue); + EncodeDecodeDataValue(encoderType, builtInType, MemoryStreamType.MemoryStream, defaultValue); } /// @@ -76,7 +76,7 @@ namespace Opc.Ua.Core.Tests.Types.Encoders { Assume.That(builtInType != BuiltInType.DiagnosticInfo); object randomData = DataGenerator.GetRandom(builtInType); - EncodeDecodeDataValue(encoderType, builtInType, randomData); + EncodeDecodeDataValue(encoderType, builtInType, MemoryStreamType.ArraySegmentStream, randomData); } /// @@ -120,7 +120,7 @@ namespace Opc.Ua.Core.Tests.Types.Encoders break; } }; - EncodeDecode(encoderType, builtInType, randomData); + EncodeDecode(encoderType, builtInType, MemoryStreamType.ArraySegmentStream, randomData); } /// @@ -140,7 +140,7 @@ namespace Opc.Ua.Core.Tests.Types.Encoders // or encoding of extension objects fails. randomData = ExtensionObject.Null; } - EncodeDecode(encoderType, builtInType, randomData); + EncodeDecode(encoderType, builtInType, MemoryStreamType.RecyclableMemoryStream, randomData); } /// @@ -156,7 +156,7 @@ namespace Opc.Ua.Core.Tests.Types.Encoders Array boundaryValues = DataGenerator.GetRandomArray(builtInType, true, 10, true); foreach (var boundaryValue in boundaryValues) { - EncodeDecode(encoderType, builtInType, boundaryValue); + EncodeDecode(encoderType, builtInType, MemoryStreamType.MemoryStream, boundaryValue); } } @@ -176,7 +176,7 @@ namespace Opc.Ua.Core.Tests.Types.Encoders // ensure different sized arrays contain different data set SetRandomSeed(arrayLength); object randomData = DataGenerator.GetRandomArray(builtInType, useBoundaryValues, arrayLength, true); - EncodeDecodeDataValue(encoderType, builtInType, randomData); + EncodeDecodeDataValue(encoderType, builtInType, MemoryStreamType.ArraySegmentStream, randomData); } /// @@ -191,7 +191,7 @@ namespace Opc.Ua.Core.Tests.Types.Encoders ) { object randomData = DataGenerator.GetRandomArray(builtInType, false, 0, true); - EncodeDecodeDataValue(encoderType, builtInType, randomData); + EncodeDecodeDataValue(encoderType, builtInType, MemoryStreamType.RecyclableMemoryStream, randomData); } /// @@ -206,7 +206,7 @@ namespace Opc.Ua.Core.Tests.Types.Encoders { SetRepeatedRandomSeed(); object randomData = DataGenerator.GetRandom(BuiltInType.Variant); - EncodeDecodeDataValue(encoderType, BuiltInType.Variant, randomData); + EncodeDecodeDataValue(encoderType, BuiltInType.Variant, MemoryStreamType.MemoryStream, randomData); } /// @@ -224,11 +224,11 @@ namespace Opc.Ua.Core.Tests.Types.Encoders { Assert.Throws( typeof(ServiceResultException), - () => EncodeDataValue(EncodingType.Json, builtInType, randomData, false) + () => EncodeDataValue(EncodingType.Json, builtInType, MemoryStreamType.ArraySegmentStream, randomData, false) ); return; } - string json = EncodeDataValue(EncodingType.Json, builtInType, randomData, false); + string json = EncodeDataValue(EncodingType.Json, builtInType, MemoryStreamType.MemoryStream, randomData, false); PrettifyAndValidateJson(json); } @@ -246,7 +246,7 @@ namespace Opc.Ua.Core.Tests.Types.Encoders { SetRandomSeed(arrayLength); object randomData = DataGenerator.GetRandomArray(builtInType, useBoundaryValues, arrayLength, true); - string json = EncodeDataValue(EncodingType.Json, builtInType, randomData, false); + string json = EncodeDataValue(EncodingType.Json, builtInType, MemoryStreamType.RecyclableMemoryStream, randomData, false); PrettifyAndValidateJson(json); } @@ -261,7 +261,7 @@ namespace Opc.Ua.Core.Tests.Types.Encoders ) { object randomData = DataGenerator.GetRandomArray(builtInType, false, 0, true); - string json = EncodeDataValue(EncodingType.Json, builtInType, randomData, false); + string json = EncodeDataValue(EncodingType.Json, builtInType, MemoryStreamType.MemoryStream, randomData, false); PrettifyAndValidateJson(json); } @@ -284,7 +284,7 @@ namespace Opc.Ua.Core.Tests.Types.Encoders //new Variant(new TestEnumType[] { TestEnumType.One, TestEnumType.Two, TestEnumType.Hundred }), new Variant(new Int32[] { 2, 3, 10 }, new TypeInfo(BuiltInType.Enumeration, 1)) }; - EncodeDecodeDataValue(encoderType, BuiltInType.Variant, variant); + EncodeDecodeDataValue(encoderType, BuiltInType.Variant, MemoryStreamType.ArraySegmentStream, variant); } /// @@ -302,7 +302,7 @@ namespace Opc.Ua.Core.Tests.Types.Encoders int arrayDimension = RandomSource.NextInt32(99) + 1; Array randomData = DataGenerator.GetRandomArray(builtInType, false, arrayDimension, true); var variant = new Variant(randomData, new TypeInfo(builtInType, 1)); - EncodeDecodeDataValue(encoderType, BuiltInType.Variant, variant); + EncodeDecodeDataValue(encoderType, BuiltInType.Variant, MemoryStreamType.RecyclableMemoryStream, variant); } /// @@ -327,7 +327,7 @@ namespace Opc.Ua.Core.Tests.Types.Encoders TestContext.Out.WriteLine(randomData); byte[] buffer; - using (var encoderStream = new MemoryStream()) + using (var encoderStream = CreateEncoderMemoryStream(MemoryStreamType.MemoryStream)) { using (IEncoder encoder = CreateEncoder(encoderType, Context, encoderStream, type, true, false)) { @@ -383,7 +383,7 @@ namespace Opc.Ua.Core.Tests.Types.Encoders int elements = ElementsFromDimension(dimensions); Array randomData = DataGenerator.GetRandomArray(builtInType, false, elements, true); var variant = new Variant(new Matrix(randomData, builtInType, dimensions)); - EncodeDecodeDataValue(encoderType, BuiltInType.Variant, variant); + EncodeDecodeDataValue(encoderType, BuiltInType.Variant, MemoryStreamType.RecyclableMemoryStream, variant); } /// @@ -403,7 +403,7 @@ namespace Opc.Ua.Core.Tests.Types.Encoders int elements = ElementsFromDimension(dimensions); Array randomData = DataGenerator.GetRandomArray(builtInType, false, elements, true); var variant = new Variant(new Matrix(randomData, builtInType, dimensions)); - string json = EncodeDataValue(EncodingType.Json, BuiltInType.Variant, variant, false); + string json = EncodeDataValue(EncodingType.Json, BuiltInType.Variant, MemoryStreamType.ArraySegmentStream, variant, false); _ = PrettifyAndValidateJson(json); } @@ -432,7 +432,7 @@ namespace Opc.Ua.Core.Tests.Types.Encoders TestContext.Out.WriteLine(matrix); byte[] buffer; - using (var encoderStream = new MemoryStream()) + using (var encoderStream = CreateEncoderMemoryStream(MemoryStreamType.MemoryStream)) { using (IEncoder encoder = CreateEncoder(encoderType, Context, encoderStream, type)) { @@ -504,7 +504,7 @@ namespace Opc.Ua.Core.Tests.Types.Encoders TestContext.Out.WriteLine(expected); byte[] buffer; - using (var encoderStream = new MemoryStream()) + using (var encoderStream = CreateEncoderMemoryStream(MemoryStreamType.MemoryStream)) { using (IEncoder encoder = CreateEncoder(encoderType, Context, encoderStream, typeof(DataValue))) { @@ -595,7 +595,7 @@ namespace Opc.Ua.Core.Tests.Types.Encoders TestContext.Out.WriteLine(expected); byte[] buffer; - using (var encoderStream = new MemoryStream()) + using (var encoderStream = CreateEncoderMemoryStream(MemoryStreamType.ArraySegmentStream)) { using (IEncoder encoder = CreateEncoder(encoderType, Context, encoderStream, typeof(DataValue))) { @@ -656,8 +656,8 @@ namespace Opc.Ua.Core.Tests.Types.Encoders [Theory] [Category("Matrix")] public void EncodeMatrixInArrayOverflow( - EncodingType encoderType, - BuiltInType builtInType + EncodingType encoderType, + BuiltInType builtInType ) { Assume.That(builtInType != BuiltInType.Null); @@ -687,7 +687,7 @@ namespace Opc.Ua.Core.Tests.Types.Encoders TestContext.Out.WriteLine(matrix); byte[] buffer; - using (var encoderStream = new MemoryStream()) + using (var encoderStream = CreateEncoderMemoryStream(MemoryStreamType.RecyclableMemoryStream)) { using (IEncoder encoder = CreateEncoder(encoderType, Context, encoderStream, type)) { diff --git a/Tests/Opc.Ua.Core.Tests/Types/Encoders/JsonEncoderBenchmarks.cs b/Tests/Opc.Ua.Core.Tests/Types/Encoders/JsonEncoderBenchmarks.cs index c55495a079..e35d97709d 100644 --- a/Tests/Opc.Ua.Core.Tests/Types/Encoders/JsonEncoderBenchmarks.cs +++ b/Tests/Opc.Ua.Core.Tests/Types/Encoders/JsonEncoderBenchmarks.cs @@ -27,13 +27,8 @@ * http://opcfoundation.org/License/MIT/1.00/ * ======================================================================*/ -using System; -using System.Collections.Generic; -using System.Globalization; using System.IO; -using System.Runtime.CompilerServices; using System.Text; -using System.Threading; using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Diagnosers; using Microsoft.IO; @@ -47,57 +42,55 @@ namespace Opc.Ua.Core.Tests.Types.Encoders [NonParallelizable] [MemoryDiagnoser] [DisassemblyDiagnoser] - public class JsonEncoderBenchmarks + public class JsonEncoderBenchmarks : EncoderBenchmarks { - const int kBufferSize = 4096; - - [Params(1, 2, 8, 64)] - public int PayLoadSize { get; set; } = 64; - [Params(128, 512, 1024, 4096, 8192, 65536)] public int StreamSize { get; set; } = 1024; /// /// Benchmark overhead to create StreamWriter, MemoryStream is kept open. /// - [Benchmark] [Test] public void StreamWriter() { - using (var test = new StreamWriter(m_memoryStream, Encoding.UTF8, StreamSize, true)) + using (var memoryStream = new MemoryStream()) + using (var test = new StreamWriter(memoryStream, Encoding.UTF8, StreamSize, true)) + { test.Flush(); + } } /// /// Benchmark overhead to create StreamWriter and MemoryStream. /// - [Benchmark] [Test] public void StreamWriterRecyclableMemoryStream() { using (var memoryStream = new RecyclableMemoryStream(m_memoryManager)) using (var test = new StreamWriter(memoryStream, Encoding.UTF8, StreamSize)) + { test.Flush(); + } } /// /// Benchmark overhead to create StreamWriter and MemoryStream. /// - [Benchmark] [Test] public void StreamWriterMemoryStream() { using (var memoryStream = new MemoryStream()) using (var test = new StreamWriter(memoryStream, Encoding.UTF8, StreamSize)) + { test.Flush(); + } } /// /// Benchmark encoding with internal memory stream. /// - [Benchmark] [Test] - public void JsonEncoderConstructor2() + public void JsonEncoderConstructor() { using (var jsonEncoder = new JsonEncoder(m_context, false)) { @@ -106,67 +99,142 @@ namespace Opc.Ua.Core.Tests.Types.Encoders } } + /// + /// Test encoding with ArrayPool memory stream. + /// + [Theory] + public void JsonEncoderArraySegmentStreamTest(bool toText) + { + using (var memoryStream = new ArraySegmentStream(m_bufferManager, StreamBufferSize, 0, StreamBufferSize)) + { + TestStreamEncode(memoryStream, toText); + } + } + + /// + /// Test encoding with memory stream kept open. + /// + [Theory] + public void JsonEncoderMemoryStreamTest(bool toText) + { + using (var memoryStream = new MemoryStream(StreamBufferSize)) + { + TestStreamEncode(memoryStream, toText); + } + } + + /// + /// Test encoding with recyclable memory stream kept open. + /// + [Theory] + public void JsonEncoderRecyclableMemoryStream(bool toText) + { + using (var memoryStream = new RecyclableMemoryStream(m_memoryManager)) + { + TestStreamEncode(memoryStream, toText); + } + } + /// /// Benchmark encoding with memory stream kept open. /// [Benchmark] [Test] - public void JsonEncoderStreamLeaveOpenMemoryStream() + public void JsonEncoderMemoryStream() { - JsonEncoder_StreamLeaveOpen(m_memoryStream); - } - - /// - /// Benchmark encoding with recyclable memory stream kept open. - /// - [Benchmark] - [Test] - public void JsonEncoderStreamLeaveOpenRecyclableMemoryStream() - { - JsonEncoder_StreamLeaveOpen(m_recyclableMemoryStream); - } - - /// - /// Benchmark encoding with recyclable memory stream kept open. - /// - [Benchmark] - [Test] - public void JsonEncoderStreamLeaveOpenArraySegmentStream() - { - JsonEncoder_StreamLeaveOpen(m_arraySegmentStream); - } - - /// - /// Benchmark encoding with memory stream kept open, - /// use internal reflection to get string from memory stream. - /// - [Benchmark] - [Test] - public void JsonEncoderConstructorStreamwriterReflection2() - { - using (var jsonEncoder = new JsonEncoder(m_context, false, false, m_memoryStream, true, StreamSize)) + using (var memoryStream = new MemoryStream(StreamBufferSize)) { - TestEncoding(jsonEncoder); - var result = jsonEncoder.CloseAndReturnText(); + JsonEncoder_StreamLeaveOpen(memoryStream); + // get buffer for write + _ = memoryStream.ToArray(); + } + } + + /// + /// Benchmark encoding with recyclable memory stream kept open. + /// + [Benchmark] + [Test] + public void JsonEncoderRecyclableMemoryStream() + { + using (var recyclableMemoryStream = new RecyclableMemoryStream(m_memoryManager)) + { + JsonEncoder_StreamLeaveOpen(recyclableMemoryStream); + // get buffers for write + _ = recyclableMemoryStream.GetReadOnlySequence(); + } + } + + /// + /// Benchmark encoding with recyclable memory stream kept open. + /// + [Benchmark] + [Test] + public void JsonEncoderArraySegmentStream() + { + using (var arraySegmentStream = new ArraySegmentStream(m_bufferManager)) + { + JsonEncoder_StreamLeaveOpen(arraySegmentStream); + // get buffers and return them to buffer manager + var buffers = arraySegmentStream.GetBuffers("writer"); + foreach (var buffer in buffers) + { + m_bufferManager.ReturnBuffer(buffer.Array, "testreturn"); + } + } + } + + /// + /// Benchmark encoding with recyclable memory stream kept open. + /// + [Benchmark] + [Test] + public void JsonEncoderArraySegmentStreamNoSpan() + { +#if NET6_0_OR_GREATER + using (var arraySegmentStream = new ArraySegmentStreamNoSpan(m_bufferManager)) +#else + using (var arraySegmentStream = new ArraySegmentStream(m_bufferManager)) +#endif + { + JsonEncoder_StreamLeaveOpen(arraySegmentStream); + // get buffers and return them to buffer manager + var buffers = arraySegmentStream.GetBuffers("writer"); + foreach (var buffer in buffers) + { + m_bufferManager.ReturnBuffer(buffer.Array, "testreturn"); + } } } #region Private Methods - private void TestEncoding(IEncoder encoder) + private void TestStreamEncode(MemoryStream memoryStream, bool toArray) { - int payLoadSize = PayLoadSize; - encoder.WriteByte("Byte", 0); - while (--payLoadSize > 0) + using (var jsonEncoder = new JsonEncoder(m_context, false, false, memoryStream, true, StreamSize)) { - encoder.WriteBoolean("Boolean", true); - encoder.WriteUInt64("UInt64", 1234566890); - encoder.WriteString("String", "The quick brown fox..."); - encoder.WriteNodeId("NodeId", s_nodeId); - encoder.WriteInt32Array("Array", s_list); + TestEncoding(jsonEncoder); + _ = jsonEncoder.Close(); + } + using (var jsonEncoder = new JsonEncoder(m_context, false, false, memoryStream, true, StreamSize)) + { + TestEncoding(jsonEncoder); + if (toArray) + { + int length = jsonEncoder.Close(); + Assert.AreEqual(length, memoryStream.Position); + var result = memoryStream.ToArray(); + Assert.NotNull(result); + Assert.AreEqual(length, result.Length); + } + else + { + var result = jsonEncoder.CloseAndReturnText(); + Assert.NotNull(result); + } } } - private void JsonEncoder_StreamLeaveOpen(MemoryStream stream) + private void JsonEncoder_StreamLeaveOpen(MemoryStream stream, bool testResult = false) { int length1; int length2; @@ -181,39 +249,22 @@ namespace Opc.Ua.Core.Tests.Types.Encoders TestEncoding(jsonEncoder); length2 = jsonEncoder.Close(); } - var result = Encoding.UTF8.GetString(stream.ToArray()); - Assert.NotNull(result); - Assert.AreEqual(length1 * 2, length2); - Assert.AreEqual(length2, result.Length); + if (testResult) + { + var result = Encoding.UTF8.GetString(stream.ToArray()); + Assert.NotNull(result); + Assert.AreEqual(length1 * 2, length2); + Assert.AreEqual(length2, result.Length); + } } #endregion #region Test Setup [OneTimeSetUp] - public void OneTimeSetUp() - { - // for validating benchmark tests - m_context = new ServiceMessageContext(); - m_memoryStream = new MemoryStream(); - m_memoryManager = new RecyclableMemoryStreamManager(); - m_recyclableMemoryStream = new RecyclableMemoryStream(m_memoryManager); - m_bufferManager = new BufferManager(nameof(BinaryEncoder), kBufferSize); - m_arraySegmentStream = new ArraySegmentStream(m_bufferManager, kBufferSize, 0, kBufferSize); - } + public new void OneTimeSetUp() => base.OneTimeSetUp(); [OneTimeTearDown] - public void OneTimeTearDown() - { - m_context = null; - m_memoryStream.Dispose(); - m_memoryStream = null; - m_recyclableMemoryStream.Dispose(); - m_recyclableMemoryStream = null; - m_memoryManager = null; - m_bufferManager = null; - m_arraySegmentStream.Dispose(); - m_arraySegmentStream = null; - } + public new void OneTimeTearDown() => base.OneTimeTearDown(); #endregion #region Benchmark Setup @@ -221,983 +272,13 @@ namespace Opc.Ua.Core.Tests.Types.Encoders /// Set up some variables for benchmarks. /// [GlobalSetup] - public void GlobalSetup() - { - // for validating benchmark tests - m_context = new ServiceMessageContext(); - m_memoryStream = new MemoryStream(); - m_memoryManager = new RecyclableMemoryStreamManager(); - m_recyclableMemoryStream = new RecyclableMemoryStream(m_memoryManager); - m_bufferManager = new BufferManager(nameof(BinaryEncoder), kBufferSize); - m_arraySegmentStream = new ArraySegmentStream(m_bufferManager, kBufferSize, 0, kBufferSize); - } + public new void GlobalSetup() => base.GlobalSetup(); /// /// Tear down benchmark variables. /// [GlobalCleanup] - public void GlobalCleanup() - { - m_context = null; - m_memoryStream.Dispose(); - m_memoryStream = null; - m_recyclableMemoryStream.Dispose(); - m_recyclableMemoryStream = null; - m_memoryManager = null; - m_bufferManager = null; - m_arraySegmentStream.Dispose(); - m_arraySegmentStream = null; - } - #endregion - - #region Private Fields - private static NodeId s_nodeId = new NodeId(1234); - private static IList s_list = new List() { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; - private IServiceMessageContext m_context; - private MemoryStream m_memoryStream; - private RecyclableMemoryStreamManager m_memoryManager; - private RecyclableMemoryStream m_recyclableMemoryStream; - private BufferManager m_bufferManager; - private ArraySegmentStream m_arraySegmentStream; - #endregion - } - - [TestFixture, Category("JsonEncoder")] - [SetCulture("en-us"), SetUICulture("en-us")] - [NonParallelizable] - [MemoryDiagnoser] - [DisassemblyDiagnoser] - public class JsonEncoderDateTimeBenchmark - { - [Params(0, 4, 7)] - public int DateTimeOmittedZeros { get; set; } = 0; - - [Benchmark] - [Test] - public void DateTimeEncodeToString() - { - _ = m_dateTime.ToUniversalTime().ToString("yyyy-MM-dd'T'HH:mm:ss.FFFFFFFK", CultureInfo.InvariantCulture); - } - - [Benchmark] - [Test] - public void ConvertToUniversalTime() - { - _ = JsonEncoder.ConvertUniversalTimeToString(m_dateTime); - } - - #region Test Setup - [OneTimeSetUp] - public void OneTimeSetUp() - { - // for validating benchmark tests - m_dateTime = DateTime.UtcNow; - } - - [OneTimeTearDown] - public void OneTimeTearDown() - { - } - #endregion - - #region Benchmark Setup - /// - /// Set up some variables for benchmarks. - /// - [GlobalSetup] - public void GlobalSetup() - { - // for validating benchmark tests - switch (DateTimeOmittedZeros) - { - case 4: m_dateTime = new DateTime(2011, 11, 11, 11, 11, 11, 999, DateTimeKind.Utc); break; - case 7: m_dateTime = new DateTime(2011, 11, 11, 11, 11, 11, DateTimeKind.Utc); break; - default: - do - { - m_dateTime = DateTime.UtcNow; - } while (m_dateTime.Ticks % 10 == 0); - break; - } - } - - /// - /// Tear down benchmark variables. - /// - [GlobalCleanup] - public void GlobalCleanup() - { - } - #endregion - - #region Private Fields - private DateTime m_dateTime; - #endregion - } - - [TestFixture, Category("JsonEncoder")] - [SetCulture("en-us"), SetUICulture("en-us")] - [NonParallelizable] - [MemoryDiagnoser] - [DisassemblyDiagnoser(printSource: true)] - public class JsonEncoderEscapeStringBenchmarks - { - public const int InnerLoops = 100; - [DatapointSource] - [Params(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)] - public int StringVariantIndex { get; set; } = 1; - - [DatapointSource] - // for benchmarking with different escaped strings - public static readonly string[] EscapeTestStrings = - { - // The use case without escape characters, plain text - "The quick brown fox jumps over the lazy dog.", - // The use case with many control characters escaped, 1 char spaces - "\" \n \r \t \b \f \\ ", - // The use case with many control characters escaped, 2 char spaces - " \" \n \r \t \b \f \\ ", - // The use case with many control characters escaped, 3 char spaces - " \" \n \r \t \b \f \\ ", - // The use case with many control characters escaped, 5 char spaces - " \" \n \r \t \b \f \\ ", - // The use case with many binary characters escaped, 1 char spaces - "\0 \x01 \x02 \x03 \x04 ", - // The use case with many binary characters escaped, 2 char spaces - " \0 \x01 \x02 \x03 \x04 ", - // The use case with many binary characters escaped, 3 char spaces - " \0 \x01 \x02 \x03 \x04 ", - // The use case with many binary characters escaped, 5 char spaces - " \0 \x01 \x02 \x03 \x04 ", - // The use case with all escape characters and a long string - "Ascii characters, special characters \n \b & control characters \0 \x04 ␀ ␁ ␂ ␃ ␄. This is a test.", - }; - - /// - /// Benchmark encoding of the previous implementation. - /// - [Benchmark(Baseline = true)] - public void EscapeStringLegacy() - { - m_memoryStream.Position = 0; - int repeats = InnerLoops; - while (repeats-- > 0) - { - EscapedStringLegacy(m_testString); - } - m_streamWriter.Flush(); - } - - /// - /// Benchmark encoding of the previous implementation with snall improvement for binary encoding. - /// - [Benchmark] - public void EscapeStringLegacyPlus() - { - m_memoryStream.Position = 0; - int repeats = InnerLoops; - while (repeats-- > 0) - { - EscapedStringLegacyPlus(m_testString); - } - m_streamWriter.Flush(); - } - - /// - /// A new implementation using StringBuilder. - /// - [Benchmark] - public void EscapeStringStringBuilder() - { - m_memoryStream.Position = 0; - int repeats = InnerLoops; - while (repeats-- > 0) - { - EscapeString(m_testString); - } - m_streamWriter.Flush(); - } - - /// - /// A new implementation using ThreadLocal StringBuilder. - /// - [Benchmark] - public void EscapeStringThreadLocal() - { - m_memoryStream.Position = 0; - int repeats = InnerLoops; - while (repeats-- > 0) - { - EscapeStringThreadLocal(m_testString); - } - m_streamWriter.Flush(); - } - - /// - /// A new implementation using ReadOnlySpan. - /// - [Benchmark] - public void EscapeStringSpan() - { - m_memoryStream.Position = 0; - int repeats = InnerLoops; - while (repeats-- > 0) - { - EscapeStringSpan(m_testString); - } - m_streamWriter.Flush(); - } - - /// - /// A new implementation using ReadOnlySpan and char write. - /// - [Benchmark] - public void EscapeStringSpanChars() - { - m_memoryStream.Position = 0; - int repeats = InnerLoops; - while (repeats-- > 0) - { - EscapeStringSpanChars(m_testString); - } - m_streamWriter.Flush(); - } - - /// - /// A new implementation using ReadOnlySpan and char write. - /// - [Benchmark] - public void EscapeStringSpanCharsInline() - { - m_memoryStream.Position = 0; - int repeats = InnerLoops; - while (repeats-- > 0) - { - EscapeStringSpanCharsInline(m_testString); - } - m_streamWriter.Flush(); - } - - /// - /// A new implementation using ReadOnlySpan and char write with const arrays. - /// - [Benchmark] - public void EscapeStringSpanCharsInlineConst() - { - m_memoryStream.Position = 0; - int repeats = InnerLoops; - while (repeats-- > 0) - { - EscapeStringSpanCharsInlineConst(m_testString); - } - m_streamWriter.Flush(); - } - - /// - /// A new implementation using ReadOnlySpan and IndexOf. - /// - [Benchmark] - public void EscapeStringSpanIndex() - { - m_memoryStream.Position = 0; - int repeats = InnerLoops; - while (repeats-- > 0) - { - EscapeStringSpanIndex(m_testString); - } - m_streamWriter.Flush(); - } - - /// - /// A new implementation using ReadOnlySpan and Dictionary. - /// - [Benchmark] - public void EscapeStringSpanDict() - { - m_memoryStream.Position = 0; - int repeats = InnerLoops; - while (repeats-- > 0) - { - EscapeStringSpanDict(m_testString); - } - m_streamWriter.Flush(); - } - - [Theory] - [TestCase("No Escape chars", 0)] - [TestCase("control chars escaped, 1 char space", 1)] - [TestCase("control chars escaped, 2 char spaces", 2)] - [TestCase("control chars escaped, 3 char spaces", 3)] - [TestCase("control chars escaped, 5 char spaces", 4)] - [TestCase("binary chars escaped, 1 char space", 5)] - [TestCase("binary chars escaped, 2 char spaces", 6)] - [TestCase("binary chars escaped, 3 char spaces", 7)] - [TestCase("binary chars escaped, 5 char spaces", 8)] - [TestCase("all escape chars and long string", 9)] - public void EscapeStringValidation(string name, int index) - { - m_testString = EscapeTestStrings[index]; - TestContext.Out.WriteLine(m_testString); - var testArray = m_testString.ToCharArray(); - - m_memoryStream.Position = 0; - EscapeStringLegacy(); - m_streamWriter.Flush(); - byte[] resultLegacy = m_memoryStream.ToArray(); - TestContext.Out.WriteLine(Encoding.UTF8.GetString(resultLegacy)); - - m_memoryStream.Position = 0; - EscapeStringLegacyPlus(); - m_streamWriter.Flush(); - byte[] resultLegacyPlus = m_memoryStream.ToArray(); - TestContext.Out.WriteLine(Encoding.UTF8.GetString(resultLegacyPlus)); - - m_memoryStream.Position = 0; - EscapeStringStringBuilder(); - m_streamWriter.Flush(); - byte[] result = m_memoryStream.ToArray(); - TestContext.Out.WriteLine(Encoding.UTF8.GetString(result)); - - m_memoryStream.Position = 0; - EscapeStringThreadLocal(); - m_streamWriter.Flush(); - byte[] resultThreadLocal = m_memoryStream.ToArray(); - TestContext.Out.WriteLine(Encoding.UTF8.GetString(resultThreadLocal)); - - m_memoryStream.Position = 0; - EscapeStringSpan(m_testString); - m_streamWriter.Flush(); - byte[] resultSpan = m_memoryStream.ToArray(); - TestContext.Out.WriteLine(Encoding.UTF8.GetString(resultSpan)); - - m_memoryStream.Position = 0; - EscapeStringSpanChars(m_testString); - m_streamWriter.Flush(); - byte[] resultSpanChars = m_memoryStream.ToArray(); - TestContext.Out.WriteLine(Encoding.UTF8.GetString(resultSpanChars)); - - m_memoryStream.Position = 0; - EscapeStringSpanCharsInline(m_testString); - m_streamWriter.Flush(); - byte[] resultSpanCharsInline = m_memoryStream.ToArray(); - TestContext.Out.WriteLine(Encoding.UTF8.GetString(resultSpanCharsInline)); - - m_memoryStream.Position = 0; - EscapeStringSpanCharsInlineConst(m_testString); - m_streamWriter.Flush(); - byte[] resultSpanCharsInlineConst = m_memoryStream.ToArray(); - TestContext.Out.WriteLine(Encoding.UTF8.GetString(resultSpanCharsInlineConst)); - - m_memoryStream.Position = 0; - EscapeStringSpanIndex(m_testString); - m_streamWriter.Flush(); - byte[] resultSpanIndex = m_memoryStream.ToArray(); - TestContext.Out.WriteLine(Encoding.UTF8.GetString(resultSpanIndex)); - - m_memoryStream.Position = 0; - EscapeStringSpanDict(m_testString); - m_streamWriter.Flush(); - byte[] resultSpanDict = m_memoryStream.ToArray(); - TestContext.Out.WriteLine(Encoding.UTF8.GetString(resultSpanDict)); - - Assert.IsTrue(Utils.IsEqual(resultLegacy, result)); - Assert.IsTrue(Utils.IsEqual(resultLegacy, resultLegacyPlus)); - Assert.IsTrue(Utils.IsEqual(resultLegacy, resultThreadLocal)); - Assert.IsTrue(Utils.IsEqual(resultLegacy, resultSpan)); - Assert.IsTrue(Utils.IsEqual(resultLegacy, resultSpanChars)); - Assert.IsTrue(Utils.IsEqual(resultLegacy, resultSpanCharsInline)); - Assert.IsTrue(Utils.IsEqual(resultLegacy, resultSpanCharsInlineConst)); - Assert.IsTrue(Utils.IsEqual(resultLegacy, resultSpanIndex)); - Assert.IsTrue(Utils.IsEqual(resultLegacy, resultSpanDict)); - } - - #region Test Setup - [OneTimeSetUp] - public void OneTimeSetUp() - { - m_memoryManager = new RecyclableMemoryStreamManager(); - m_memoryStream = new RecyclableMemoryStream(m_memoryManager); - m_streamWriter = new StreamWriter(m_memoryStream, new UTF8Encoding(false), m_streamSize, false); - } - - [OneTimeTearDown] - public void OneTimeTearDown() - { - m_streamWriter?.Dispose(); - m_streamWriter = null; - m_memoryStream?.Dispose(); - m_memoryStream = null; - m_memoryManager = null; - } - #endregion - - #region Benchmark Setup - /// 4 - /// Set up some variables for benchmarks. - /// - [GlobalSetup] - public void GlobalSetup() - { - m_memoryManager = new RecyclableMemoryStreamManager(); - m_memoryStream = new RecyclableMemoryStream(m_memoryManager); - m_streamWriter = new StreamWriter(m_memoryStream, new UTF8Encoding(false), m_streamSize, false); - m_testString = EscapeTestStrings[StringVariantIndex - 1]; - } - - [GlobalCleanup] - public void GlobalCleanup() - { - m_streamWriter?.Dispose(); - m_streamWriter = null; - m_memoryStream?.Dispose(); - m_memoryStream = null; - m_memoryManager = null; - } - #endregion - - #region Private Methods - /// - /// Version used previously in JsonEncoder. - /// - private void EscapedStringLegacy(string value) - { - foreach (char ch in value) - { - bool found = false; - - for (int ii = 0; ii < m_specialChars.Length; ii++) - { - if (m_specialChars[ii] == ch) - { - m_streamWriter.Write('\\'); - m_streamWriter.Write(m_substitution[ii]); - found = true; - break; - } - } - - if (!found) - { - if (ch < 32) - { - m_streamWriter.Write("\\u"); - m_streamWriter.Write("{0:X4}", (int)ch); - continue; - } - - m_streamWriter.Write(ch); - } - } - } - - /// - /// Version used previously in JsonEncoder plus improvement of binary encoding. - /// - /// - /// For the underlying stream writer it is faster to write two chars than a 2 char string. - /// - private void EscapedStringLegacyPlus(string value) - { - foreach (char ch in value) - { - bool found = false; - - for (int ii = 0; ii < m_specialChars.Length; ii++) - { - if (m_specialChars[ii] == ch) - { - m_streamWriter.Write('\\'); - m_streamWriter.Write(m_substitution[ii]); - found = true; - break; - } - } - - if (!found) - { - if (ch < 32) - { - m_streamWriter.Write('\\'); - m_streamWriter.Write('u'); - m_streamWriter.Write(((int)ch).ToString("X4", CultureInfo.InvariantCulture)); - continue; - } - - m_streamWriter.Write(ch); - } - } - } - - /// - /// Using a span to escape the string, write strings to stream writer if possible. - /// - private void EscapeStringSpan(string value) - { - ReadOnlySpan charSpan = value.AsSpan(); - int lastOffset = 0; - - for (int i = 0; i < charSpan.Length; i++) - { - bool found = false; - char ch = charSpan[i]; - - for (int ii = 0; ii < m_specialChars.Length; ii++) - { - if (m_specialChars[ii] == ch) - { - if (lastOffset < i) - { -#if NETCOREAPP2_1_OR_GREATER - m_streamWriter.Write(charSpan.Slice(lastOffset, i - lastOffset)); -#else - m_streamWriter.Write(charSpan.Slice(lastOffset, i - lastOffset).ToString()); -#endif - } - lastOffset = i + 1; - m_streamWriter.Write(m_substitutionStrings[ii]); - found = true; - break; - } - } - - if (!found && ch < 32) - { - if (lastOffset < i) - { -#if NETCOREAPP2_1_OR_GREATER - m_streamWriter.Write(charSpan.Slice(lastOffset, i - lastOffset)); -#else - m_streamWriter.Write(charSpan.Slice(lastOffset, i - lastOffset).ToString()); -#endif - } - lastOffset = i + 1; - m_streamWriter.Write("\\u"); - m_streamWriter.Write(((int)ch).ToString("X4", CultureInfo.InvariantCulture)); - } - } - if (lastOffset == 0) - { - m_streamWriter.Write(value); - } - else if (lastOffset < charSpan.Length) - { -#if NETCOREAPP2_1_OR_GREATER - m_streamWriter.Write(charSpan.Slice(lastOffset, charSpan.Length - lastOffset)); -#else - m_streamWriter.Write(charSpan.Slice(lastOffset, charSpan.Length - lastOffset).ToString()); -#endif - } - } - - /// - /// Use span to escape the string, write only chars to stream writer. - /// - /// - private void EscapeStringSpanChars(string value) - { - ReadOnlySpan charSpan = value.AsSpan(); - - int lastOffset = 0; - for (int i = 0; i < charSpan.Length; i++) - { - bool found = false; - char ch = charSpan[i]; - - for (int ii = 0; ii < m_specialChars.Length; ii++) - { - if (m_specialChars[ii] == ch) - { - if (lastOffset < i) - { -#if NETCOREAPP2_1_OR_GREATER - m_streamWriter.Write(charSpan.Slice(lastOffset, i - lastOffset)); -#else - m_streamWriter.Write(charSpan.Slice(lastOffset, i - lastOffset).ToString()); -#endif - } - lastOffset = i + 1; - m_streamWriter.Write('\\'); - m_streamWriter.Write(m_substitution[ii]); - found = true; - break; - } - } - - if (!found && ch < 32) - { - if (lastOffset < i - 1) - { -#if NETCOREAPP2_1_OR_GREATER - m_streamWriter.Write(charSpan.Slice(lastOffset, i - lastOffset)); -#else - m_streamWriter.Write(charSpan.Slice(lastOffset, i - lastOffset).ToString()); -#endif - } - else - { - while (lastOffset < i) - { - m_streamWriter.Write(charSpan[lastOffset++]); - } - } - lastOffset = i + 1; - m_streamWriter.Write('\\'); - m_streamWriter.Write('u'); - m_streamWriter.Write(((int)ch).ToString("X4", CultureInfo.InvariantCulture)); - } - } - - if (lastOffset == 0) - { -#if NETCOREAPP2_1_OR_GREATER - m_streamWriter.Write(charSpan); -#else - m_streamWriter.Write(value); -#endif - } - else if (lastOffset < charSpan.Length) - { -#if NETCOREAPP2_1_OR_GREATER - m_streamWriter.Write(charSpan.Slice(lastOffset, charSpan.Length - lastOffset)); -#else - m_streamWriter.Write(charSpan.Slice(lastOffset, charSpan.Length - lastOffset).ToString()); -#endif - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void WriteSpan(ref int lastOffset, ReadOnlySpan valueSpan, int index) - { - if (lastOffset < index - 2) - { -#if NETCOREAPP2_1_OR_GREATER - m_streamWriter.Write(valueSpan.Slice(lastOffset, index - lastOffset)); -#else - m_streamWriter.Write(valueSpan.Slice(lastOffset, index - lastOffset).ToString()); -#endif - } - else - { - while (lastOffset < index) - { - m_streamWriter.Write(valueSpan[lastOffset++]); - } - } - lastOffset = index + 1; - } - - /// - /// Write only chars to stream writer, inline the write sequence for readability. - /// - /// - private void EscapeStringSpanCharsInline(string value) - { - ReadOnlySpan charSpan = value.AsSpan(); - int lastOffset = 0; - - for (int i = 0; i < charSpan.Length; i++) - { - bool found = false; - char ch = charSpan[i]; - - for (int ii = 0; ii < m_specialChars.Length; ii++) - { - if (m_specialChars[ii] == ch) - { - WriteSpan(ref lastOffset, charSpan, i); - m_streamWriter.Write('\\'); - m_streamWriter.Write(m_substitution[ii]); - found = true; - break; - } - } - - if (!found && ch < 32) - { - WriteSpan(ref lastOffset, charSpan, i); - m_streamWriter.Write('\\'); - m_streamWriter.Write('u'); - m_streamWriter.Write(((int)ch).ToString("X4", CultureInfo.InvariantCulture)); - } - } - - if (lastOffset == 0) - { -#if NETCOREAPP2_1_OR_GREATER - m_streamWriter.Write(charSpan); -#else - m_streamWriter.Write(value); -#endif - } - else - { - WriteSpan(ref lastOffset, charSpan, charSpan.Length); - } - } - - // create version of EscapeStringSpanCharsInline that references cosnt arrays - private void EscapeStringSpanCharsInlineConst(string value) - { - ReadOnlySpan charSpan = value.AsSpan(); - int lastOffset = 0; - - for (int i = 0; i < charSpan.Length; i++) - { - bool found = false; - char ch = charSpan[i]; - - for (int ii = 0; ii < m_specialCharsConst.Length; ii++) - { - if (m_specialCharsConst[ii] == ch) - { - WriteSpan(ref lastOffset, charSpan, i); - m_streamWriter.Write('\\'); - m_streamWriter.Write(m_substitutionConst[ii]); - found = true; - break; - } - } - - if (!found && ch < 32) - { - WriteSpan(ref lastOffset, charSpan, i); - m_streamWriter.Write('\\'); - m_streamWriter.Write('u'); - m_streamWriter.Write(((int)ch).ToString("X4", CultureInfo.InvariantCulture)); - } - } - - if (lastOffset == 0) - { - m_streamWriter.Write(value); - } - else - { - WriteSpan(ref lastOffset, charSpan, charSpan.Length); - } - } - - - private void EscapeStringSpanIndex(string value) - { - ReadOnlySpan charSpan = value.AsSpan(); - - int lastOffset = 0; - for (int i = 0; i < charSpan.Length; i++) - { - char ch = charSpan[i]; - - int index = m_specialString.IndexOf(ch); - if (index >= 0) - { - if (lastOffset < i) - { -#if NETCOREAPP2_1_OR_GREATER - m_streamWriter.Write(charSpan.Slice(lastOffset, i - lastOffset)); -#else - m_streamWriter.Write(charSpan.Slice(lastOffset, i - lastOffset).ToString()); -#endif - } - lastOffset = i + 1; - m_streamWriter.Write(m_substitutionStrings[index]); - continue; - } - - if (ch < 32) - { - if (lastOffset < i) - { -#if NETCOREAPP2_1_OR_GREATER - m_streamWriter.Write(charSpan.Slice(lastOffset, i - lastOffset)); -#else - m_streamWriter.Write(charSpan.Slice(lastOffset, i - lastOffset).ToString()); -#endif - } - lastOffset = i + 1; - m_streamWriter.Write('\\'); - m_streamWriter.Write('u'); - m_streamWriter.Write(((int)ch).ToString("X4", CultureInfo.InvariantCulture)); - } - } - if (lastOffset == 0) - { - m_streamWriter.Write(value); - } - else if (lastOffset < charSpan.Length) - { -#if NETCOREAPP2_1_OR_GREATER - m_streamWriter.Write(charSpan.Slice(lastOffset, charSpan.Length - lastOffset)); -#else - m_streamWriter.Write(charSpan.Slice(lastOffset, charSpan.Length - lastOffset).ToString()); -#endif - } - } - - private void EscapeStringSpanDict(string value) - { - ReadOnlySpan charSpan = value.AsSpan(); - - int lastOffset = 0; - for (int i = 0; i < charSpan.Length; i++) - { - char ch = charSpan[i]; - - if (m_replace.TryGetValue(ch, out string escapeSequence)) - { - if (lastOffset < i) - { -#if NETCOREAPP2_1_OR_GREATER - m_streamWriter.Write(charSpan.Slice(lastOffset, i - lastOffset)); -#else - m_streamWriter.Write(charSpan.Slice(lastOffset, i - lastOffset).ToString()); -#endif - } - lastOffset = i + 1; - m_streamWriter.Write(escapeSequence); - continue; - } - - if (ch < 32) - { - if (lastOffset < i) - { -#if NETCOREAPP2_1_OR_GREATER - m_streamWriter.Write(charSpan.Slice(lastOffset, i - lastOffset)); -#else - m_streamWriter.Write(charSpan.Slice(lastOffset, i - lastOffset).ToString()); -#endif - } - lastOffset = i + 1; - m_streamWriter.Write('\\'); - m_streamWriter.Write('u'); - m_streamWriter.Write(((int)ch).ToString("X4", CultureInfo.InvariantCulture)); - } - } - if (lastOffset == 0) - { - m_streamWriter.Write(value); - } - else if (lastOffset < charSpan.Length) - { -#if NETCOREAPP2_1_OR_GREATER - m_streamWriter.Write(charSpan.Slice(lastOffset, charSpan.Length - lastOffset)); -#else - m_streamWriter.Write(charSpan.Slice(lastOffset, charSpan.Length - lastOffset).ToString()); -#endif - } - } - - private void EscapeString(string value) - { - StringBuilder stringBuilder = new StringBuilder(value.Length * 2); - - foreach (char ch in value) - { - if (m_replace.TryGetValue(ch, out string escapeSequence)) - { - stringBuilder.Append(escapeSequence); - } - else if (ch < 32) - { - stringBuilder.Append("\\u"); - stringBuilder.Append(((int)ch).ToString("X4", CultureInfo.InvariantCulture)); - } - else - { - stringBuilder.Append(ch); - } - } - m_streamWriter.Write(stringBuilder); - } - - private void EscapeStringThreadLocal(string value) - { - StringBuilder stringBuilder = m_stringBuilderPool.Value; - stringBuilder.Clear(); - stringBuilder.EnsureCapacity(value.Length * 2); - - foreach (char ch in value) - { - if (m_replace.TryGetValue(ch, out string escapeSequence)) - { - stringBuilder.Append(escapeSequence); - } - else if (ch < 32) - { - stringBuilder.Append("\\u"); - stringBuilder.Append(((int)ch).ToString("X4", CultureInfo.InvariantCulture)); - } - else - { - stringBuilder.Append(ch); - } - } - m_streamWriter.Write(stringBuilder); - } - #endregion - - #region Private Fields - private ThreadLocal m_stringBuilderPool = new ThreadLocal(() => new StringBuilder()); - private static string m_testString; - private RecyclableMemoryStreamManager m_memoryManager; - private RecyclableMemoryStream m_memoryStream; - private StreamWriter m_streamWriter; - private int m_streamSize = 1024; - private static readonly string m_specialString = "\"\\\n\r\t\b\f"; - - // Declare static readonly characters for the special characters - private static readonly char sro_quotation = '\"'; - private static readonly char sro_backslash = '\\'; - private static readonly char sro_newline = '\n'; - private static readonly char sro_return = '\r'; - private static readonly char sro_tab = '\t'; - private static readonly char sro_backspace = '\b'; - private static readonly char sro_formfeed = '\f'; - private static readonly char[] m_specialChars = new char[] { sro_quotation, sro_backslash, sro_newline, sro_return, sro_tab, sro_backspace, sro_formfeed }; - - // Declare static readonly characters for the substitution characters - private static readonly char sro_quotationSub = '\"'; - private static readonly char sro_backslashSub = '\\'; - private static readonly char sro_newlineSub = 'n'; - private static readonly char sro_returnSub = 'r'; - private static readonly char sro_tabSub = 't'; - private static readonly char sro_backspaceSub = 'b'; - private static readonly char sro_formfeedSub = 'f'; - private static readonly char[] m_substitution = new char[] { sro_quotationSub, sro_backslashSub, sro_newlineSub, sro_returnSub, sro_tabSub, sro_backspaceSub, sro_formfeedSub }; - - // Special characters as const - private const char s_quotation = '\"'; - private const char s_backslash = '\\'; - private const char s_newline = '\n'; - private const char s_return = '\r'; - private const char s_tab = '\t'; - private const char s_backspace = '\b'; - private const char s_formfeed = '\f'; - - private static readonly char[] m_specialCharsConst = new char[] { s_quotation, s_backslash, s_newline, s_return, s_tab, s_backspace, s_formfeed }; - - // Substitution as const - private const char s_quotationSub = '\"'; - private const char s_backslashSub = '\\'; - private const char s_newlineSub = 'n'; - private const char s_returnSub = 'r'; - private const char s_tabSub = 't'; - private const char s_backspaceSub = 'b'; - private const char s_formfeedSub = 'f'; - - private static readonly char[] m_substitutionConst = new char[] { s_quotationSub, s_backslashSub, s_newlineSub, s_returnSub, s_tabSub, s_backspaceSub, s_formfeedSub }; - - private static readonly string[] m_substitutionStrings = new string[] { "\\\"", "\\\\", "\\n", "\\r", "\\t", "\\b", "\\f" }; - private static readonly Dictionary m_replace = new Dictionary - { - { '\"', "\\\"" }, - { '\\', "\\\\" }, - { '\n', "\\n" }, - { '\r', "\\r" }, - { '\t', "\\t" }, - { '\b', "\\b" }, - { '\f', "\\f" } - }; + public new void GlobalCleanup() => base.GlobalCleanup(); #endregion } } diff --git a/Tests/Opc.Ua.Core.Tests/Types/Encoders/JsonEncoderDateTimeBenchmark.cs b/Tests/Opc.Ua.Core.Tests/Types/Encoders/JsonEncoderDateTimeBenchmark.cs new file mode 100644 index 0000000000..08a97d4382 --- /dev/null +++ b/Tests/Opc.Ua.Core.Tests/Types/Encoders/JsonEncoderDateTimeBenchmark.cs @@ -0,0 +1,110 @@ +/* ======================================================================== + * Copyright (c) 2005-2024 The OPC Foundation, Inc. All rights reserved. + * + * OPC Foundation MIT License 1.00 + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + * + * The complete license agreement can be found here: + * http://opcfoundation.org/License/MIT/1.00/ + * ======================================================================*/ + +using System; +using System.Globalization; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Diagnosers; +using NUnit.Framework; + +namespace Opc.Ua.Core.Tests.Types.Encoders +{ + [TestFixture, Category("JsonEncoder")] + [SetCulture("en-us"), SetUICulture("en-us")] + [NonParallelizable] + [MemoryDiagnoser] + [DisassemblyDiagnoser] + public class JsonEncoderDateTimeBenchmark + { + [Params(0, 4, 7)] + public int DateTimeOmittedZeros { get; set; } = 0; + + [Benchmark] + [Test] + public void DateTimeEncodeToString() + { + _ = m_dateTime.ToUniversalTime().ToString("yyyy-MM-dd'T'HH:mm:ss.FFFFFFFK", CultureInfo.InvariantCulture); + } + + [Benchmark] + [Test] + public void ConvertToUniversalTime() + { + _ = JsonEncoder.ConvertUniversalTimeToString(m_dateTime); + } + + #region Test Setup + [OneTimeSetUp] + public void OneTimeSetUp() + { + // for validating benchmark tests + m_dateTime = DateTime.UtcNow; + } + + [OneTimeTearDown] + public void OneTimeTearDown() + { + } + #endregion + + #region Benchmark Setup + /// + /// Set up some variables for benchmarks. + /// + [GlobalSetup] + public void GlobalSetup() + { + // for validating benchmark tests + switch (DateTimeOmittedZeros) + { + case 4: m_dateTime = new DateTime(2011, 11, 11, 11, 11, 11, 999, DateTimeKind.Utc); break; + case 7: m_dateTime = new DateTime(2011, 11, 11, 11, 11, 11, DateTimeKind.Utc); break; + default: + do + { + m_dateTime = DateTime.UtcNow; + } while (m_dateTime.Ticks % 10 == 0); + break; + } + } + + /// + /// Tear down benchmark variables. + /// + [GlobalCleanup] + public void GlobalCleanup() + { + } + #endregion + + #region Private Fields + private DateTime m_dateTime; + #endregion + } +} diff --git a/Tests/Opc.Ua.Core.Tests/Types/Encoders/JsonEncoderEscapeStringBenchmarks.cs b/Tests/Opc.Ua.Core.Tests/Types/Encoders/JsonEncoderEscapeStringBenchmarks.cs new file mode 100644 index 0000000000..c1746fd4d5 --- /dev/null +++ b/Tests/Opc.Ua.Core.Tests/Types/Encoders/JsonEncoderEscapeStringBenchmarks.cs @@ -0,0 +1,860 @@ +/* ======================================================================== + * Copyright (c) 2005-2024 The OPC Foundation, Inc. All rights reserved. + * + * OPC Foundation MIT License 1.00 + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + * + * The complete license agreement can be found here: + * http://opcfoundation.org/License/MIT/1.00/ + * ======================================================================*/ + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Runtime.CompilerServices; +using System.Text; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Diagnosers; +using Microsoft.IO; +using NUnit.Framework; + +namespace Opc.Ua.Core.Tests.Types.Encoders +{ + [TestFixture, Category("JsonEncoder")] + [SetCulture("en-us"), SetUICulture("en-us")] + [NonParallelizable] + [MemoryDiagnoser] + [DisassemblyDiagnoser(printSource: true)] + public class JsonEncoderEscapeStringBenchmarks + { + public const int InnerLoops = 100; + [DatapointSource] + [Params(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)] + public int StringVariantIndex { get; set; } = 1; + + [DatapointSource] + // for benchmarking with different escaped strings + public static readonly string[] EscapeTestStrings = + { + // The use case without escape characters, plain text + "The quick brown fox jumps over the lazy dog.", + // The use case with many control characters escaped, 1 char spaces + "\" \n \r \t \b \f \\ ", + // The use case with many control characters escaped, 2 char spaces + " \" \n \r \t \b \f \\ ", + // The use case with many control characters escaped, 3 char spaces + " \" \n \r \t \b \f \\ ", + // The use case with many control characters escaped, 5 char spaces + " \" \n \r \t \b \f \\ ", + // The use case with many binary characters escaped, 1 char spaces + "\0 \x01 \x02 \x03 \x04 ", + // The use case with many binary characters escaped, 2 char spaces + " \0 \x01 \x02 \x03 \x04 ", + // The use case with many binary characters escaped, 3 char spaces + " \0 \x01 \x02 \x03 \x04 ", + // The use case with many binary characters escaped, 5 char spaces + " \0 \x01 \x02 \x03 \x04 ", + // The use case with all escape characters and a long string + "Ascii characters, special characters \n \b & control characters \0 \x04 ␀ ␁ ␂ ␃ ␄. This is a test.", + }; + + /// + /// Benchmark encoding of the previous implementation. + /// + [Benchmark(Baseline = true)] + public void EscapeStringLegacy() + { + m_memoryStream.Position = 0; + int repeats = InnerLoops; + while (repeats-- > 0) + { + EscapedStringLegacy(m_testString); + } + m_streamWriter.Flush(); + } + + /// + /// Benchmark encoding of the previous implementation with snall improvement for binary encoding. + /// + [Benchmark] + public void EscapeStringLegacyPlus() + { + m_memoryStream.Position = 0; + int repeats = InnerLoops; + while (repeats-- > 0) + { + EscapedStringLegacyPlus(m_testString); + } + m_streamWriter.Flush(); + } + + /// + /// A new implementation using StringBuilder. + /// + [Benchmark] + public void EscapeStringStringBuilder() + { + m_memoryStream.Position = 0; + int repeats = InnerLoops; + while (repeats-- > 0) + { + EscapeString(m_testString); + } + m_streamWriter.Flush(); + } + + /// + /// A new implementation using ReadOnlySpan. + /// + [Benchmark] + public void EscapeStringSpan() + { + m_memoryStream.Position = 0; + int repeats = InnerLoops; + while (repeats-- > 0) + { + EscapeStringSpan(m_testString); + } + m_streamWriter.Flush(); + } + + /// + /// A new implementation using ReadOnlySpan and char write. + /// + [Benchmark] + public void EscapeStringSpanChars() + { + m_memoryStream.Position = 0; + int repeats = InnerLoops; + while (repeats-- > 0) + { + EscapeStringSpanChars(m_testString); + } + m_streamWriter.Flush(); + } + + /// + /// A new implementation using ReadOnlySpan and char write. + /// + [Benchmark] + public void EscapeStringSpanCharsInline() + { + m_memoryStream.Position = 0; + int repeats = InnerLoops; + while (repeats-- > 0) + { + EscapeStringSpanCharsInline(m_testString); + } + m_streamWriter.Flush(); + } + + /// + /// A new implementation using ReadOnlySpan and char write with const arrays. + /// + [Benchmark] + public void EscapeStringSpanCharsInlineConst() + { + m_memoryStream.Position = 0; + int repeats = InnerLoops; + while (repeats-- > 0) + { + EscapeStringSpanCharsInlineConst(m_testString); + } + m_streamWriter.Flush(); + } + + /// + /// A new implementation using ReadOnlySpan and IndexOf. + /// + [Benchmark] + public void EscapeStringSpanIndex() + { + m_memoryStream.Position = 0; + int repeats = InnerLoops; + while (repeats-- > 0) + { + EscapeStringSpanIndex(m_testString); + } + m_streamWriter.Flush(); + } + + /// + /// A new implementation using ReadOnlySpan and Dictionary. + /// + [Benchmark] + public void EscapeStringSpanDict() + { + m_memoryStream.Position = 0; + int repeats = InnerLoops; + while (repeats-- > 0) + { + EscapeStringSpanDict(m_testString); + } + m_streamWriter.Flush(); + } + + [Theory] + [TestCase("No Escape chars", 0)] + [TestCase("control chars escaped, 1 char space", 1)] + [TestCase("control chars escaped, 2 char spaces", 2)] + [TestCase("control chars escaped, 3 char spaces", 3)] + [TestCase("control chars escaped, 5 char spaces", 4)] + [TestCase("binary chars escaped, 1 char space", 5)] + [TestCase("binary chars escaped, 2 char spaces", 6)] + [TestCase("binary chars escaped, 3 char spaces", 7)] + [TestCase("binary chars escaped, 5 char spaces", 8)] + [TestCase("all escape chars and long string", 9)] + public void EscapeStringValidation(string name, int index) + { + m_testString = EscapeTestStrings[index]; + TestContext.Out.WriteLine(m_testString); + var testArray = m_testString.ToCharArray(); + + m_memoryStream.Position = 0; + EscapeStringLegacy(); + m_streamWriter.Flush(); + byte[] resultLegacy = m_memoryStream.ToArray(); + TestContext.Out.WriteLine(Encoding.UTF8.GetString(resultLegacy)); + + m_memoryStream.Position = 0; + EscapeStringLegacyPlus(); + m_streamWriter.Flush(); + byte[] resultLegacyPlus = m_memoryStream.ToArray(); + TestContext.Out.WriteLine(Encoding.UTF8.GetString(resultLegacyPlus)); + + m_memoryStream.Position = 0; + EscapeStringStringBuilder(); + m_streamWriter.Flush(); + byte[] result = m_memoryStream.ToArray(); + TestContext.Out.WriteLine(Encoding.UTF8.GetString(result)); + + m_memoryStream.Position = 0; + EscapeStringSpan(m_testString); + m_streamWriter.Flush(); + byte[] resultSpan = m_memoryStream.ToArray(); + TestContext.Out.WriteLine(Encoding.UTF8.GetString(resultSpan)); + + m_memoryStream.Position = 0; + EscapeStringSpanChars(m_testString); + m_streamWriter.Flush(); + byte[] resultSpanChars = m_memoryStream.ToArray(); + TestContext.Out.WriteLine(Encoding.UTF8.GetString(resultSpanChars)); + + m_memoryStream.Position = 0; + EscapeStringSpanCharsInline(m_testString); + m_streamWriter.Flush(); + byte[] resultSpanCharsInline = m_memoryStream.ToArray(); + TestContext.Out.WriteLine(Encoding.UTF8.GetString(resultSpanCharsInline)); + + m_memoryStream.Position = 0; + EscapeStringSpanCharsInlineConst(m_testString); + m_streamWriter.Flush(); + byte[] resultSpanCharsInlineConst = m_memoryStream.ToArray(); + TestContext.Out.WriteLine(Encoding.UTF8.GetString(resultSpanCharsInlineConst)); + + m_memoryStream.Position = 0; + EscapeStringSpanIndex(m_testString); + m_streamWriter.Flush(); + byte[] resultSpanIndex = m_memoryStream.ToArray(); + TestContext.Out.WriteLine(Encoding.UTF8.GetString(resultSpanIndex)); + + m_memoryStream.Position = 0; + EscapeStringSpanDict(m_testString); + m_streamWriter.Flush(); + byte[] resultSpanDict = m_memoryStream.ToArray(); + TestContext.Out.WriteLine(Encoding.UTF8.GetString(resultSpanDict)); + + Assert.IsTrue(Utils.IsEqual(resultLegacy, result)); + Assert.IsTrue(Utils.IsEqual(resultLegacy, resultLegacyPlus)); + Assert.IsTrue(Utils.IsEqual(resultLegacy, resultSpan)); + Assert.IsTrue(Utils.IsEqual(resultLegacy, resultSpanChars)); + Assert.IsTrue(Utils.IsEqual(resultLegacy, resultSpanCharsInline)); + Assert.IsTrue(Utils.IsEqual(resultLegacy, resultSpanCharsInlineConst)); + Assert.IsTrue(Utils.IsEqual(resultLegacy, resultSpanIndex)); + Assert.IsTrue(Utils.IsEqual(resultLegacy, resultSpanDict)); + } + + #region Test Setup + [OneTimeSetUp] + public void OneTimeSetUp() + { + m_memoryManager = new RecyclableMemoryStreamManager(); + m_memoryStream = new RecyclableMemoryStream(m_memoryManager); + m_streamWriter = new StreamWriter(m_memoryStream, new UTF8Encoding(false), m_streamSize, false); + } + + [OneTimeTearDown] + public void OneTimeTearDown() + { + m_streamWriter?.Dispose(); + m_streamWriter = null; + m_memoryStream?.Dispose(); + m_memoryStream = null; + m_memoryManager = null; + } + #endregion + + #region Benchmark Setup + /// 4 + /// Set up some variables for benchmarks. + /// + [GlobalSetup] + public void GlobalSetup() + { + m_memoryManager = new RecyclableMemoryStreamManager(); + m_memoryStream = new RecyclableMemoryStream(m_memoryManager); + m_streamWriter = new StreamWriter(m_memoryStream, Encoding.UTF8, m_streamSize, false); + m_testString = EscapeTestStrings[StringVariantIndex - 1]; + } + + [GlobalCleanup] + public void GlobalCleanup() + { + m_streamWriter?.Dispose(); + m_streamWriter = null; + m_memoryStream?.Dispose(); + m_memoryStream = null; + m_memoryManager = null; + } + #endregion + + #region Private Methods + /// + /// Version used previously in JsonEncoder. + /// + private void EscapedStringLegacy(string value) + { + foreach (char ch in value) + { + bool found = false; + + for (int ii = 0; ii < m_specialChars.Length; ii++) + { + if (m_specialChars[ii] == ch) + { + m_streamWriter.Write('\\'); + m_streamWriter.Write(m_substitution[ii]); + found = true; + break; + } + } + + if (!found) + { + if (ch < 32) + { + m_streamWriter.Write("\\u"); + m_streamWriter.Write("{0:X4}", (int)ch); + continue; + } + + m_streamWriter.Write(ch); + } + } + } + + /// + /// Version used previously in JsonEncoder plus improvement of binary encoding. + /// + /// + /// For the underlying stream writer it is faster to write two chars than a 2 char string. + /// + private void EscapedStringLegacyPlus(string value) + { + foreach (char ch in value) + { + bool found = false; + + for (int ii = 0; ii < m_specialChars.Length; ii++) + { + if (m_specialChars[ii] == ch) + { + m_streamWriter.Write('\\'); + m_streamWriter.Write(m_substitution[ii]); + found = true; + break; + } + } + + if (!found) + { + if (ch < 32) + { + m_streamWriter.Write('\\'); + m_streamWriter.Write('u'); + m_streamWriter.Write(((int)ch).ToString("X4", CultureInfo.InvariantCulture)); + continue; + } + + m_streamWriter.Write(ch); + } + } + } + + /// + /// Using a span to escape the string, write strings to stream writer if possible. + /// + private void EscapeStringSpan(string value) + { + ReadOnlySpan charSpan = value.AsSpan(); + int lastOffset = 0; + + for (int i = 0; i < charSpan.Length; i++) + { + bool found = false; + char ch = charSpan[i]; + + for (int ii = 0; ii < m_specialChars.Length; ii++) + { + if (m_specialChars[ii] == ch) + { + if (lastOffset < i) + { +#if NETCOREAPP2_1_OR_GREATER + m_streamWriter.Write(charSpan.Slice(lastOffset, i - lastOffset)); +#else + m_streamWriter.Write(charSpan.Slice(lastOffset, i - lastOffset).ToString()); +#endif + } + lastOffset = i + 1; + m_streamWriter.Write(m_substitutionStrings[ii]); + found = true; + break; + } + } + + if (!found && ch < 32) + { + if (lastOffset < i) + { +#if NETCOREAPP2_1_OR_GREATER + m_streamWriter.Write(charSpan.Slice(lastOffset, i - lastOffset)); +#else + m_streamWriter.Write(charSpan.Slice(lastOffset, i - lastOffset).ToString()); +#endif + } + lastOffset = i + 1; + m_streamWriter.Write("\\u"); + m_streamWriter.Write(((int)ch).ToString("X4", CultureInfo.InvariantCulture)); + } + } + if (lastOffset == 0) + { + m_streamWriter.Write(value); + } + else if (lastOffset < charSpan.Length) + { +#if NETCOREAPP2_1_OR_GREATER + m_streamWriter.Write(charSpan.Slice(lastOffset, charSpan.Length - lastOffset)); +#else + m_streamWriter.Write(charSpan.Slice(lastOffset, charSpan.Length - lastOffset).ToString()); +#endif + } + } + + /// + /// Use span to escape the string, write only chars to stream writer. + /// + /// + private void EscapeStringSpanChars(string value) + { + ReadOnlySpan charSpan = value.AsSpan(); + + int lastOffset = 0; + for (int i = 0; i < charSpan.Length; i++) + { + bool found = false; + char ch = charSpan[i]; + + for (int ii = 0; ii < m_specialChars.Length; ii++) + { + if (m_specialChars[ii] == ch) + { + if (lastOffset < i) + { +#if NETCOREAPP2_1_OR_GREATER + m_streamWriter.Write(charSpan.Slice(lastOffset, i - lastOffset)); +#else + m_streamWriter.Write(charSpan.Slice(lastOffset, i - lastOffset).ToString()); +#endif + } + lastOffset = i + 1; + m_streamWriter.Write('\\'); + m_streamWriter.Write(m_substitution[ii]); + found = true; + break; + } + } + + if (!found && ch < 32) + { + if (lastOffset < i - 1) + { +#if NETCOREAPP2_1_OR_GREATER + m_streamWriter.Write(charSpan.Slice(lastOffset, i - lastOffset)); +#else + m_streamWriter.Write(charSpan.Slice(lastOffset, i - lastOffset).ToString()); +#endif + } + else + { + while (lastOffset < i) + { + m_streamWriter.Write(charSpan[lastOffset++]); + } + } + lastOffset = i + 1; + m_streamWriter.Write('\\'); + m_streamWriter.Write('u'); + m_streamWriter.Write(((int)ch).ToString("X4", CultureInfo.InvariantCulture)); + } + } + + if (lastOffset == 0) + { +#if NETCOREAPP2_1_OR_GREATER + m_streamWriter.Write(charSpan); +#else + m_streamWriter.Write(value); +#endif + } + else if (lastOffset < charSpan.Length) + { +#if NETCOREAPP2_1_OR_GREATER + m_streamWriter.Write(charSpan.Slice(lastOffset, charSpan.Length - lastOffset)); +#else + m_streamWriter.Write(charSpan.Slice(lastOffset, charSpan.Length - lastOffset).ToString()); +#endif + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void WriteSpan(ref int lastOffset, ReadOnlySpan valueSpan, int index) + { + if (lastOffset < index - 2) + { +#if NETCOREAPP2_1_OR_GREATER + m_streamWriter.Write(valueSpan.Slice(lastOffset, index - lastOffset)); +#else + m_streamWriter.Write(valueSpan.Slice(lastOffset, index - lastOffset).ToString()); +#endif + } + else + { + while (lastOffset < index) + { + m_streamWriter.Write(valueSpan[lastOffset++]); + } + } + lastOffset = index + 1; + } + + /// + /// Write only chars to stream writer, inline the write sequence for readability. + /// + /// + private void EscapeStringSpanCharsInline(string value) + { + ReadOnlySpan charSpan = value.AsSpan(); + int lastOffset = 0; + + for (int i = 0; i < charSpan.Length; i++) + { + bool found = false; + char ch = charSpan[i]; + + for (int ii = 0; ii < m_specialChars.Length; ii++) + { + if (m_specialChars[ii] == ch) + { + WriteSpan(ref lastOffset, charSpan, i); + m_streamWriter.Write('\\'); + m_streamWriter.Write(m_substitution[ii]); + found = true; + break; + } + } + + if (!found && ch < 32) + { + WriteSpan(ref lastOffset, charSpan, i); + m_streamWriter.Write('\\'); + m_streamWriter.Write('u'); + m_streamWriter.Write(((int)ch).ToString("X4", CultureInfo.InvariantCulture)); + } + } + + if (lastOffset == 0) + { +#if NETCOREAPP2_1_OR_GREATER + m_streamWriter.Write(charSpan); +#else + m_streamWriter.Write(value); +#endif + } + else + { + WriteSpan(ref lastOffset, charSpan, charSpan.Length); + } + } + + // create version of EscapeStringSpanCharsInline that references cosnt arrays + private void EscapeStringSpanCharsInlineConst(string value) + { + ReadOnlySpan charSpan = value.AsSpan(); + int lastOffset = 0; + + for (int i = 0; i < charSpan.Length; i++) + { + bool found = false; + char ch = charSpan[i]; + + for (int ii = 0; ii < m_specialCharsConst.Length; ii++) + { + if (m_specialCharsConst[ii] == ch) + { + WriteSpan(ref lastOffset, charSpan, i); + m_streamWriter.Write('\\'); + m_streamWriter.Write(m_substitutionConst[ii]); + found = true; + break; + } + } + + if (!found && ch < 32) + { + WriteSpan(ref lastOffset, charSpan, i); + m_streamWriter.Write('\\'); + m_streamWriter.Write('u'); + m_streamWriter.Write(((int)ch).ToString("X4", CultureInfo.InvariantCulture)); + } + } + + if (lastOffset == 0) + { + m_streamWriter.Write(value); + } + else + { + WriteSpan(ref lastOffset, charSpan, charSpan.Length); + } + } + + + private void EscapeStringSpanIndex(string value) + { + ReadOnlySpan charSpan = value.AsSpan(); + + int lastOffset = 0; + for (int i = 0; i < charSpan.Length; i++) + { + char ch = charSpan[i]; + + int index = m_specialString.IndexOf(ch); + if (index >= 0) + { + if (lastOffset < i) + { +#if NETCOREAPP2_1_OR_GREATER + m_streamWriter.Write(charSpan.Slice(lastOffset, i - lastOffset)); +#else + m_streamWriter.Write(charSpan.Slice(lastOffset, i - lastOffset).ToString()); +#endif + } + lastOffset = i + 1; + m_streamWriter.Write(m_substitutionStrings[index]); + continue; + } + + if (ch < 32) + { + if (lastOffset < i) + { +#if NETCOREAPP2_1_OR_GREATER + m_streamWriter.Write(charSpan.Slice(lastOffset, i - lastOffset)); +#else + m_streamWriter.Write(charSpan.Slice(lastOffset, i - lastOffset).ToString()); +#endif + } + lastOffset = i + 1; + m_streamWriter.Write('\\'); + m_streamWriter.Write('u'); + m_streamWriter.Write(((int)ch).ToString("X4", CultureInfo.InvariantCulture)); + } + } + if (lastOffset == 0) + { + m_streamWriter.Write(value); + } + else if (lastOffset < charSpan.Length) + { +#if NETCOREAPP2_1_OR_GREATER + m_streamWriter.Write(charSpan.Slice(lastOffset, charSpan.Length - lastOffset)); +#else + m_streamWriter.Write(charSpan.Slice(lastOffset, charSpan.Length - lastOffset).ToString()); +#endif + } + } + + private void EscapeStringSpanDict(string value) + { + ReadOnlySpan charSpan = value.AsSpan(); + + int lastOffset = 0; + for (int i = 0; i < charSpan.Length; i++) + { + char ch = charSpan[i]; + + if (m_replace.TryGetValue(ch, out string escapeSequence)) + { + if (lastOffset < i) + { +#if NETCOREAPP2_1_OR_GREATER + m_streamWriter.Write(charSpan.Slice(lastOffset, i - lastOffset)); +#else + m_streamWriter.Write(charSpan.Slice(lastOffset, i - lastOffset).ToString()); +#endif + } + lastOffset = i + 1; + m_streamWriter.Write(escapeSequence); + continue; + } + + if (ch < 32) + { + if (lastOffset < i) + { +#if NETCOREAPP2_1_OR_GREATER + m_streamWriter.Write(charSpan.Slice(lastOffset, i - lastOffset)); +#else + m_streamWriter.Write(charSpan.Slice(lastOffset, i - lastOffset).ToString()); +#endif + } + lastOffset = i + 1; + m_streamWriter.Write('\\'); + m_streamWriter.Write('u'); + m_streamWriter.Write(((int)ch).ToString("X4", CultureInfo.InvariantCulture)); + } + } + if (lastOffset == 0) + { + m_streamWriter.Write(value); + } + else if (lastOffset < charSpan.Length) + { +#if NETCOREAPP2_1_OR_GREATER + m_streamWriter.Write(charSpan.Slice(lastOffset, charSpan.Length - lastOffset)); +#else + m_streamWriter.Write(charSpan.Slice(lastOffset, charSpan.Length - lastOffset).ToString()); +#endif + } + } + + private void EscapeString(string value) + { + StringBuilder stringBuilder = new StringBuilder(value.Length * 2); + + foreach (char ch in value) + { + if (m_replace.TryGetValue(ch, out string escapeSequence)) + { + stringBuilder.Append(escapeSequence); + } + else if (ch < 32) + { + stringBuilder.Append("\\u"); + stringBuilder.Append(((int)ch).ToString("X4", CultureInfo.InvariantCulture)); + } + else + { + stringBuilder.Append(ch); + } + } + m_streamWriter.Write(stringBuilder); + } + #endregion + + #region Private Fields + private static string m_testString; + private RecyclableMemoryStreamManager m_memoryManager; + private RecyclableMemoryStream m_memoryStream; + private StreamWriter m_streamWriter; + private int m_streamSize = 1024; + private static readonly string m_specialString = "\"\\\n\r\t\b\f"; + + // Declare static readonly characters for the special characters + private static readonly char sro_quotation = '\"'; + private static readonly char sro_backslash = '\\'; + private static readonly char sro_newline = '\n'; + private static readonly char sro_return = '\r'; + private static readonly char sro_tab = '\t'; + private static readonly char sro_backspace = '\b'; + private static readonly char sro_formfeed = '\f'; + private static readonly char[] m_specialChars = new char[] { sro_quotation, sro_backslash, sro_newline, sro_return, sro_tab, sro_backspace, sro_formfeed }; + + // Declare static readonly characters for the substitution characters + private static readonly char sro_quotationSub = '\"'; + private static readonly char sro_backslashSub = '\\'; + private static readonly char sro_newlineSub = 'n'; + private static readonly char sro_returnSub = 'r'; + private static readonly char sro_tabSub = 't'; + private static readonly char sro_backspaceSub = 'b'; + private static readonly char sro_formfeedSub = 'f'; + private static readonly char[] m_substitution = new char[] { sro_quotationSub, sro_backslashSub, sro_newlineSub, sro_returnSub, sro_tabSub, sro_backspaceSub, sro_formfeedSub }; + + // Special characters as const + private const char s_quotation = '\"'; + private const char s_backslash = '\\'; + private const char s_newline = '\n'; + private const char s_return = '\r'; + private const char s_tab = '\t'; + private const char s_backspace = '\b'; + private const char s_formfeed = '\f'; + + private static readonly char[] m_specialCharsConst = new char[] { s_quotation, s_backslash, s_newline, s_return, s_tab, s_backspace, s_formfeed }; + + // Substitution as const + private const char s_quotationSub = '\"'; + private const char s_backslashSub = '\\'; + private const char s_newlineSub = 'n'; + private const char s_returnSub = 'r'; + private const char s_tabSub = 't'; + private const char s_backspaceSub = 'b'; + private const char s_formfeedSub = 'f'; + + private static readonly char[] m_substitutionConst = new char[] { s_quotationSub, s_backslashSub, s_newlineSub, s_returnSub, s_tabSub, s_backspaceSub, s_formfeedSub }; + + private static readonly string[] m_substitutionStrings = new string[] { "\\\"", "\\\\", "\\n", "\\r", "\\t", "\\b", "\\f" }; + private static readonly Dictionary m_replace = new Dictionary + { + { '\"', "\\\"" }, + { '\\', "\\\\" }, + { '\n', "\\n" }, + { '\r', "\\r" }, + { '\t', "\\t" }, + { '\b', "\\b" }, + { '\f', "\\f" } + }; + #endregion + } +} diff --git a/Tests/Opc.Ua.Core.Tests/Types/Encoders/JsonEncoderTests.cs b/Tests/Opc.Ua.Core.Tests/Types/Encoders/JsonEncoderTests.cs index 4cb3ae1503..e35b1acfe9 100644 --- a/Tests/Opc.Ua.Core.Tests/Types/Encoders/JsonEncoderTests.cs +++ b/Tests/Opc.Ua.Core.Tests/Types/Encoders/JsonEncoderTests.cs @@ -35,7 +35,9 @@ using System.Linq; using System.Text; using System.Xml; using BenchmarkDotNet.Attributes; +using Microsoft.IO; using NUnit.Framework; +using Opc.Ua.Bindings; namespace Opc.Ua.Core.Tests.Types.Encoders { @@ -365,14 +367,54 @@ namespace Opc.Ua.Core.Tests.Types.Encoders } /// - /// Use a constructor with external Stream, + /// Use a MemoryStream constructor with external Stream, /// keep the stream open for more encodings. /// [Test] - public void ConstructorStream() + public void ConstructorMemoryStream() + { + using (var memoryStream = new MemoryStream()) + { + ConstructorStream(memoryStream); + } + } + + /// + /// Use a ArraySegmentStream constructor with external Stream, + /// keep the stream open for more encodings. + /// + [Test] + public void ConstructorArraySegmentStream() + { + using (var memoryStream = new ArraySegmentStream(BufferManager)) + { + ConstructorStream(memoryStream); + } + } + + /// + /// Use a RecylableMemoryStream constructor with external Stream, + /// keep the stream open for more encodings. + /// + [Test] + public void ConstructorRecyclableMemoryStream() + { + var recyclableMemoryStreamManager = new RecyclableMemoryStreamManager(new RecyclableMemoryStreamManager.Options { + BlockSize = BufferManager.MaxSuggestedBufferSize, + }); + using (var memoryStream = new RecyclableMemoryStream(recyclableMemoryStreamManager)) + { + ConstructorStream(memoryStream); + } + } + + /// + /// Use a constructor with external Stream, + /// keep the stream open for more encodings. + /// + public void ConstructorStream(MemoryStream memoryStream) { var context = new ServiceMessageContext(); - var memoryStream = new MemoryStream(); using (var jsonEncoder = new JsonEncoder(context, true, false, memoryStream, true)) { TestEncoding(jsonEncoder); @@ -384,7 +426,7 @@ namespace Opc.Ua.Core.Tests.Types.Encoders // recycle the StreamWriter, ensure the result is equal memoryStream.Position = 0; - using (var jsonEncoder = new JsonEncoder(context, true, false, memoryStream, true)) + using (IJsonEncoder jsonEncoder = new JsonEncoder(context, true, false, memoryStream, true)) { TestEncoding(jsonEncoder); } @@ -411,14 +453,141 @@ namespace Opc.Ua.Core.Tests.Types.Encoders Assert.Throws(() => _ = new StreamWriter(memoryStream)); } + /// + /// Use a constructor with external ArraySegmentStream, + /// keep the stream open for more encodings. + /// Alternate use of sequence. + /// + [Test] + public void ConstructorArraySegmentStreamSequence() + { + var context = new ServiceMessageContext(); + using (var memoryStream = new ArraySegmentStream(BufferManager)) + { + using (var jsonEncoder = new JsonEncoder(context, true, false, memoryStream, true)) + { + TestEncoding(jsonEncoder); + } + + // get the buffers and save the result +#if NET5_0_OR_GREATER + string result1; + using (var sequence = memoryStream.GetSequence(nameof(ConstructorStream))) + { + result1 = Encoding.UTF8.GetString(sequence.Sequence); + Assert.IsNotEmpty(result1); + TestContext.Out.WriteLine("Result1:"); + _ = PrettifyAndValidateJson(result1); + } +#else + var result1 = Encoding.UTF8.GetString(memoryStream.ToArray()); + Assert.IsNotEmpty(result1); + TestContext.Out.WriteLine("Result1:"); + _ = PrettifyAndValidateJson(result1); +#endif + + // recycle the memory stream, ensure the result is equal + memoryStream.Position = 0; + using (var jsonEncoder = new JsonEncoder(context, true, false, memoryStream, true)) + { + TestEncoding(jsonEncoder); + } + var result2 = Encoding.UTF8.GetString(memoryStream.ToArray()); + Assert.IsNotEmpty(result2); + TestContext.Out.WriteLine("Result2:"); + _ = PrettifyAndValidateJson(result2); + Assert.AreEqual(result1, result2); + + // recycle the StreamWriter, ensure the result is equal, + // use reflection to return result in external stream + memoryStream.Position = 0; + using (IJsonEncoder jsonEncoder = new JsonEncoder(context, true, false, memoryStream, false)) + { + TestEncoding(jsonEncoder); + var result3 = jsonEncoder.CloseAndReturnText(); + Assert.IsNotEmpty(result3); + TestContext.Out.WriteLine("Result3:"); + _ = PrettifyAndValidateJson(result3); + Assert.AreEqual(result1, result3); + } + + // ensure the memory stream was closed + Assert.Throws(() => _ = new StreamWriter(memoryStream)); + } + } + + /// + /// Use a constructor with external RecyclableMemoryStream, + /// keep the stream open for more encodings. + /// Alternate use of sequence. + /// + [Test] + public void ConstructorRecyclableMemoryStreamSequence() + { + var context = new ServiceMessageContext(); + using (var memoryStream = new RecyclableMemoryStream(RecyclableMemoryManager)) + { + using (var jsonEncoder = new JsonEncoder(context, true, false, memoryStream, true)) + { + TestEncoding(jsonEncoder); + } + + // get the buffers and save the result +#if NET5_0_OR_GREATER + string result1; + { + var sequence = memoryStream.GetReadOnlySequence(); + result1 = Encoding.UTF8.GetString(sequence); + Assert.IsNotEmpty(result1); + TestContext.Out.WriteLine("Result1:"); + _ = PrettifyAndValidateJson(result1); + } +#else + var result1 = Encoding.UTF8.GetString(memoryStream.ToArray()); + Assert.IsNotEmpty(result1); + TestContext.Out.WriteLine("Result1:"); + _ = PrettifyAndValidateJson(result1); +#endif + + // recycle the memory stream, ensure the result is equal + memoryStream.Position = 0; + using (var jsonEncoder = new JsonEncoder(context, true, false, memoryStream, true)) + { + TestEncoding(jsonEncoder); + } + var result2 = Encoding.UTF8.GetString(memoryStream.ToArray()); + Assert.IsNotEmpty(result2); + TestContext.Out.WriteLine("Result2:"); + _ = PrettifyAndValidateJson(result2); + Assert.AreEqual(result1, result2); + + // recycle the StreamWriter, ensure the result is equal, + // use reflection to return result in external stream + memoryStream.Position = 0; + using (IJsonEncoder jsonEncoder = new JsonEncoder(context, true, false, memoryStream, false)) + { + TestEncoding(jsonEncoder); + var result3 = jsonEncoder.CloseAndReturnText(); + Assert.IsNotEmpty(result3); + TestContext.Out.WriteLine("Result3:"); + _ = PrettifyAndValidateJson(result3); + Assert.AreEqual(result1, result3); + } + + // ensure the memory stream was closed + Assert.Throws(() => _ = new StreamWriter(memoryStream)); + } + } + /// /// Verify reversible Json encoding. /// [Theory] - public void JsonEncodeRev(JsonValidationData jsonValidationData) + public void JsonEncodeRev(JsonValidationData jsonValidationData, MemoryStreamType memoryStreamType) { EncodeJsonVerifyResult( jsonValidationData.BuiltInType, + memoryStreamType, jsonValidationData.Instance, true, jsonValidationData.ExpectedReversible, @@ -430,10 +599,11 @@ namespace Opc.Ua.Core.Tests.Types.Encoders /// Verify non reversible Json encoding. /// [Theory] - public void JsonEncodeNonRev(JsonValidationData jsonValidationData) + public void JsonEncodeNonRev(JsonValidationData jsonValidationData, MemoryStreamType memoryStreamType) { EncodeJsonVerifyResult( jsonValidationData.BuiltInType, + memoryStreamType, jsonValidationData.Instance, false, jsonValidationData.ExpectedNonReversible ?? jsonValidationData.ExpectedReversible, diff --git a/common.props b/common.props index bbfba2af14..84f1105324 100644 --- a/common.props +++ b/common.props @@ -21,9 +21,14 @@ - true + true + false false + + + true + images/logo.jpg