Improve ArraySegmentStream with ReadOnlyMemory/Span signatures, avoid extra alloc/copy (#2556)

- Implement `int Read(Span<byte> buffer)` and `void Write(ReadOnlySpan<byte> buffer)` in ArraySegmentStream
- for ArraySegmentStream streaming lower the CPU load for .NET8 OPC UA binary encoding/decoding by 50%
- Implement dispose to return buffers that were not transferred. 
- Implement a GetSequence in ArraySegmentStream
This commit is contained in:
Martin Regen
2024-03-29 11:50:55 +01:00
committed by GitHub
parent d717f67440
commit ebb55ad25e
24 changed files with 2487 additions and 1342 deletions
+5 -4
View File
@@ -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 }}'
+6
View File
@@ -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
@@ -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);
}
/// <summary>
/// Creates a writeable stream that creates buffers as necessary.
/// </summary>
/// <param name="bufferManager">The buffer manager.</param>
public ArraySegmentStream(BufferManager bufferManager)
: this(bufferManager, bufferManager.MaxSuggestedBufferSize, 0, bufferManager.MaxSuggestedBufferSize)
{
}
#endregion
#region IDisposable
/// <inheritdoc/>
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<byte>(m_buffers[ii].Array, m_buffers[ii].Offset, GetBufferCount(ii)));
}
m_buffers.Clear();
ClearBuffers();
return buffers;
}
/// <summary>
/// Returns sequence of the buffers stored in the stream.
/// </summary>
/// <remarks>
/// The buffers ownership is transferred to the sequence,
/// the stream can be disposed.
/// The new owner is responisble to dispose the sequence after use.
/// </remarks>
public BufferSequence GetSequence(string owner)
{
if (m_buffers.Count == 0)
{
return new BufferSequence(m_bufferManager, owner, null, ReadOnlySequence<byte>.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<byte>(firstSegment, 0, nextSegment, endIndex);
ClearBuffers();
return new BufferSequence(m_bufferManager, owner, firstSegment, sequence);
}
#endregion
#region Overridden Methods
/// <summary cref="Stream.CanRead" />
/// <inheritdoc/>
public override bool CanRead
{
get { return true; }
get { return m_buffers != null; }
}
/// <summary cref="Stream.CanSeek" />
/// <inheritdoc/>
public override bool CanSeek
{
get { return true; }
get { return m_buffers != null; }
}
/// <summary cref="Stream.CanWrite" />
/// <inheritdoc/>
public override bool CanWrite
{
get { return true; }
get { return m_buffers != null; }
}
/// <summary cref="Stream.Flush" />
/// <inheritdoc/>
public override void Flush()
{
// nothing to do.
}
/// <summary cref="Stream.Length" />
/// <inheritdoc/>
public override long Length
{
get { return GetAbsoluteLength(); }
}
/// <summary cref="Stream.Position" />
/// <inheritdoc/>
public override long Position
{
get
@@ -132,7 +201,7 @@ namespace Opc.Ua.Bindings
}
}
/// <summary cref="Stream.ReadByte()" />
/// <inheritdoc/>
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);
}
/// <summary cref="Stream.Read(byte[], int, int)" />
#if STREAM_WITH_SPAN_SUPPORT
/// <summary>
/// Helper to benchmark the performance of the stream.
/// </summary>
internal int ReadMemoryStream(Span<byte> buffer) => base.Read(buffer);
/// <inheritdoc/>
public override int Read(Span<byte> 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
/// <inheritdoc/>
public override int Read(byte[] buffer, int offset, int count)
{
int bytesRead = 0;
@@ -195,7 +315,7 @@ namespace Opc.Ua.Bindings
return bytesRead;
}
/// <summary cref="Stream.Seek" />
/// <inheritdoc/>
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.");
}
/// <summary cref="Stream.SetLength(long)" />
/// <inheritdoc/>
public override void SetLength(long value)
{
throw new NotSupportedException();
}
/// <summary cref="Stream.WriteByte(byte)" />
/// <inheritdoc/>
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);
}
/// <summary cref="Stream.Write(byte[],int,int)" />
#if STREAM_WITH_SPAN_SUPPORT
/// <summary>
/// Helper to benchmark the performance of the stream.
/// </summary>
internal void WriteMemoryStream(ReadOnlySpan<byte> buffer) => base.Write(buffer);
/// <inheritdoc/>
public override void Write(ReadOnlySpan<byte> 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
/// <inheritdoc/>
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
}
}
/// <summary cref="MemoryStream.ToArray()" />
/// <inheritdoc/>
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
/// <summary>
/// Update the current buffer count.
/// </summary>
[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;
}
}
}
/// <summary>
/// Sets the current buffer.
/// </summary>
@@ -446,6 +623,17 @@ namespace Opc.Ua.Bindings
SetCurrentBuffer(m_buffers.Count - 1);
}
}
/// <summary>
/// Clears the buffers and resets the state variables.
/// </summary>
private void ClearBuffers()
{
m_buffers.Clear();
m_bufferIndex = 0;
m_endOfLastBuffer = 0;
SetCurrentBuffer(0);
}
#endregion
#region Private Fields
@@ -125,6 +125,7 @@ namespace Opc.Ua.Bindings
? ArrayPool<byte>.Shared
: ArrayPool<byte>.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);
}
/// <summary>
/// Returns the suggested max rent size for data in the buffers.
/// </summary>
/// <param name="maxBufferSize">The max buffer size configured.</param>
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;
}
/// <summary>
/// Helper to round up to the next power of two.
/// </summary>
private int RoundUpToPowerOfTwo(int value)
{
int result = 1;
while (result < value && result != 0)
{
result <<= 1;
}
return result;
}
/// <summary>
/// Returns the max size of data in the buffers.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
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;
@@ -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
{
/// <summary>
/// Helper to build a ReadOnlySequence from a set of buffers.
/// </summary>
public sealed class BufferSegment : ReadOnlySequenceSegment<byte>
{
/// <summary>
/// Returns the base array of the buffer.
/// </summary>
public byte[] Array() => m_array;
/// <summary>
/// Constructor for a buffer segment.
/// </summary>
public BufferSegment(byte[] array, int offset, int length)
{
Memory = new ReadOnlyMemory<byte>(array, offset, length);
m_array = array;
}
/// <summary>
/// Appends a buffer to the sequence.
/// </summary>
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
}
}
@@ -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
{
/// <summary>
/// A class to hold a sequence of buffers until disposed.
/// </summary>
public sealed class BufferSequence : IDisposable
{
/// <summary>
/// The constructor to create the sequence of buffers.
/// </summary>
public BufferSequence(BufferManager bufferManager, string owner, BufferSegment firstSegment, ReadOnlySequence<byte> sequence)
{
m_bufferManager = bufferManager;
m_owner = owner;
m_firstSegment = firstSegment;
m_sequence = sequence;
}
/// <inheritdoc/>
public void Dispose()
{
BufferSegment segment = m_firstSegment;
while (segment != null)
{
m_bufferManager.ReturnBuffer(segment.Array(), m_owner);
segment = (BufferSegment)segment.Next;
}
m_sequence = ReadOnlySequence<byte>.Empty;
m_firstSegment = null;
GC.SuppressFinalize(this);
}
/// <summary>
/// Returns a sequence which can be used to access the buffers.
/// </summary>
public ReadOnlySequence<byte> Sequence => m_sequence;
#region Private
private BufferManager m_bufferManager;
private BufferSegment m_firstSegment;
private ReadOnlySequence<byte> m_sequence;
private string m_owner;
#endregion
}
}
+44 -27
View File
@@ -283,41 +283,31 @@ namespace Opc.Ua
/// </summary>
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());
}
/// <summary>
/// Completes writing and returns the text length.
/// The StreamWriter is disposed.
/// </summary>
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
/// <summary>
/// Completes writing and returns the text length.
/// </summary>
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;
}
/// <summary>
/// Writes a DiagnosticInfo to the stream.
/// Ignores InnerDiagnosticInfo field if the nesting level
@@ -276,6 +276,7 @@ namespace Opc.Ua.Client.ComplexTypes.Tests.Types
/// </summary>
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)))
{
@@ -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);
}
/// <summary>
@@ -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);
}
/// <summary>
@@ -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
}
@@ -120,7 +120,8 @@ namespace Opc.Ua.Client.ComplexTypes.Tests.Types
/// </summary>
[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
/// </summary>
[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
/// </summary>
[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
/// </summary>
[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
/// </summary>
[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,
@@ -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.
/// </summary>
[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.
/// </summary>
[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 .
/// </summary>
[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
+9 -6
View File
@@ -614,13 +614,16 @@ namespace Opc.Ua.Client.Tests
sre = Assert.Throws<ServiceResultException>(() => 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);
}
}
}
@@ -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;
@@ -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
{
/// <summary>
/// Test decoding with internal memory stream.
/// </summary>
[Test]
public void BinaryDecoderInternalMemoryStreamTest()
{
using (var binaryDecoder = new BinaryDecoder(m_encodedByteArray, m_context))
{
TestDecoding(binaryDecoder);
}
}
/// <summary>
/// Test decoding with ArrayPool memory stream.
/// </summary>
[Test]
public void BinaryDecoderArraySegmentStreamTest()
{
using (var memoryStream = new ArraySegmentStream(m_encodedBufferList))
using (var binaryDecoder = new BinaryDecoder(memoryStream, m_context))
{
TestDecoding(binaryDecoder);
}
}
/// <summary>
/// Test decoding with ArrayPool memory stream that has no span support.
/// </summary>
[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);
}
}
/// <summary>
/// Benchmark decoding with memory stream.
/// </summary>
[Benchmark(Baseline = true)]
[Test]
public void BinaryDecoderMemoryStream()
{
using (var memoryStream = new MemoryStream(m_encodedByteArray))
{
BinaryDecoder_Stream(memoryStream);
}
}
/// <summary>
/// Benchmark decoding with array segment memory stream.
/// </summary>
[Benchmark]
[Test]
public void BinaryDecoderArraySegmentStream()
{
using (var arraySegmentStream = new ArraySegmentStream(m_encodedBufferList))
{
BinaryDecoder_Stream(arraySegmentStream);
}
}
/// <summary>
/// Benchmark decoding with array segment memory stream without span support.
/// </summary>
[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
/// <summary>
/// Set up some variables for benchmarks.
/// </summary>
[GlobalSetup]
public new void GlobalSetup()
{
base.GlobalSetup();
InitializeEncodedBenchmarkData();
}
/// <summary>
/// Tear down benchmark variables.
/// </summary>
[GlobalCleanup]
public new void GlobalCleanup()
{
base.GlobalCleanup();
}
#endregion
#region Private Methods
/// <summary>
/// Initialize encoded data.
/// </summary>
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");
}
}
/// <summary>
/// Initialize encoded data.
/// </summary>
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;
}
}
@@ -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;
/// <summary>
/// Benchmark encoding with internal memory stream.
/// Test encoding with internal memory stream.
/// </summary>
[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);
}
}
/// <summary>
/// Test encoding with internal memory stream,
/// uses reflection to get array from memory stream.
/// </summary>
[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);
}
}
/// <summary>
/// Benchmark encoding with ArrayPool memory stream.
/// Test encoding with ArrayPool memory stream.
/// </summary>
[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);
}
}
/// <summary>
/// Test encoding with memory stream kept open.
/// </summary>
[Theory]
public void BinaryEncoderMemoryStreamTest(bool toArray)
{
using (var memoryStream = new MemoryStream(StreamBufferSize))
{
TestStreamEncode(memoryStream, toArray);
}
}
/// <summary>
/// Test encoding with recyclable memory stream kept open.
/// </summary>
[Theory]
public void BinaryEncoderRecyclableMemoryStream(bool toArray)
{
using (var memoryStream = new RecyclableMemoryStream(m_memoryManager))
{
TestStreamEncode(memoryStream, toArray);
}
}
/// <summary>
/// Benchmark encoding with memory stream kept open.
/// </summary>
[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();
}
}
/// <summary>
/// Encoding test with memory stream kept open.
/// </summary>
[Benchmark]
[Test]
public void BinaryEncoderStreamLeaveOpenMemoryStream()
{
BinaryEncoder_StreamLeaveOpen(m_memoryStream);
}
/// <summary>
/// Benchmark encoding with recyclable memory stream kept open.
/// </summary>
[Benchmark]
[Test]
public void BinaryEncoderStreamLeaveOpenRecyclableMemoryStream()
public void BinaryEncoderRecyclableMemoryStream()
{
BinaryEncoder_StreamLeaveOpen(m_recyclableMemoryStream);
}
/// <summary>
/// Encoding test with memory stream kept open.
/// </summary>
[Benchmark]
[Test]
public void BinaryEncoderStreamLeaveOpenArraySegmentStream()
{
BinaryEncoder_StreamLeaveOpen(m_arraySegmentStream);
}
/// <summary>
/// Benchmark encoding with memory stream kept open,
/// use internal reflection to get string from memory stream.
/// </summary>
[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();
}
}
/// <summary>
/// Benchmark encoding with array segment memory stream kept open.
/// </summary>
[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");
}
}
}
/// <summary>
/// Benchmark encoding with array segment memory stream kept open,
/// to compare the version without span support.
/// </summary>
[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.
/// </summary>
[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();
/// <summary>
/// Tear down benchmark variables.
/// </summary>
[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<Int32> s_list = new List<Int32>() { 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
}
}
@@ -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))
{
@@ -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
{
/// <summary>
/// Common code for encoder/decoder benchmarks.
/// </summary>
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<int>();
for (int i = 0; i < DataValueCount; i++)
{
m_list.Add(m_random.Next());
}
m_values = new List<DataValue>();
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
/// <summary>
/// Set up some variables for benchmarks.
/// </summary>
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);
}
/// <summary>
/// Tear down benchmark variables.
/// </summary>
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<Int32> m_list;
protected List<DataValue> m_values;
protected IServiceMessageContext m_context;
protected RecyclableMemoryStreamManager m_memoryManager;
protected BufferManager m_bufferManager;
#endregion
}
#if NET6_0_OR_GREATER
/// <summary>
/// Helper class to test ArraySegmentStream without Span support.
/// </summary>
public class ArraySegmentStreamNoSpan : ArraySegmentStream
{
public ArraySegmentStreamNoSpan(BufferManager bufferManager)
: base(bufferManager)
{
}
public ArraySegmentStreamNoSpan(BufferCollection buffers)
: base(buffers)
{
}
public override int Read(Span<byte> buffer)
{
return base.ReadMemoryStream(buffer);
}
public override void Write(ReadOnlySpan<byte> buffer)
{
base.WriteMemoryStream(buffer);
}
}
#endif
}
@@ -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
{
/// <summary>
/// Supported memory stream types.
/// </summary>
public enum MemoryStreamType
{
MemoryStream,
ArraySegmentStream,
RecyclableMemoryStream
}
/// <summary>
/// Base class for the encoder tests.
/// </summary>
@@ -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
/// </summary>
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;
}
/// <summary>
/// Returns various implementations of a memory stream.
/// </summary>
/// <param name="memoryStreamType"></param>
/// <returns>A MemoryStream</returns>
/// <exception cref="ArgumentOutOfRangeException"></exception>
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.");
}
}
/// <summary>
/// Encoder factory for all encoding types.
/// </summary>
@@ -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
};
@@ -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);
}
/// <summary>
@@ -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);
}
/// <summary>
@@ -120,7 +120,7 @@ namespace Opc.Ua.Core.Tests.Types.Encoders
break;
}
};
EncodeDecode(encoderType, builtInType, randomData);
EncodeDecode(encoderType, builtInType, MemoryStreamType.ArraySegmentStream, randomData);
}
/// <summary>
@@ -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);
}
/// <summary>
@@ -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);
}
/// <summary>
@@ -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);
}
/// <summary>
@@ -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);
}
/// <summary>
@@ -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);
}
/// <summary>
@@ -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);
}
/// <summary>
@@ -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);
}
/// <summary>
@@ -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))
{
File diff suppressed because it is too large Load Diff
@@ -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
/// <summary>
/// Set up some variables for benchmarks.
/// </summary>
[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;
}
}
/// <summary>
/// Tear down benchmark variables.
/// </summary>
[GlobalCleanup]
public void GlobalCleanup()
{
}
#endregion
#region Private Fields
private DateTime m_dateTime;
#endregion
}
}
@@ -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.",
};
/// <summary>
/// Benchmark encoding of the previous implementation.
/// </summary>
[Benchmark(Baseline = true)]
public void EscapeStringLegacy()
{
m_memoryStream.Position = 0;
int repeats = InnerLoops;
while (repeats-- > 0)
{
EscapedStringLegacy(m_testString);
}
m_streamWriter.Flush();
}
/// <summary>
/// Benchmark encoding of the previous implementation with snall improvement for binary encoding.
/// </summary>
[Benchmark]
public void EscapeStringLegacyPlus()
{
m_memoryStream.Position = 0;
int repeats = InnerLoops;
while (repeats-- > 0)
{
EscapedStringLegacyPlus(m_testString);
}
m_streamWriter.Flush();
}
/// <summary>
/// A new implementation using StringBuilder.
/// </summary>
[Benchmark]
public void EscapeStringStringBuilder()
{
m_memoryStream.Position = 0;
int repeats = InnerLoops;
while (repeats-- > 0)
{
EscapeString(m_testString);
}
m_streamWriter.Flush();
}
/// <summary>
/// A new implementation using ReadOnlySpan.
/// </summary>
[Benchmark]
public void EscapeStringSpan()
{
m_memoryStream.Position = 0;
int repeats = InnerLoops;
while (repeats-- > 0)
{
EscapeStringSpan(m_testString);
}
m_streamWriter.Flush();
}
/// <summary>
/// A new implementation using ReadOnlySpan and char write.
/// </summary>
[Benchmark]
public void EscapeStringSpanChars()
{
m_memoryStream.Position = 0;
int repeats = InnerLoops;
while (repeats-- > 0)
{
EscapeStringSpanChars(m_testString);
}
m_streamWriter.Flush();
}
/// <summary>
/// A new implementation using ReadOnlySpan and char write.
/// </summary>
[Benchmark]
public void EscapeStringSpanCharsInline()
{
m_memoryStream.Position = 0;
int repeats = InnerLoops;
while (repeats-- > 0)
{
EscapeStringSpanCharsInline(m_testString);
}
m_streamWriter.Flush();
}
/// <summary>
/// A new implementation using ReadOnlySpan and char write with const arrays.
/// </summary>
[Benchmark]
public void EscapeStringSpanCharsInlineConst()
{
m_memoryStream.Position = 0;
int repeats = InnerLoops;
while (repeats-- > 0)
{
EscapeStringSpanCharsInlineConst(m_testString);
}
m_streamWriter.Flush();
}
/// <summary>
/// A new implementation using ReadOnlySpan and IndexOf.
/// </summary>
[Benchmark]
public void EscapeStringSpanIndex()
{
m_memoryStream.Position = 0;
int repeats = InnerLoops;
while (repeats-- > 0)
{
EscapeStringSpanIndex(m_testString);
}
m_streamWriter.Flush();
}
/// <summary>
/// A new implementation using ReadOnlySpan and Dictionary.
/// </summary>
[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
/// <summary>4
/// Set up some variables for benchmarks.
/// </summary>
[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
/// <summary>
/// Version used previously in JsonEncoder.
/// </summary>
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);
}
}
}
/// <summary>
/// Version used previously in JsonEncoder plus improvement of binary encoding.
/// </summary>
/// <remarks>
/// For the underlying stream writer it is faster to write two chars than a 2 char string.
/// </remarks>
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);
}
}
}
/// <summary>
/// Using a span to escape the string, write strings to stream writer if possible.
/// </summary>
private void EscapeStringSpan(string value)
{
ReadOnlySpan<char> 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
}
}
/// <summary>
/// Use span to escape the string, write only chars to stream writer.
/// </summary>
/// <param name="value"></param>
private void EscapeStringSpanChars(string value)
{
ReadOnlySpan<char> 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<char> 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;
}
/// <summary>
/// Write only chars to stream writer, inline the write sequence for readability.
/// </summary>
/// <param name="value"></param>
private void EscapeStringSpanCharsInline(string value)
{
ReadOnlySpan<char> 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<char> 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<char> 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<char> 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<char, string> m_replace = new Dictionary<char, string>
{
{ '\"', "\\\"" },
{ '\\', "\\\\" },
{ '\n', "\\n" },
{ '\r', "\\r" },
{ '\t', "\\t" },
{ '\b', "\\b" },
{ '\f', "\\f" }
};
#endregion
}
}
@@ -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
}
/// <summary>
/// Use a constructor with external Stream,
/// Use a MemoryStream constructor with external Stream,
/// keep the stream open for more encodings.
/// </summary>
[Test]
public void ConstructorStream()
public void ConstructorMemoryStream()
{
using (var memoryStream = new MemoryStream())
{
ConstructorStream(memoryStream);
}
}
/// <summary>
/// Use a ArraySegmentStream constructor with external Stream,
/// keep the stream open for more encodings.
/// </summary>
[Test]
public void ConstructorArraySegmentStream()
{
using (var memoryStream = new ArraySegmentStream(BufferManager))
{
ConstructorStream(memoryStream);
}
}
/// <summary>
/// Use a RecylableMemoryStream constructor with external Stream,
/// keep the stream open for more encodings.
/// </summary>
[Test]
public void ConstructorRecyclableMemoryStream()
{
var recyclableMemoryStreamManager = new RecyclableMemoryStreamManager(new RecyclableMemoryStreamManager.Options {
BlockSize = BufferManager.MaxSuggestedBufferSize,
});
using (var memoryStream = new RecyclableMemoryStream(recyclableMemoryStreamManager))
{
ConstructorStream(memoryStream);
}
}
/// <summary>
/// Use a constructor with external Stream,
/// keep the stream open for more encodings.
/// </summary>
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<ArgumentException>(() => _ = new StreamWriter(memoryStream));
}
/// <summary>
/// Use a constructor with external ArraySegmentStream,
/// keep the stream open for more encodings.
/// Alternate use of sequence.
/// </summary>
[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<ArgumentException>(() => _ = new StreamWriter(memoryStream));
}
}
/// <summary>
/// Use a constructor with external RecyclableMemoryStream,
/// keep the stream open for more encodings.
/// Alternate use of sequence.
/// </summary>
[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<ArgumentException>(() => _ = new StreamWriter(memoryStream));
}
}
/// <summary>
/// Verify reversible Json encoding.
/// </summary>
[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.
/// </summary>
[Theory]
public void JsonEncodeNonRev(JsonValidationData jsonValidationData)
public void JsonEncodeNonRev(JsonValidationData jsonValidationData, MemoryStreamType memoryStreamType)
{
EncodeJsonVerifyResult(
jsonValidationData.BuiltInType,
memoryStreamType,
jsonValidationData.Instance,
false,
jsonValidationData.ExpectedNonReversible ?? jsonValidationData.ExpectedReversible,
+6 -1
View File
@@ -21,9 +21,14 @@
</PropertyGroup>
<PropertyGroup>
<EnableNETAnalyzers>true</EnableNETAnalyzers>
<AccelerateBuildsInVisualStudio>true</AccelerateBuildsInVisualStudio>
<EnableNETAnalyzers>false</EnableNETAnalyzers>
<CodeAnalysisTreatWarningsAsErrors>false</CodeAnalysisTreatWarningsAsErrors>
</PropertyGroup>
<PropertyGroup Condition="'$(GITHUB_ACTIONS)' == 'true'">
<EnableNETAnalyzers>true</EnableNETAnalyzers>
</PropertyGroup>
<PropertyGroup>
<PackageIcon>images/logo.jpg</PackageIcon>