From 21beb6100f702a594dbb3a0477336667273e003e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 22 Mar 2024 06:26:46 -0700 Subject: [PATCH 1/9] Bump coverlet.collector from 6.0.1 to 6.0.2 (#2560) Bumps [coverlet.collector](https://github.com/coverlet-coverage/coverlet) from 6.0.1 to 6.0.2. - [Release notes](https://github.com/coverlet-coverage/coverlet/releases) - [Commits](https://github.com/coverlet-coverage/coverlet/compare/v6.0.1...v6.0.2) --- updated-dependencies: - dependency-name: coverlet.collector dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../Opc.Ua.Client.ComplexTypes.Tests.csproj | 2 +- Tests/Opc.Ua.Client.Tests/Opc.Ua.Client.Tests.csproj | 2 +- .../Opc.Ua.Configuration.Tests.csproj | 2 +- Tests/Opc.Ua.Core.Tests/Opc.Ua.Core.Tests.csproj | 2 +- Tests/Opc.Ua.Gds.Tests/Opc.Ua.Gds.Tests.csproj | 2 +- Tests/Opc.Ua.PubSub.Tests/Opc.Ua.PubSub.Tests.csproj | 2 +- .../Opc.Ua.Security.Certificates.Tests.csproj | 2 +- Tests/Opc.Ua.Server.Tests/Opc.Ua.Server.Tests.csproj | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Tests/Opc.Ua.Client.ComplexTypes.Tests/Opc.Ua.Client.ComplexTypes.Tests.csproj b/Tests/Opc.Ua.Client.ComplexTypes.Tests/Opc.Ua.Client.ComplexTypes.Tests.csproj index 7aafcb134..bb2402572 100644 --- a/Tests/Opc.Ua.Client.ComplexTypes.Tests/Opc.Ua.Client.ComplexTypes.Tests.csproj +++ b/Tests/Opc.Ua.Client.ComplexTypes.Tests/Opc.Ua.Client.ComplexTypes.Tests.csproj @@ -14,7 +14,7 @@ all runtime; build; native; contentfiles; analyzers - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Tests/Opc.Ua.Client.Tests/Opc.Ua.Client.Tests.csproj b/Tests/Opc.Ua.Client.Tests/Opc.Ua.Client.Tests.csproj index ab4d2a849..f4aa4733f 100644 --- a/Tests/Opc.Ua.Client.Tests/Opc.Ua.Client.Tests.csproj +++ b/Tests/Opc.Ua.Client.Tests/Opc.Ua.Client.Tests.csproj @@ -16,7 +16,7 @@ all runtime; build; native; contentfiles; analyzers - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Tests/Opc.Ua.Configuration.Tests/Opc.Ua.Configuration.Tests.csproj b/Tests/Opc.Ua.Configuration.Tests/Opc.Ua.Configuration.Tests.csproj index 01092c681..ad10318d1 100644 --- a/Tests/Opc.Ua.Configuration.Tests/Opc.Ua.Configuration.Tests.csproj +++ b/Tests/Opc.Ua.Configuration.Tests/Opc.Ua.Configuration.Tests.csproj @@ -15,7 +15,7 @@ all runtime; build; native; contentfiles; analyzers - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Tests/Opc.Ua.Core.Tests/Opc.Ua.Core.Tests.csproj b/Tests/Opc.Ua.Core.Tests/Opc.Ua.Core.Tests.csproj index b75d78987..5cfd6d209 100644 --- a/Tests/Opc.Ua.Core.Tests/Opc.Ua.Core.Tests.csproj +++ b/Tests/Opc.Ua.Core.Tests/Opc.Ua.Core.Tests.csproj @@ -18,7 +18,7 @@ all runtime; build; native; contentfiles; analyzers - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Tests/Opc.Ua.Gds.Tests/Opc.Ua.Gds.Tests.csproj b/Tests/Opc.Ua.Gds.Tests/Opc.Ua.Gds.Tests.csproj index 2cc61c4f5..a9fb79ac3 100644 --- a/Tests/Opc.Ua.Gds.Tests/Opc.Ua.Gds.Tests.csproj +++ b/Tests/Opc.Ua.Gds.Tests/Opc.Ua.Gds.Tests.csproj @@ -19,7 +19,7 @@ all runtime; build; native; contentfiles; analyzers - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Tests/Opc.Ua.PubSub.Tests/Opc.Ua.PubSub.Tests.csproj b/Tests/Opc.Ua.PubSub.Tests/Opc.Ua.PubSub.Tests.csproj index b228e9a8b..576804c4f 100644 --- a/Tests/Opc.Ua.PubSub.Tests/Opc.Ua.PubSub.Tests.csproj +++ b/Tests/Opc.Ua.PubSub.Tests/Opc.Ua.PubSub.Tests.csproj @@ -17,7 +17,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Tests/Opc.Ua.Security.Certificates.Tests/Opc.Ua.Security.Certificates.Tests.csproj b/Tests/Opc.Ua.Security.Certificates.Tests/Opc.Ua.Security.Certificates.Tests.csproj index 7bbbbff21..a37c580d7 100644 --- a/Tests/Opc.Ua.Security.Certificates.Tests/Opc.Ua.Security.Certificates.Tests.csproj +++ b/Tests/Opc.Ua.Security.Certificates.Tests/Opc.Ua.Security.Certificates.Tests.csproj @@ -30,7 +30,7 @@ all runtime; build; native; contentfiles; analyzers - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Tests/Opc.Ua.Server.Tests/Opc.Ua.Server.Tests.csproj b/Tests/Opc.Ua.Server.Tests/Opc.Ua.Server.Tests.csproj index 7a55a50ac..3d32fcc30 100644 --- a/Tests/Opc.Ua.Server.Tests/Opc.Ua.Server.Tests.csproj +++ b/Tests/Opc.Ua.Server.Tests/Opc.Ua.Server.Tests.csproj @@ -16,7 +16,7 @@ all runtime; build; native; contentfiles; analyzers - + all runtime; build; native; contentfiles; analyzers; buildtransitive From 48fd872e6ec272589896401294aaa8e4e8634f2a Mon Sep 17 00:00:00 2001 From: romanett Date: Tue, 26 Mar 2024 18:06:51 +0100 Subject: [PATCH 2/9] fix Role Permissions of Method UnregisterApplication & Add Parameter to CertificateStoreIdentifier OpenStore Method (#2558) Fix allowed Roles of Method Unregister Application allow Application Self Admin Privilge: https://reference.opcfoundation.org/GDS/v105/docs/6.6.8 Add Parameter NoPrivateKeys to OpenStoreMethod of CertificateStoreIdentifier to allow GDS client to update the private key of a store which is not its own store. --- .../Opc.Ua.Gds.Server.Common/ApplicationsNodeManager.cs | 2 +- .../RoleBasedUserManagement/AuthorizationHelper.cs | 1 + .../Security/Certificates/CertificateStoreIdentifier.cs | 7 ++++--- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Libraries/Opc.Ua.Gds.Server.Common/ApplicationsNodeManager.cs b/Libraries/Opc.Ua.Gds.Server.Common/ApplicationsNodeManager.cs index 982915e96..5d92d4ab3 100644 --- a/Libraries/Opc.Ua.Gds.Server.Common/ApplicationsNodeManager.cs +++ b/Libraries/Opc.Ua.Gds.Server.Common/ApplicationsNodeManager.cs @@ -539,7 +539,7 @@ private ServiceResult OnUnregisterApplication( NodeId objectId, NodeId applicationId) { - AuthorizationHelper.HasAuthorization(context, AuthorizationHelper.DiscoveryAdmin); + AuthorizationHelper.HasAuthorization(context, AuthorizationHelper.DiscoveryAdminOrSelfAdmin); Utils.LogInfo("OnUnregisterApplication: {0}", applicationId.ToString()); diff --git a/Libraries/Opc.Ua.Gds.Server.Common/RoleBasedUserManagement/AuthorizationHelper.cs b/Libraries/Opc.Ua.Gds.Server.Common/RoleBasedUserManagement/AuthorizationHelper.cs index 228591e5a..a60920293 100644 --- a/Libraries/Opc.Ua.Gds.Server.Common/RoleBasedUserManagement/AuthorizationHelper.cs +++ b/Libraries/Opc.Ua.Gds.Server.Common/RoleBasedUserManagement/AuthorizationHelper.cs @@ -41,6 +41,7 @@ internal static class AuthorizationHelper { internal static List AuthenticatedUser { get; } = new List { Role.AuthenticatedUser }; internal static List DiscoveryAdmin { get; } = new List { GdsRole.DiscoveryAdmin }; + internal static List DiscoveryAdminOrSelfAdmin { get; } = new List { GdsRole.DiscoveryAdmin, GdsRole.ApplicationSelfAdmin }; internal static List AuthenticatedUserOrSelfAdmin { get; } = new List { Role.AuthenticatedUser, GdsRole.ApplicationSelfAdmin }; internal static List CertificateAuthorityAdminOrSelfAdmin { get; } = new List { GdsRole.CertificateAuthorityAdmin, GdsRole.ApplicationSelfAdmin }; internal static List CertificateAuthorityAdmin { get; } = new List { GdsRole.CertificateAuthorityAdmin }; diff --git a/Stack/Opc.Ua.Core/Security/Certificates/CertificateStoreIdentifier.cs b/Stack/Opc.Ua.Core/Security/Certificates/CertificateStoreIdentifier.cs index a08e22083..403ecc924 100644 --- a/Stack/Opc.Ua.Core/Security/Certificates/CertificateStoreIdentifier.cs +++ b/Stack/Opc.Ua.Core/Security/Certificates/CertificateStoreIdentifier.cs @@ -189,18 +189,19 @@ public virtual ICertificateStore OpenStore() store.Open(this.StorePath); return store; } - /// /// Returns an object to access the store containing the certificates. /// /// /// Opens an instance of the store which contains public keys. /// + /// location of the store + /// Indicates whether NO private keys are found in the store. Default true. /// A disposable instance of the . - public static ICertificateStore OpenStore(string path) + public static ICertificateStore OpenStore(string path, bool noPrivateKeys = true) { ICertificateStore store = CertificateStoreIdentifier.CreateStore(CertificateStoreIdentifier.DetermineStoreType(path)); - store.Open(path); + store.Open(path, noPrivateKeys); return store; } #endregion From ec05e8aaac3587cf0d100511ccee10059010b825 Mon Sep 17 00:00:00 2001 From: Martin Regen Date: Tue, 26 Mar 2024 18:08:03 +0100 Subject: [PATCH 3/9] Accept abort message in early message type check (#2557) In #2510 a message type check was added to catch non UA service connections earlier. But the Abort message type was missing in the check, so an Abort caused a disconnect. However, in such a case the transport can just continue with the next message. --- Stack/Opc.Ua.Core/Stack/Tcp/TcpMessageType.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Stack/Opc.Ua.Core/Stack/Tcp/TcpMessageType.cs b/Stack/Opc.Ua.Core/Stack/Tcp/TcpMessageType.cs index 5043920cc..602d9aeaf 100644 --- a/Stack/Opc.Ua.Core/Stack/Tcp/TcpMessageType.cs +++ b/Stack/Opc.Ua.Core/Stack/Tcp/TcpMessageType.cs @@ -130,7 +130,7 @@ public static bool IsValid(uint messageType) } uint chunkTypeMask = messageType & ChunkTypeMask; - if ((chunkTypeMask != Final) && (chunkTypeMask != Intermediate)) + if ((chunkTypeMask != Final) && (chunkTypeMask != Intermediate) && (chunkTypeMask != Abort)) { return false; } From 2bae83414d6966fdce42a3f334c9f14d7a6babb3 Mon Sep 17 00:00:00 2001 From: Martin Regen Date: Tue, 26 Mar 2024 18:09:32 +0100 Subject: [PATCH 4/9] Set cert validator property may deadlock in semaphore (#2555) When property is set, call internal update to avoid deadlock (2 properties affected) --- .../Security/Certificates/CertificateValidator.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Stack/Opc.Ua.Core/Security/Certificates/CertificateValidator.cs b/Stack/Opc.Ua.Core/Security/Certificates/CertificateValidator.cs index 9de96a07e..61b7e693b 100644 --- a/Stack/Opc.Ua.Core/Security/Certificates/CertificateValidator.cs +++ b/Stack/Opc.Ua.Core/Security/Certificates/CertificateValidator.cs @@ -385,7 +385,7 @@ public ushort MinimumCertificateKeySize if (m_minimumCertificateKeySize != value) { m_minimumCertificateKeySize = value; - ResetValidatedCertificates(); + InternalResetValidatedCertificates(); } } finally @@ -411,14 +411,13 @@ public bool UseValidatedCertificates if (m_useValidatedCertificates != value) { m_useValidatedCertificates = value; - ResetValidatedCertificates(); + InternalResetValidatedCertificates(); } } finally { m_semaphore.Release(); } - } } From d06231a81388c3e49f8c8a5b4a01362862f6dc4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s=20Kleizer?= <67748994+tamaskleizer@users.noreply.github.com> Date: Wed, 27 Mar 2024 14:29:11 +0100 Subject: [PATCH 5/9] Sensitive information redaction (#2545) Added redaction (disabled by default) Added redaction strategies for username, password, endpoint, and exception message. Updated logging to use redaction where it was critical --- Libraries/Opc.Ua.Client/NodeCache.cs | 5 +- Libraries/Opc.Ua.Client/NodeCacheAsync.cs | 5 +- .../Opc.Ua.Client/SessionReconnectHandler.cs | 9 +- .../ApplicationInstance.cs | 2 +- .../Certificates/CertificateValidator.cs | 5 +- .../Certificates/DirectoryCertificateStore.cs | 3 +- .../Configuration/ConfiguredEndpoints.cs | 2 +- Stack/Opc.Ua.Core/Stack/Tcp/TcpServiceHost.cs | 2 +- .../Types/Redaction/IRedactionStrategy.cs | 46 +++++ Stack/Opc.Ua.Core/Types/Redaction/Redact.cs | 48 ++++++ .../Types/Redaction/RedactionStrategies.cs | 89 ++++++++++ .../Types/Redaction/RedactionWrapper.cs | 54 ++++++ .../Redaction/SimpleRedactionStrategy.cs | 144 ++++++++++++++++ Stack/Opc.Ua.Core/Types/Utils/LoggerUtils.cs | 3 +- .../Types/Utils/RedactionTests.cs | 112 ++++++++++++ .../Utils/SimpleRedactionStrategyTests.cs | 159 ++++++++++++++++++ 16 files changed, 673 insertions(+), 15 deletions(-) create mode 100644 Stack/Opc.Ua.Core/Types/Redaction/IRedactionStrategy.cs create mode 100644 Stack/Opc.Ua.Core/Types/Redaction/Redact.cs create mode 100644 Stack/Opc.Ua.Core/Types/Redaction/RedactionStrategies.cs create mode 100644 Stack/Opc.Ua.Core/Types/Redaction/RedactionWrapper.cs create mode 100644 Stack/Opc.Ua.Core/Types/Redaction/SimpleRedactionStrategy.cs create mode 100644 Tests/Opc.Ua.Core.Tests/Types/Utils/RedactionTests.cs create mode 100644 Tests/Opc.Ua.Core.Tests/Types/Utils/SimpleRedactionStrategyTests.cs diff --git a/Libraries/Opc.Ua.Client/NodeCache.cs b/Libraries/Opc.Ua.Client/NodeCache.cs index 66b6d2eb8..00dddacd2 100644 --- a/Libraries/Opc.Ua.Client/NodeCache.cs +++ b/Libraries/Opc.Ua.Client/NodeCache.cs @@ -32,6 +32,7 @@ using System.Linq; using System.Reflection; using System.Threading; +using Opc.Ua.Redaction; namespace Opc.Ua.Client { @@ -131,7 +132,7 @@ public INode Find(ExpandedNodeId nodeId) } catch (Exception e) { - Utils.LogError("Could not fetch node from server: NodeId={0}, Reason='{1}'.", nodeId, e.Message); + Utils.LogError("Could not fetch node from server: NodeId={0}, Reason='{1}'.", nodeId, Redact.Create(e)); // m_nodes[nodeId] = null; return null; } @@ -856,7 +857,7 @@ public Node FetchNode(ExpandedNodeId nodeId) } catch (Exception e) { - Utils.LogError("Could not fetch references for valid node with NodeId = {0}. Error = {1}", nodeId, e.Message); + Utils.LogError("Could not fetch references for valid node with NodeId = {0}. Error = {1}", nodeId, Redact.Create(e)); } InternalWriteLockedAttach(source); diff --git a/Libraries/Opc.Ua.Client/NodeCacheAsync.cs b/Libraries/Opc.Ua.Client/NodeCacheAsync.cs index a7da676d7..ebd8b1ea2 100644 --- a/Libraries/Opc.Ua.Client/NodeCacheAsync.cs +++ b/Libraries/Opc.Ua.Client/NodeCacheAsync.cs @@ -34,6 +34,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; +using Opc.Ua.Redaction; namespace Opc.Ua.Client { @@ -80,7 +81,7 @@ public async Task FindAsync(ExpandedNodeId nodeId, CancellationToken ct = } catch (Exception e) { - Utils.LogError("Could not fetch node from server: NodeId={0}, Reason='{1}'.", nodeId, e.Message); + Utils.LogError("Could not fetch node from server: NodeId={0}, Reason='{1}'.", nodeId, Redact.Create(e)); // m_nodes[nodeId] = null; return null; } @@ -263,7 +264,7 @@ public async Task FetchNodeAsync(ExpandedNodeId nodeId, CancellationToken } catch (Exception e) { - Utils.LogError("Could not fetch references for valid node with NodeId = {0}. Error = {1}", nodeId, e.Message); + Utils.LogError("Could not fetch references for valid node with NodeId = {0}. Error = {1}", nodeId, Redact.Create(e)); } InternalWriteLockedAttach(source); diff --git a/Libraries/Opc.Ua.Client/SessionReconnectHandler.cs b/Libraries/Opc.Ua.Client/SessionReconnectHandler.cs index 0de7799d8..9ec0ac072 100644 --- a/Libraries/Opc.Ua.Client/SessionReconnectHandler.cs +++ b/Libraries/Opc.Ua.Client/SessionReconnectHandler.cs @@ -30,6 +30,7 @@ using System; using System.Threading; using System.Threading.Tasks; +using Opc.Ua.Redaction; namespace Opc.Ua.Client { @@ -339,7 +340,7 @@ await DoReconnectAsync().ConfigureAwait(false)) } catch (Exception exception) { - Utils.LogError(exception, "Unexpected error during reconnect."); + Utils.LogError("Unexpected error during reconnect: {0}", Redact.Create(exception)); } // schedule the next reconnect. @@ -510,17 +511,17 @@ await endpoint.UpdateFromServerAsync( { m_reconnectPeriod = m_baseReconnectPeriod; } - Utils.LogError("Could not reconnect due to failed security check. Request endpoint update from server. {0}", sre.Message); + Utils.LogError("Could not reconnect due to failed security check. Request endpoint update from server. {0}", Redact.Create(sre)); } else { - Utils.LogError("Could not reconnect the Session. {0}", sre.Message); + Utils.LogError("Could not reconnect the Session. {0}", Redact.Create(sre)); } return false; } catch (Exception exception) { - Utils.LogError("Could not reconnect the Session. {0}", exception.Message); + Utils.LogError("Could not reconnect the Session. {0}", Redact.Create(exception)); return false; } finally diff --git a/Libraries/Opc.Ua.Configuration/ApplicationInstance.cs b/Libraries/Opc.Ua.Configuration/ApplicationInstance.cs index da509a13e..64dba21e1 100644 --- a/Libraries/Opc.Ua.Configuration/ApplicationInstance.cs +++ b/Libraries/Opc.Ua.Configuration/ApplicationInstance.cs @@ -999,7 +999,7 @@ private static async Task AddToTrustedStoreAsync(ApplicationConfiguration config } catch (Exception e) { - Utils.LogError(e, "Could not add certificate to trusted peer store."); + Utils.LogError("Could not add certificate to trusted peer store: {0}", Redaction.Redact.Create(e)); } } diff --git a/Stack/Opc.Ua.Core/Security/Certificates/CertificateValidator.cs b/Stack/Opc.Ua.Core/Security/Certificates/CertificateValidator.cs index 61b7e693b..2b445d6fb 100644 --- a/Stack/Opc.Ua.Core/Security/Certificates/CertificateValidator.cs +++ b/Stack/Opc.Ua.Core/Security/Certificates/CertificateValidator.cs @@ -20,6 +20,7 @@ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; +using Opc.Ua.Redaction; using Opc.Ua.Security.Certificates; namespace Opc.Ua @@ -1480,13 +1481,13 @@ public void ValidateDomains(X509Certificate2 serverCertificate, ConfiguredEndpoi { if (serverValidation) { - Utils.LogError(message, endpointUrl.DnsSafeHost); + Utils.LogError(message, Redact.Create(endpointUrl)); } else { // write the invalid certificate to rejected store if specified. Utils.LogCertificate(LogLevel.Error, "Certificate rejected. Reason={0}.", - serverCertificate, serviceResult != null ? serviceResult.ToString() : "Unknown Error"); + serverCertificate, Redact.Create(serviceResult)); SaveCertificate(serverCertificate); } diff --git a/Stack/Opc.Ua.Core/Security/Certificates/DirectoryCertificateStore.cs b/Stack/Opc.Ua.Core/Security/Certificates/DirectoryCertificateStore.cs index 4042b2545..b897ba1e2 100644 --- a/Stack/Opc.Ua.Core/Security/Certificates/DirectoryCertificateStore.cs +++ b/Stack/Opc.Ua.Core/Security/Certificates/DirectoryCertificateStore.cs @@ -19,6 +19,7 @@ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. using System.Text; using System.Threading.Tasks; using Opc.Ua.Security.Certificates; +using Opc.Ua.Redaction; namespace Opc.Ua { @@ -479,7 +480,7 @@ public async Task LoadPrivateKey(string thumbprint, string sub // found a certificate, but some error occurred if (certificateFound) { - Utils.LogError(Utils.TraceMasks.Security, "The private key for the certificate with subject {0} failed to import.", subjectName); + Utils.LogError(Utils.TraceMasks.Security, "The private key for the certificate with subject {0} failed to import.", Redact.Create(subjectName)); if (importException != null) { Utils.LogError(importException, "Certificate import failed."); diff --git a/Stack/Opc.Ua.Core/Stack/Configuration/ConfiguredEndpoints.cs b/Stack/Opc.Ua.Core/Stack/Configuration/ConfiguredEndpoints.cs index 03eb1036d..29cf6b273 100644 --- a/Stack/Opc.Ua.Core/Stack/Configuration/ConfiguredEndpoints.cs +++ b/Stack/Opc.Ua.Core/Stack/Configuration/ConfiguredEndpoints.cs @@ -198,7 +198,7 @@ public static ConfiguredEndpointCollection Load(Stream istrm) } catch (Exception e) { - Utils.LogError(e, "Unexpected error loading ConfiguredEndpoints."); + Utils.LogError("Unexpected error loading ConfiguredEndpoints: {0}", Redaction.Redact.Create(e)); throw; } } diff --git a/Stack/Opc.Ua.Core/Stack/Tcp/TcpServiceHost.cs b/Stack/Opc.Ua.Core/Stack/Tcp/TcpServiceHost.cs index 46d2b3ca7..717053026 100644 --- a/Stack/Opc.Ua.Core/Stack/Tcp/TcpServiceHost.cs +++ b/Stack/Opc.Ua.Core/Stack/Tcp/TcpServiceHost.cs @@ -128,7 +128,7 @@ public List CreateServiceHost( } else { - Utils.LogError("Failed to create endpoint {0} because the transport profile is unsupported.", uri); + Utils.LogError("Failed to create endpoint {0} because the transport profile is unsupported.", Redaction.Redact.Create(uri)); } } diff --git a/Stack/Opc.Ua.Core/Types/Redaction/IRedactionStrategy.cs b/Stack/Opc.Ua.Core/Types/Redaction/IRedactionStrategy.cs new file mode 100644 index 000000000..eb7529f8b --- /dev/null +++ b/Stack/Opc.Ua.Core/Types/Redaction/IRedactionStrategy.cs @@ -0,0 +1,46 @@ +/* ======================================================================== + * 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/ + * ======================================================================*/ + +namespace Opc.Ua.Redaction +{ + /// + /// Represents a redaction (censoring) strategy. + /// + /// + /// Use redaction to hide sensitive data in log messages, exception messages, etc. + /// To set your redaction strategy see . + /// + public interface IRedactionStrategy + { + /// + /// Returns a string representation of without sensitive data. + /// + string Redact(object value); + } +} diff --git a/Stack/Opc.Ua.Core/Types/Redaction/Redact.cs b/Stack/Opc.Ua.Core/Types/Redaction/Redact.cs new file mode 100644 index 000000000..9ad19126f --- /dev/null +++ b/Stack/Opc.Ua.Core/Types/Redaction/Redact.cs @@ -0,0 +1,48 @@ +/* ======================================================================== + * 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/ + * ======================================================================*/ + +namespace Opc.Ua.Redaction +{ + /// + /// Helper class for redacting sensitive data. + /// + public static class Redact + { + /// + /// Creates a wrapper to hold . + /// When is called on the resulting wrapper + /// it will invoke the redaction strategy to censor the + /// sensitive data held by . + /// + public static RedactionWrapper Create(T value) + { + return new RedactionWrapper(value); + } + } +} diff --git a/Stack/Opc.Ua.Core/Types/Redaction/RedactionStrategies.cs b/Stack/Opc.Ua.Core/Types/Redaction/RedactionStrategies.cs new file mode 100644 index 000000000..d5485128f --- /dev/null +++ b/Stack/Opc.Ua.Core/Types/Redaction/RedactionStrategies.cs @@ -0,0 +1,89 @@ +/* ======================================================================== + * 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; + +namespace Opc.Ua.Redaction +{ + /// + /// Collection of the redaction strategies. + /// The redaction (censoring) can be applied to log message parameters, exception messages, and other sensitive data. + ///
+ /// To use a strategy wrap the value with , e.g. + /// + /// Utils.LogDebug("The password is {0}", RedactionWrapper.Create(password)); + /// Utils.LogError("An exception occurred: {0}", RedactionWrapper.Create(exception)); + /// + ///
+ /// + /// The redaction is off by default and can be enabled by implementing + /// and setting it via . + /// + public static partial class RedactionStrategies + { + private static readonly IRedactionStrategy s_fallbackStrategy = new FallbackRedactionStrategy(); + + /// + /// Gets the current redaction strategy. + /// + internal static IRedactionStrategy CurrentStrategy { get; private set; } = s_fallbackStrategy; + + /// + /// Sets the current redaction strategy. + /// + public static void SetStrategy(IRedactionStrategy strategy) + { + if (strategy == null) + { + throw new ArgumentNullException(nameof(strategy)); + } + + CurrentStrategy = strategy; + } + + /// + /// Resets the fallback strategy to the default (empty) implementation. + /// + public static void ResetStrategy() + { + CurrentStrategy = s_fallbackStrategy; + } + + /// + /// Fallback for when no other strategy was set. It returns the string representation of the value. + /// + private class FallbackRedactionStrategy : IRedactionStrategy + { + public string Redact(object value) + { + return value?.ToString() ?? "null"; + } + } + } +} diff --git a/Stack/Opc.Ua.Core/Types/Redaction/RedactionWrapper.cs b/Stack/Opc.Ua.Core/Types/Redaction/RedactionWrapper.cs new file mode 100644 index 000000000..ea73aff97 --- /dev/null +++ b/Stack/Opc.Ua.Core/Types/Redaction/RedactionWrapper.cs @@ -0,0 +1,54 @@ +/* ======================================================================== + * 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/ + * ======================================================================*/ + +namespace Opc.Ua.Redaction +{ + /// + /// Wraps the supplied value and applies the redaction strategy when converting to string. + /// + /// Type of the supplied value. + public class RedactionWrapper + { + private readonly T m_value; + + /// + /// Initializes a new instance of the class. + /// + public RedactionWrapper(T value) + { + m_value = value; + } + + /// + public override string ToString() + { + return RedactionStrategies.CurrentStrategy.Redact(m_value); + } + } +} diff --git a/Stack/Opc.Ua.Core/Types/Redaction/SimpleRedactionStrategy.cs b/Stack/Opc.Ua.Core/Types/Redaction/SimpleRedactionStrategy.cs new file mode 100644 index 000000000..44a70a234 --- /dev/null +++ b/Stack/Opc.Ua.Core/Types/Redaction/SimpleRedactionStrategy.cs @@ -0,0 +1,144 @@ +/* ======================================================================== + * 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.Text; +using Opc.Ua.Redaction; + +namespace Opc.Ua.Types.Redaction +{ + /// + /// Sample implementation of the redaction strategy. + /// + public class SimpleRedactionStrategy : IRedactionStrategy + { + private const int kDefaultMinLength = 8; + private const int kDefaultMaxLength = 16; + private const char kReplacementChar = '*'; + + private readonly int m_minLength; + private readonly int m_maxLength; + + + /// + /// Creates a new instance of the with default lengths. + /// + public SimpleRedactionStrategy() : this(kDefaultMinLength, kDefaultMaxLength) + { } + + /// + /// Creates a new instance of the with default lengths. + /// + public SimpleRedactionStrategy(int minLength, int maxLength) + { + if (minLength < 0) + { + throw new ArgumentOutOfRangeException(nameof(minLength), "Must be a non-negative number"); + } + + if (maxLength < -1) + { + throw new ArgumentOutOfRangeException(nameof(maxLength), "Must be a non-negative number, or -1"); + } + + m_minLength = minLength; + m_maxLength = maxLength; + } + + /// + public string Redact(object value) + { + if (value == null) + { + return "null"; + } + + if (value is Uri uri) + { + return RedactUri(uri); + } + + if (value is UriBuilder uriBuilder) + { + return RedactUri(uriBuilder.Uri); + } + + if (value is Exception exception) + { + return RedactException(exception); + } + + string valueString = value.ToString(); + + if (valueString.Length < m_minLength) + { + return new string(kReplacementChar, m_minLength); + } + + if (m_maxLength == -1 || valueString.Length <= m_maxLength) + { + return new string(kReplacementChar, valueString.Length); + } + + return new string(kReplacementChar, m_maxLength); + } + + private string RedactException(Exception exception) + { + return "An exception of type " + exception.GetType() + " was redacted because it may contain sensitive information."; + } + + private string RedactUri(Uri uri) + { + if (!uri.IsWellFormedOriginalString() || !uri.IsAbsoluteUri || string.IsNullOrEmpty(uri.Host)) + { + return Redact(uri.ToString()); + } + + string redactedHost = Redact(uri.Host); + + StringBuilder sb = new StringBuilder() + .Append(uri.Scheme) + .Append(Uri.SchemeDelimiter) + .Append(redactedHost); + + if (!uri.IsDefaultPort) + { + sb.Append(':').Append(uri.Port); + } + + if (uri.LocalPath != null && !string.Equals(uri.LocalPath, "/", StringComparison.Ordinal)) + { + sb.Append(uri.LocalPath); + } + + return sb.ToString(); + } + } +} diff --git a/Stack/Opc.Ua.Core/Types/Utils/LoggerUtils.cs b/Stack/Opc.Ua.Core/Types/Utils/LoggerUtils.cs index 9dadcc904..47d315300 100644 --- a/Stack/Opc.Ua.Core/Types/Utils/LoggerUtils.cs +++ b/Stack/Opc.Ua.Core/Types/Utils/LoggerUtils.cs @@ -44,6 +44,7 @@ using System.Security.Cryptography.X509Certificates; using System.Text; using Microsoft.Extensions.Logging; +using Opc.Ua.Redaction; namespace Opc.Ua { @@ -161,7 +162,7 @@ public static void LogCertificate(LogLevel logLevel, EventId eventId, string mes { allArgs[i] = args[i]; } - allArgs[argsLength] = certificate.Subject; + allArgs[argsLength] = Redact.Create(certificate.Subject); allArgs[argsLength + 1] = certificate.Thumbprint; Log(logLevel, eventId, builder.ToString(), allArgs); } diff --git a/Tests/Opc.Ua.Core.Tests/Types/Utils/RedactionTests.cs b/Tests/Opc.Ua.Core.Tests/Types/Utils/RedactionTests.cs new file mode 100644 index 000000000..cd628481f --- /dev/null +++ b/Tests/Opc.Ua.Core.Tests/Types/Utils/RedactionTests.cs @@ -0,0 +1,112 @@ +/* ======================================================================== + * 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 NUnit.Framework; +using Opc.Ua.Redaction; + +namespace Opc.Ua.Core.Tests.Types.UtilsTests +{ + [TestFixture, Category("Utils")] + public class RedactionTests + { + [Test] + public void FallbackStrategyIsInvokedWhenNoStrategyWasAdded() + { + RedactionStrategies.ResetStrategy(); + + string original = "Original test string"; + + string result = Redact.Create(original).ToString(); + + Assert.That(result, Is.EqualTo(original)); + } + + [Test] + public void FallbackStrategyIsRedactingNullCorrectly() + { + RedactionStrategies.ResetStrategy(); + + string original = null; + + string result = Redact.Create(original).ToString(); + + Assert.That(result, Is.EqualTo("null")); + } + + [Test] + public void StrategyIsInvokedWhenItExists() + { + RedactionStrategies.ResetStrategy(); + + RedactionStrategies.SetStrategy(new TestRedactionStrategy("int_")); + + int original = 123; + + string result = Redact.Create(original).ToString(); + + Assert.That(result, Is.EqualTo("int_123")); + } + + [Test] + public void TheLastStrategyIsInvokedWhenMultipleWereSet() + { + RedactionStrategies.ResetStrategy(); + + RedactionStrategies.SetStrategy(new TestRedactionStrategy("first_")); + RedactionStrategies.SetStrategy(new TestRedactionStrategy("second_")); + + int originalNumber = 456; + + string resultNumber = Redact.Create(originalNumber).ToString(); + + Assert.That(resultNumber, Is.EqualTo("second_456")); + + string originalString = "test string 890"; + + string resultString = Redact.Create(originalString).ToString(); + + Assert.That(resultString, Is.EqualTo("second_test string 890")); + } + + private class TestRedactionStrategy : IRedactionStrategy + { + private readonly string m_prefix; + + public TestRedactionStrategy(string prefix) + { + m_prefix = prefix; + } + + public string Redact(object value) + { + return $"{m_prefix}{value}"; + } + } + } +} diff --git a/Tests/Opc.Ua.Core.Tests/Types/Utils/SimpleRedactionStrategyTests.cs b/Tests/Opc.Ua.Core.Tests/Types/Utils/SimpleRedactionStrategyTests.cs new file mode 100644 index 000000000..735d88ece --- /dev/null +++ b/Tests/Opc.Ua.Core.Tests/Types/Utils/SimpleRedactionStrategyTests.cs @@ -0,0 +1,159 @@ +/* ======================================================================== + * 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 NUnit.Framework; +using Opc.Ua.Redaction; +using Opc.Ua.Types.Redaction; + +namespace Opc.Ua.Core.Tests.Types.UtilsTests +{ + [TestFixture, Category("Utils")] + internal class SimpleRedactionStrategyTests + { + [SetUp] + public void Setup() + { + RedactionStrategies.SetStrategy(new SimpleRedactionStrategy()); + } + + [TearDown] + public void Teardown() + { + RedactionStrategies.ResetStrategy(); + } + + [Test] + public void ZeroMinimumLengthIsAllowed() + { + _ = new SimpleRedactionStrategy(0, 12); + } + + [Test] + public void ZeroMaximumLengthIsAllowed() + { + _ = new SimpleRedactionStrategy(12, 0); + } + + [Test] + public void NegativeMinimumLengthThrows() + { + Assert.That( + Assert.Throws( + () => _ = new SimpleRedactionStrategy(-1, 0)) + .ParamName, Is.EqualTo("minLength")); + } + + [Test] + public void MaximumLengthLowerThanNegativeOneThrows() + { + Assert.That( + Assert.Throws( + () => _ = new SimpleRedactionStrategy(12, -2)) + .ParamName, Is.EqualTo("maxLength")); + } + + [Test] + public void NegativeOneMaximumLengthMeansNoLimit() + { + var strategy = new SimpleRedactionStrategy(0, -1); + + string original = new string('a', 200); + string expected = new string('*', 200); + + string result = strategy.Redact(original); + + Assert.That(result, Is.EqualTo(expected)); + } + + [Test] + public void RedactString() + { + var redacted = Redact.Create("my long test string"); + + Assert.That(redacted.ToString(), Is.EqualTo("****************")); + } + + [Test] + public void RedactNullString() + { + string original = null; + + string result = Redact.Create(original).ToString(); + + Assert.That(result, Is.EqualTo("null")); + } + + [Test] + public void RedactUri() + { + var redacted = Redact.Create(new Uri("http://example.com:8080")); + + Assert.That(redacted.ToString(), Is.EqualTo("http://***********:8080")); + } + + [Test] + public void RedactUriBuilder() + { + var redacted = Redact.Create(new UriBuilder("test.com/index.html")); + + Assert.That(redacted.ToString(), Is.EqualTo("http://********/index.html")); + } + + [Test] + public void RedactNullUri() + { + Uri uri = null; + + string result = Redact.Create(uri).ToString(); + + Assert.That(result, Is.EqualTo("null")); + } + + [Test] + public void RedactException() + { + var original = new Exception("Test message with $ecret"); + + string result = Redact.Create(original).ToString(); + + Assert.That(result, Does.Not.Contain('$')); + } + + [Test] + public void RedactNullException() + { + Exception exception = null; + + string result = Redact.Create(exception).ToString(); + + Assert.That(result, Is.EqualTo("null")); + } + } +} From d717f67440b675a2196d483877a52cb3d65d957a Mon Sep 17 00:00:00 2001 From: romanett Date: Fri, 29 Mar 2024 10:46:18 +0100 Subject: [PATCH 6/9] GDS: Add Audit Events to GDS and missing Events to ServerConfigurationNode (#2554) * add events TrustListUpdateRequested & CertificateUpdateRequested * add events: CertificateRequested, ApplicationRegistrationChanged, CertificateRequested Make the GDS an Auditing Server by adding all needed Audit Events to the GDS: ApplicationRegistrationChangedAuditEventType https://reference.opcfoundation.org/GDS/v105/docs/6.6.12 CertificateDeliveredAuditEventType https://reference.opcfoundation.org/GDS/v105/docs/7.9.13 CertificateRequestedAuditEventType https://reference.opcfoundation.org/GDS/v105/docs/7.9.12 Add missing evetns to Server ConfigurationNode: TrustListUpdateRequestedAuditEventType https://reference.opcfoundation.org/GDS/v105/docs/7.8.2.10 CertificateUpdateRequestedAuditEventType https://reference.opcfoundation.org/GDS/v105/docs/7.10.13 --- .../ApplicationsNodeManager.cs | 21 +- .../Diagnostics/AuditEvents.cs | 198 ++++++++++++++++++ .../Configuration/ConfigurationNodeManager.cs | 1 + .../Opc.Ua.Server/Configuration/TrustList.cs | 10 +- .../Opc.Ua.Server/Diagnostics/AuditEvents.cs | 100 ++++++++- 5 files changed, 325 insertions(+), 5 deletions(-) create mode 100644 Libraries/Opc.Ua.Gds.Server.Common/Diagnostics/AuditEvents.cs diff --git a/Libraries/Opc.Ua.Gds.Server.Common/ApplicationsNodeManager.cs b/Libraries/Opc.Ua.Gds.Server.Common/ApplicationsNodeManager.cs index 5d92d4ab3..51ede019f 100644 --- a/Libraries/Opc.Ua.Gds.Server.Common/ApplicationsNodeManager.cs +++ b/Libraries/Opc.Ua.Gds.Server.Common/ApplicationsNodeManager.cs @@ -1,5 +1,5 @@ /* ======================================================================== - * Copyright (c) 2005-2020 The OPC Foundation, Inc. All rights reserved. + * Copyright (c) 2005-2024 The OPC Foundation, Inc. All rights reserved. * * OPC Foundation MIT License 1.00 * @@ -35,6 +35,7 @@ using System.Text; using System.Threading.Tasks; using Opc.Ua.Gds.Server.Database; +using Opc.Ua.Gds.Server.Diagnostics; using Opc.Ua.Server; namespace Opc.Ua.Gds.Server @@ -508,6 +509,12 @@ private ServiceResult OnRegisterApplication( applicationId = m_database.RegisterApplication(application); + if (applicationId != null) + { + object[] inputArguments = new object[] { application, applicationId }; + Server.ReportApplicationRegistrationChangedAuditEvent(context, objectId, method, inputArguments); + } + return ServiceResult.Good; } @@ -530,6 +537,9 @@ private ServiceResult OnUpdateApplication( m_database.RegisterApplication(application); + object[] inputArguments = new object[] { application }; + Server.ReportApplicationRegistrationChangedAuditEvent(context, objectId, method, inputArguments); + return ServiceResult.Good; } @@ -564,6 +574,9 @@ private ServiceResult OnUnregisterApplication( m_database.UnregisterApplication(applicationId); + object[] inputArguments = new object[] { applicationId }; + Server.ReportApplicationRegistrationChangedAuditEvent(context, objectId, method, inputArguments); + return ServiceResult.Good; } @@ -825,6 +838,9 @@ private ServiceResult OnStartNewKeyPairRequest( string privateKeyPassword, ref NodeId requestId) { + object[] inputArguments = new object[] { applicationId, certificateGroupId, certificateTypeId, subjectName, domainNames, privateKeyFormat, privateKeyPassword }; + Server.ReportCertificateRequestedAuditEvent(context, objectId, method, inputArguments, certificateGroupId, certificateTypeId); + AuthorizationHelper.HasAuthorization(context, AuthorizationHelper.CertificateAuthorityAdminOrSelfAdmin, applicationId); ; var application = m_database.GetApplication(applicationId); @@ -1183,6 +1199,9 @@ out privateKeyPassword m_request.AcceptRequest(requestId, signedCertificate); + object[] inputArguments = new object[] { applicationId, requestId, signedCertificate, privateKey, issuerCertificates }; + Server.ReportCertificateDeliveredAuditEvent(context, objectId, method, inputArguments); + return ServiceResult.Good; } diff --git a/Libraries/Opc.Ua.Gds.Server.Common/Diagnostics/AuditEvents.cs b/Libraries/Opc.Ua.Gds.Server.Common/Diagnostics/AuditEvents.cs new file mode 100644 index 000000000..9834cc16a --- /dev/null +++ b/Libraries/Opc.Ua.Gds.Server.Common/Diagnostics/AuditEvents.cs @@ -0,0 +1,198 @@ +/* ======================================================================== + * 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 Opc.Ua.Server; + +namespace Opc.Ua.Gds.Server.Diagnostics +{ + public static class AuditEvents + { + /// + /// Raise CertificateDeliveredAudit event + /// + /// The server which reports audit events. + /// The current system context. + /// The id of the object used for the method + /// The method that triggered the audit event. + /// The input arguments used to call the method that triggered the audit event. + public static void ReportCertificateDeliveredAuditEvent( + this IAuditEventServer server, + ISystemContext systemContext, + NodeId objectId, + MethodState method, + object[] inputArguments) + { + try + { + CertificateDeliveredAuditEventState e = new CertificateDeliveredAuditEventState(null); + + TranslationInfo message = new TranslationInfo( + "CertificateUpdateRequestedAuditEvent", + "en-US", + "CertificateUpdateRequestedAuditEvent."); + + e.Initialize( + systemContext, + null, + EventSeverity.Min, + new LocalizedText(message), + true, + DateTime.UtcNow); // initializes Status, ActionTimeStamp, ServerId, ClientAuditEntryId, ClientUserId + + e.SetChildValue(systemContext, Ua.BrowseNames.SourceNode, objectId, false); + e.SetChildValue(systemContext, Ua.BrowseNames.SourceName, "Method/UpdateCertificate", false); + e.SetChildValue(systemContext, Ua.BrowseNames.LocalTime, Utils.GetTimeZoneInfo(), false); + + e.SetChildValue(systemContext, Ua.BrowseNames.MethodId, method?.NodeId, false); + e.SetChildValue(systemContext, Ua.BrowseNames.InputArguments, inputArguments, false); + + server?.ReportAuditEvent(systemContext, e); + } + catch (Exception ex) + { + Utils.LogError(ex, "Error while reporting CertificateDeliveredAuditEventState event."); + } + } + + /// + /// Raise CertificateDeliveredAudit event + /// + /// The server which reports audit events. + /// The current system context. + /// The id of the object used for the method + /// The method that triggered the audit event. + /// The input arguments used to call the method that triggered the audit event. + /// The id of the certificate group + /// the certificate ype id + /// The exception resulted after executing the StartNewKeyPairRequest StartNewSigningRequest method. If null, the operation was successfull. + public static void ReportCertificateRequestedAuditEvent( + this IAuditEventServer server, + ISystemContext systemContext, + NodeId objectId, + MethodState method, + object[] inputArguments, + NodeId certificateGroupId, + NodeId certificateTypeId, + Exception exception = null) + { + try + { + CertificateRequestedAuditEventState e = new CertificateRequestedAuditEventState(null); + + TranslationInfo message = null; + if (exception == null) + { + message = new TranslationInfo( + "CertificateRequestedAuditEvent", + "en-US", + "CertificateRequestedAuditEvent."); + } + else + { + message = new TranslationInfo( + "CertificateRequestedAuditEvent", + "en-US", + $"CertificateRequestedAuditEvent - Exception: {exception.Message}."); + } + + e.Initialize( + systemContext, + null, + EventSeverity.Min, + new LocalizedText(message), + exception == null, + DateTime.UtcNow); // initializes Status, ActionTimeStamp, ServerId, ClientAuditEntryId, ClientUserId + + e.SetChildValue(systemContext, Ua.BrowseNames.SourceNode, objectId, false); + e.SetChildValue(systemContext, Ua.BrowseNames.SourceName, "Method/UpdateCertificate", false); + e.SetChildValue(systemContext, Ua.BrowseNames.LocalTime, Utils.GetTimeZoneInfo(), false); + + e.SetChildValue(systemContext, Ua.BrowseNames.MethodId, method?.NodeId, false); + e.SetChildValue(systemContext, Ua.BrowseNames.InputArguments, inputArguments, false); + + e.SetChildValue(systemContext, BrowseNames.CertificateGroup, certificateGroupId, false); + e.SetChildValue(systemContext, BrowseNames.CertificateType, certificateTypeId, false); + + server?.ReportAuditEvent(systemContext, e); + } + catch (Exception ex) + { + Utils.LogError(ex, "Error while reporting CertificateDeliveredAuditEventState event."); + } + } + + /// + /// Raise ApplicationRegistrationChanged event + /// + /// The server which reports audit events. + /// The current system context. + /// The id of the object used for register Application method + /// The method that triggered the audit event. + /// The input arguments used to call the method that triggered the audit event. + public static void ReportApplicationRegistrationChangedAuditEvent( + this IAuditEventServer server, + ISystemContext systemContext, + NodeId objectId, + MethodState method, + object[] inputArguments) + { + try + { + ApplicationRegistrationChangedAuditEventState e = new ApplicationRegistrationChangedAuditEventState(null); + + var message = new TranslationInfo( + "ApplicationRegistrationChangedAuditEvent", + "en-US", + "ApplicationRegistrationChangedAuditEvent."); + + e.Initialize( + systemContext, + null, + EventSeverity.Min, + new LocalizedText(message), + true, + DateTime.UtcNow); // initializes Status, ActionTimeStamp, ServerId, ClientAuditEntryId, ClientUserId + + e.SetChildValue(systemContext, Ua.BrowseNames.SourceNode, objectId, false); + e.SetChildValue(systemContext, Ua.BrowseNames.SourceName, "Method/UpdateCertificate", false); + e.SetChildValue(systemContext, Ua.BrowseNames.LocalTime, Utils.GetTimeZoneInfo(), false); + + e.SetChildValue(systemContext, Ua.BrowseNames.MethodId, method?.NodeId, false); + e.SetChildValue(systemContext, Ua.BrowseNames.InputArguments, inputArguments, false); + + server?.ReportAuditEvent(systemContext, e); + } + catch (Exception ex) + { + Utils.LogError(ex, "Error while reporting CertificateDeliveredAuditEventState event."); + } + } + } +} diff --git a/Libraries/Opc.Ua.Server/Configuration/ConfigurationNodeManager.cs b/Libraries/Opc.Ua.Server/Configuration/ConfigurationNodeManager.cs index 5973b952c..0350b5522 100644 --- a/Libraries/Opc.Ua.Server/Configuration/ConfigurationNodeManager.cs +++ b/Libraries/Opc.Ua.Server/Configuration/ConfigurationNodeManager.cs @@ -350,6 +350,7 @@ private ServiceResult UpdateCertificate( object[] inputArguments = new object[] { certificateGroupId, certificateTypeId, certificate, issuerCertificates, privateKeyFormat, privateKey }; X509Certificate2 newCert = null; + Server.ReportCertificateUpdateRequestedAuditEvent(context, objectId, method, inputArguments); try { if (certificate == null) diff --git a/Libraries/Opc.Ua.Server/Configuration/TrustList.cs b/Libraries/Opc.Ua.Server/Configuration/TrustList.cs index 3c16d62d5..202f014ec 100644 --- a/Libraries/Opc.Ua.Server/Configuration/TrustList.cs +++ b/Libraries/Opc.Ua.Server/Configuration/TrustList.cs @@ -302,6 +302,8 @@ private ServiceResult CloseAndUpdate( uint fileHandle, ref bool restartRequired) { + object[] inputParameters = new object[] { fileHandle }; + m_node.ReportTrustListUpdateRequestedAuditEvent(context, objectId, "Method/CloseAndUpdate", method.NodeId, inputParameters); HasSecureWriteAccess(context); ServiceResult result = StatusCodes.Good; @@ -416,7 +418,6 @@ private ServiceResult CloseAndUpdate( restartRequired = false; // report the TrustListUpdatedAuditEvent - object[] inputParameters = new object[] { fileHandle }; m_node.ReportTrustListUpdatedAuditEvent(context, objectId, "Method/CloseAndUpdate", method.NodeId, inputParameters, result.StatusCode); return result; @@ -429,6 +430,8 @@ private ServiceResult AddCertificate( byte[] certificate, bool isTrustedCertificate) { + object[] inputParameters = new object[] { certificate, isTrustedCertificate }; + m_node.ReportTrustListUpdateRequestedAuditEvent(context, objectId, "Method/AddCertificate", method.NodeId, inputParameters); HasSecureWriteAccess(context); ServiceResult result = StatusCodes.Good; @@ -471,7 +474,6 @@ private ServiceResult AddCertificate( } // report the TrustListUpdatedAuditEvent - object[] inputParameters = new object[] { certificate, isTrustedCertificate }; m_node.ReportTrustListUpdatedAuditEvent(context, objectId, "Method/AddCertificate", method.NodeId, inputParameters, result.StatusCode); return result; @@ -485,6 +487,9 @@ private ServiceResult RemoveCertificate( string thumbprint, bool isTrustedCertificate) { + object[] inputParameters = new object[] { thumbprint }; + m_node.ReportTrustListUpdateRequestedAuditEvent(context, objectId, "Method/RemoveCertificate", method.NodeId, inputParameters); + HasSecureWriteAccess(context); ServiceResult result = StatusCodes.Good; lock (m_lock) @@ -548,7 +553,6 @@ private ServiceResult RemoveCertificate( } // report the TrustListUpdatedAuditEvent - object[] inputParameters = new object[] { thumbprint }; m_node.ReportTrustListUpdatedAuditEvent(context, objectId, "Method/RemoveCertificate", method.NodeId, inputParameters, result.StatusCode); return result; diff --git a/Libraries/Opc.Ua.Server/Diagnostics/AuditEvents.cs b/Libraries/Opc.Ua.Server/Diagnostics/AuditEvents.cs index 073f10396..07497f9a4 100644 --- a/Libraries/Opc.Ua.Server/Diagnostics/AuditEvents.cs +++ b/Libraries/Opc.Ua.Server/Diagnostics/AuditEvents.cs @@ -1010,7 +1010,7 @@ public static void ReportAuditTransferSubscriptionEvent( /// /// The server which reports audit events. /// The current system context. - /// The id of the object ued for update certificate method + /// The id of the object used for update certificate method /// The method that triggered the audit event. /// The input arguments used to call the method that triggered the audit event. /// The id of the certificate group @@ -1072,6 +1072,55 @@ public static void ReportCertificateUpdatedAuditEvent( } } + /// + /// Raise CertificateUpdateRequestedAudit event + /// + /// The server which reports audit events. + /// The current system context. + /// The id of the object used for update certificate method + /// The method that triggered the audit event. + /// The input arguments used to call the method that triggered the audit event. + public static void ReportCertificateUpdateRequestedAuditEvent( + this IAuditEventServer server, + ISystemContext systemContext, + NodeId objectId, + MethodState method, + object[] inputArguments) + { + try + { + CertificateUpdateRequestedAuditEventState e = new CertificateUpdateRequestedAuditEventState(null); + + TranslationInfo message = new TranslationInfo( + "CertificateUpdateRequestedAuditEvent", + "en-US", + "CertificateUpdateRequestedAuditEvent."); + + + + e.Initialize( + systemContext, + null, + EventSeverity.Min, + new LocalizedText(message), + true, + DateTime.UtcNow); // initializes Status, ActionTimeStamp, ServerId, ClientAuditEntryId, ClientUserId + + e.SetChildValue(systemContext, BrowseNames.SourceNode, objectId, false); + e.SetChildValue(systemContext, BrowseNames.SourceName, "Method/UpdateCertificate", false); + e.SetChildValue(systemContext, BrowseNames.LocalTime, Utils.GetTimeZoneInfo(), false); + + e.SetChildValue(systemContext, BrowseNames.MethodId, method?.NodeId, false); + e.SetChildValue(systemContext, BrowseNames.InputArguments, inputArguments, false); + + server?.ReportAuditEvent(systemContext, e); + } + catch (Exception ex) + { + Utils.LogError(ex, "Error while reporting CertificateUpdateRequestedAuditEvent event."); + } + } + /// /// Report the AuditAddNodesEvent /// @@ -1470,6 +1519,55 @@ public static void ReportTrustListUpdatedAuditEvent( Utils.LogError(ex, "Error while reporting ReportTrustListUpdatedAuditEvent event."); } } + + /// + /// Reports an TrustListUpdatedAudit event. + /// + /// The trustlist node. + /// The current system context + /// The object id where the truest list update methods was called + /// The source name string + /// The id of the method that was called + /// The input parameters of the called method + public static void ReportTrustListUpdateRequestedAuditEvent( + this TrustListState node, + ISystemContext systemContext, + NodeId objectId, + string sourceName, + NodeId methodId, + object[] inputParameters) + { + try + { + TrustListUpdateRequestedAuditEventState e = new TrustListUpdateRequestedAuditEventState(null); + + TranslationInfo message = new TranslationInfo( + "TrustListUpdateRequestedAuditEvent", + "en-US", + $"TrustListUpdateRequestedAuditEvent."); + + e.Initialize( + systemContext, + null, + EventSeverity.Min, + new LocalizedText(message), + true, + DateTime.UtcNow); // initializes Status, ActionTimeStamp, ServerId, ClientAuditEntryId, ClientUserId + + e.SetChildValue(systemContext, BrowseNames.SourceNode, objectId, false); + e.SetChildValue(systemContext, BrowseNames.SourceName, sourceName, false); + e.SetChildValue(systemContext, BrowseNames.LocalTime, Utils.GetTimeZoneInfo(), false); + + e.SetChildValue(systemContext, BrowseNames.MethodId, methodId, false); + e.SetChildValue(systemContext, BrowseNames.InputArguments, inputParameters, false); + + node?.ReportEvent(systemContext, e); + } + catch (Exception ex) + { + Utils.LogError(ex, "Error while reporting TrustListUpdateRequestedAuditEvent event."); + } + } #endregion Report Audit Events #region Private helpers From ebb55ad25e033aeddcc4ed722d7ea2345194331b Mon Sep 17 00:00:00 2001 From: Martin Regen Date: Fri, 29 Mar 2024 11:50:55 +0100 Subject: [PATCH 7/9] Improve ArraySegmentStream with ReadOnlyMemory/Span signatures, avoid extra alloc/copy (#2556) - Implement `int Read(Span buffer)` and `void Write(ReadOnlySpan 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 --- .azurepipelines/test.yml | 9 +- .editorconfig | 6 + .../Stack/Bindings/ArraySegmentStream.cs | 258 +++- .../Stack/Bindings/BufferManager.cs | 46 +- .../Stack/Bindings/BufferSegment.cs | 70 + .../Stack/Bindings/BufferSequence.cs | 77 ++ .../Opc.Ua.Core/Types/Encoders/JsonEncoder.cs | 71 +- .../Types/ComplexTypesCommon.cs | 3 +- .../Types/EncoderTests.cs | 18 +- .../Types/JsonEncoderTests.cs | 24 +- .../Types/MockResolverTests.cs | 19 +- Tests/Opc.Ua.Client.Tests/ClientTest.cs | 15 +- ...rManager.cs => BufferManagerBenchmarks.cs} | 2 +- .../Types/Encoders/BinaryDecoderBenchmarks.cs | 239 ++++ .../Types/Encoders/BinaryEncoderBenchmarks.cs | 247 ++-- .../Types/Encoders/EncodeableTests.cs | 9 +- .../Types/Encoders/EncoderBenchmarks.cs | 201 +++ .../Types/Encoders/EncoderCommon.cs | 71 +- .../Types/Encoders/EncoderTests.cs | 46 +- .../Types/Encoders/JsonEncoderBenchmarks.cs | 1135 ++--------------- .../Encoders/JsonEncoderDateTimeBenchmark.cs | 110 ++ .../JsonEncoderEscapeStringBenchmarks.cs | 860 +++++++++++++ .../Types/Encoders/JsonEncoderTests.cs | 182 ++- common.props | 7 +- 24 files changed, 2435 insertions(+), 1290 deletions(-) create mode 100644 Stack/Opc.Ua.Core/Stack/Bindings/BufferSegment.cs create mode 100644 Stack/Opc.Ua.Core/Stack/Bindings/BufferSequence.cs rename Tests/Opc.Ua.Core.Tests/Types/Bindings/{BufferManager.cs => BufferManagerBenchmarks.cs} (98%) create mode 100644 Tests/Opc.Ua.Core.Tests/Types/Encoders/BinaryDecoderBenchmarks.cs create mode 100644 Tests/Opc.Ua.Core.Tests/Types/Encoders/EncoderBenchmarks.cs create mode 100644 Tests/Opc.Ua.Core.Tests/Types/Encoders/JsonEncoderDateTimeBenchmark.cs create mode 100644 Tests/Opc.Ua.Core.Tests/Types/Encoders/JsonEncoderEscapeStringBenchmarks.cs diff --git a/.azurepipelines/test.yml b/.azurepipelines/test.yml index 089b20227..9a285c095 100644 --- a/.azurepipelines/test.yml +++ b/.azurepipelines/test.yml @@ -22,10 +22,11 @@ jobs: inputs: targetType: filePath filePath: ./.azurepipelines/get-matrix.ps1 - arguments: -FileName azure-pipelines.yml -AgentTable ${{ parameters.agents }} + arguments: -FileName *.Tests.csproj -AgentTable ${{ parameters.agents }} - job: testall${{ parameters.jobnamesuffix }} displayName: Tests (${{ parameters.framework }}) dependsOn: testprep${{ parameters.jobnamesuffix }} + timeoutInMinutes: 120 strategy: matrix: $[dependencies.testprep${{ parameters.jobnamesuffix }}.outputs['testmatrix.jobMatrix'] ] variables: @@ -62,12 +63,12 @@ jobs: displayName: Restore ${{ parameters.configuration }} inputs: command: restore - projects: '**/*.Tests.csproj' + projects: $(file) arguments: '${{ variables.DotCliCommandline }} --configuration ${{ parameters.configuration }}' - task: DotNetCoreCLI@2 displayName: Test ${{ parameters.configuration }} - timeoutInMinutes: 60 + timeoutInMinutes: 30 inputs: command: test - projects: '**/*.Tests.csproj' + projects: $(file) arguments: '--no-restore ${{ variables.DotCliCommandline }} --configuration ${{ parameters.configuration }}' diff --git a/.editorconfig b/.editorconfig index 12e1f7923..1db323f25 100644 --- a/.editorconfig +++ b/.editorconfig @@ -336,6 +336,12 @@ dotnet_diagnostic.IDE1005.severity = # CA1305: Specify IFormatProvider dotnet_diagnostic.CA1305.severity = warning +# CA1819: Properties should not return arrays +dotnet_diagnostic.CA1819.severity = silent + +# CA1721: The property name is confusing given the existence of another method with the same name. +dotnet_diagnostic.CA1721.severity = silent + # exclude generated code [**/Generated/*.cs] generated_code = true diff --git a/Stack/Opc.Ua.Core/Stack/Bindings/ArraySegmentStream.cs b/Stack/Opc.Ua.Core/Stack/Bindings/ArraySegmentStream.cs index 93d61973e..8c4a53147 100644 --- a/Stack/Opc.Ua.Core/Stack/Bindings/ArraySegmentStream.cs +++ b/Stack/Opc.Ua.Core/Stack/Bindings/ArraySegmentStream.cs @@ -10,7 +10,12 @@ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ +#if NETSTANDARD2_1_OR_GREATER || NET5_0_OR_GREATER +#define STREAM_WITH_SPAN_SUPPORT +#endif + using System; +using System.Buffers; using System.IO; using System.Runtime.CompilerServices; @@ -63,6 +68,37 @@ public ArraySegmentStream( SetCurrentBuffer(0); } + + /// + /// Creates a writeable stream that creates buffers as necessary. + /// + /// The buffer manager. + public ArraySegmentStream(BufferManager bufferManager) + : this(bufferManager, bufferManager.MaxSuggestedBufferSize, 0, bufferManager.MaxSuggestedBufferSize) + { + } + #endregion + + #region IDisposable + /// + protected override void Dispose(bool disposing) + { + if (disposing) + { + if (m_buffers != null && m_bufferManager != null) + { + for (int ii = 0; ii < m_buffers.Count; ii++) + { + m_bufferManager.ReturnBuffer(m_buffers[ii].Array, "ArraySegmentStream.Dispose"); + } + m_buffers.Clear(); + m_buffers = null; + } + m_bufferManager = null; + } + + base.Dispose(disposing); + } #endregion #region Public Methods @@ -81,44 +117,77 @@ public BufferCollection GetBuffers(string owner) buffers.Add(new ArraySegment(m_buffers[ii].Array, m_buffers[ii].Offset, GetBufferCount(ii))); } - m_buffers.Clear(); + ClearBuffers(); return buffers; } + + /// + /// Returns sequence of the buffers stored in the stream. + /// + /// + /// The buffers ownership is transferred to the sequence, + /// the stream can be disposed. + /// The new owner is responisble to dispose the sequence after use. + /// + public BufferSequence GetSequence(string owner) + { + if (m_buffers.Count == 0) + { + return new BufferSequence(m_bufferManager, owner, null, ReadOnlySequence.Empty); + } + + int endIndex = GetBufferCount(0); + var firstSegment = new BufferSegment(m_buffers[0].Array, m_buffers[0].Offset, endIndex); + m_bufferManager.TransferBuffer(m_buffers[0].Array, owner); + BufferSegment nextSegment = firstSegment; + for (int ii = 1; ii < m_buffers.Count; ii++) + { + m_bufferManager.TransferBuffer(m_buffers[ii].Array, owner); + endIndex = GetBufferCount(ii); + nextSegment = nextSegment.Append(m_buffers[ii].Array, m_buffers[ii].Offset, endIndex); + } + + var sequence = new ReadOnlySequence(firstSegment, 0, nextSegment, endIndex); + + ClearBuffers(); + + return new BufferSequence(m_bufferManager, owner, firstSegment, sequence); + } #endregion #region Overridden Methods - /// + /// public override bool CanRead { - get { return true; } + get { return m_buffers != null; } } - /// + /// public override bool CanSeek { - get { return true; } + get { return m_buffers != null; } } - /// + /// public override bool CanWrite { - get { return true; } + get { return m_buffers != null; } } - /// + /// public override void Flush() { // nothing to do. } - /// + /// public override long Length { get { return GetAbsoluteLength(); } } - /// + /// public override long Position { get @@ -132,7 +201,7 @@ public override long Position } } - /// + /// public override int ReadByte() { do @@ -148,7 +217,11 @@ public override int ReadByte() // 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 @@ public override int ReadByte() } while (true); } - /// +#if STREAM_WITH_SPAN_SUPPORT + /// + /// Helper to benchmark the performance of the stream. + /// + internal int ReadMemoryStream(Span buffer) => base.Read(buffer); + + /// + public override int Read(Span buffer) + { + int count = buffer.Length; + int offset = 0; + int bytesRead = 0; + + while (count > 0) + { + // check for end of stream. + if (m_currentBuffer.Array == null) + { + return bytesRead; + } + + int bytesLeft = GetBufferCount(m_bufferIndex) - m_currentPosition; + + // copy the bytes requested. + if (bytesLeft > count) + { + m_currentBuffer.AsSpan(m_currentPosition, count).CopyTo(buffer.Slice(offset)); + bytesRead += count; + m_currentPosition += count; + return bytesRead; + } + + // copy the bytes available and move to next buffer. + m_currentBuffer.AsSpan(m_currentPosition, bytesLeft).CopyTo(buffer.Slice(offset)); + bytesRead += bytesLeft; + + offset += bytesLeft; + count -= bytesLeft; + + // move to next buffer. + SetCurrentBuffer(m_bufferIndex + 1); + } + + return bytesRead; + } +#endif + + /// public override int Read(byte[] buffer, int offset, int count) { int bytesRead = 0; @@ -195,7 +315,7 @@ public override int Read(byte[] buffer, int offset, int count) return bytesRead; } - /// + /// public override long Seek(long offset, SeekOrigin origin) { switch (origin) @@ -223,6 +343,13 @@ public override long Seek(long offset, SeekOrigin origin) 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 @@ public override long Seek(long offset, SeekOrigin origin) throw new IOException("Cannot seek beyond the end of the stream."); } - /// + /// public override void SetLength(long value) { throw new NotSupportedException(); } - /// + /// public override void WriteByte(byte value) { do @@ -266,16 +393,12 @@ public override void WriteByte(byte value) // 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 @@ public override void WriteByte(byte value) } while (true); } - /// +#if STREAM_WITH_SPAN_SUPPORT + /// + /// Helper to benchmark the performance of the stream. + /// + internal void WriteMemoryStream(ReadOnlySpan buffer) => base.Write(buffer); + + /// + public override void Write(ReadOnlySpan buffer) + { + int count = buffer.Length; + int offset = 0; + while (count > 0) + { + // check for end of stream. + CheckEndOfStream(); + + int bytesLeft = m_currentBuffer.Count - m_currentPosition; + + // copy the bytes requested. + if (bytesLeft >= count) + { + buffer.Slice(offset, count).CopyTo(m_currentBuffer.AsSpan(m_currentPosition)); + + UpdateCurrentPosition(count); + + return; + } + + // copy the bytes available and move to next buffer. + buffer.Slice(offset, bytesLeft).CopyTo(m_currentBuffer.AsSpan(m_currentPosition)); + + offset += bytesLeft; + count -= bytesLeft; + + // move to next buffer. + SetCurrentBuffer(m_bufferIndex + 1); + } + } +#endif + + /// public override void Write(byte[] buffer, int offset, int count) { while (count > 0) @@ -301,15 +464,7 @@ public override void Write(byte[] buffer, int offset, int count) { 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 @@ public override void Write(byte[] buffer, int offset, int count) } } - /// + /// public override byte[] ToArray() { + if (m_buffers == null) + { + throw new ObjectDisposedException(nameof(ArraySegmentStream)); + } + int absoluteLength = GetAbsoluteLength(); if (absoluteLength == 0) { @@ -353,6 +513,23 @@ public override byte[] ToArray() #endregion #region Private Methods + /// + /// Update the current buffer count. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void UpdateCurrentPosition(int count) + { + m_currentPosition += count; + + if (m_bufferIndex == m_buffers.Count - 1) + { + if (m_endOfLastBuffer < m_currentPosition) + { + m_endOfLastBuffer = m_currentPosition; + } + } + } + /// /// Sets the current buffer. /// @@ -446,6 +623,17 @@ private void CheckEndOfStream() SetCurrentBuffer(m_buffers.Count - 1); } } + + /// + /// Clears the buffers and resets the state variables. + /// + private void ClearBuffers() + { + m_buffers.Clear(); + m_bufferIndex = 0; + m_endOfLastBuffer = 0; + SetCurrentBuffer(0); + } #endregion #region Private Fields diff --git a/Stack/Opc.Ua.Core/Stack/Bindings/BufferManager.cs b/Stack/Opc.Ua.Core/Stack/Bindings/BufferManager.cs index 1e3a50502..730b2b708 100644 --- a/Stack/Opc.Ua.Core/Stack/Bindings/BufferManager.cs +++ b/Stack/Opc.Ua.Core/Stack/Bindings/BufferManager.cs @@ -125,6 +125,7 @@ public BufferManager(string name, int maxBufferSize) ? ArrayPool.Shared : ArrayPool.Create(maxBufferSize + kCookieLength, 4); m_maxBufferSize = maxBufferSize; + m_maxSuggestedBufferSize = DetermineSuggestedBufferSize(maxBufferSize); } #endregion @@ -336,11 +337,54 @@ public void ReturnBuffer(byte[] buffer, string owner) #endif m_arrayPool.Return(buffer); } + + /// + /// Returns the suggested max rent size for data in the buffers. + /// + /// The max buffer size configured. + private int DetermineSuggestedBufferSize(int maxBufferSize) + { + int bufferArrayPoolSize = RoundUpToPowerOfTwo(maxBufferSize); + int maxDataRentSize = RoundUpToPowerOfTwo(maxBufferSize + kCookieLength); + if (bufferArrayPoolSize != maxDataRentSize) + { + Utils.LogWarning("BufferManager: Max buffer size {0} + cookie length {1} may waste memory because it allocates buffers in the next bucket!", maxBufferSize, kCookieLength); + return bufferArrayPoolSize - kCookieLength; + } + return maxBufferSize; + } + + /// + /// Helper to round up to the next power of two. + /// + private int RoundUpToPowerOfTwo(int value) + { + int result = 1; + + while (result < value && result != 0) + { + result <<= 1; + } + + return result; + } + + /// + /// Returns the max size of data in the buffers. + /// + /// + /// Due to the underlying implementation of the ArrayPool, + /// the actual buffer size may be larger than this value. + /// To avoid memory waste, use this value as a guideline + /// for the maximum buffer size when taking buffers. + /// + public int MaxSuggestedBufferSize => m_maxSuggestedBufferSize; #endregion #region Private Fields private readonly string m_name; private readonly int m_maxBufferSize; + private readonly int m_maxSuggestedBufferSize; #if TRACE_MEMORY private int m_buffersTaken = 0; #endif @@ -348,7 +392,7 @@ public void ReturnBuffer(byte[] buffer, string owner) private const byte kCookieLocked = 0xa5; private const byte kCookieUnlocked = 0x5a; #if TRACK_MEMORY - const byte kCookieLength = 5; + private const byte kCookieLength = 5; class Allocation { public int Id; diff --git a/Stack/Opc.Ua.Core/Stack/Bindings/BufferSegment.cs b/Stack/Opc.Ua.Core/Stack/Bindings/BufferSegment.cs new file mode 100644 index 000000000..06750fb5d --- /dev/null +++ b/Stack/Opc.Ua.Core/Stack/Bindings/BufferSegment.cs @@ -0,0 +1,70 @@ +/* ======================================================================== + * Copyright (c) 2005-2024 The OPC Foundation, Inc. All rights reserved. + * + * OPC Foundation MIT License 1.00 + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + * + * The complete license agreement can be found here: + * http://opcfoundation.org/License/MIT/1.00/ + * ======================================================================*/ + +using System; +using System.Buffers; + +namespace Opc.Ua.Bindings +{ + /// + /// Helper to build a ReadOnlySequence from a set of buffers. + /// + public sealed class BufferSegment : ReadOnlySequenceSegment + { + /// + /// Returns the base array of the buffer. + /// + public byte[] Array() => m_array; + + /// + /// Constructor for a buffer segment. + /// + public BufferSegment(byte[] array, int offset, int length) + { + Memory = new ReadOnlyMemory(array, offset, length); + m_array = array; + } + + /// + /// Appends a buffer to the sequence. + /// + public BufferSegment Append(byte[] array, int offset, int length) + { + var segment = new BufferSegment(array, offset, length) { + RunningIndex = RunningIndex + Memory.Length + }; + Next = segment; + return segment; + } + + #region Private Fields + private byte[] m_array; + #endregion + } +} diff --git a/Stack/Opc.Ua.Core/Stack/Bindings/BufferSequence.cs b/Stack/Opc.Ua.Core/Stack/Bindings/BufferSequence.cs new file mode 100644 index 000000000..81a647101 --- /dev/null +++ b/Stack/Opc.Ua.Core/Stack/Bindings/BufferSequence.cs @@ -0,0 +1,77 @@ +/* ======================================================================== + * Copyright (c) 2005-2024 The OPC Foundation, Inc. All rights reserved. + * + * OPC Foundation MIT License 1.00 + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + * + * The complete license agreement can be found here: + * http://opcfoundation.org/License/MIT/1.00/ + * ======================================================================*/ + +using System; +using System.Buffers; + +namespace Opc.Ua.Bindings +{ + /// + /// A class to hold a sequence of buffers until disposed. + /// + public sealed class BufferSequence : IDisposable + { + /// + /// The constructor to create the sequence of buffers. + /// + public BufferSequence(BufferManager bufferManager, string owner, BufferSegment firstSegment, ReadOnlySequence sequence) + { + m_bufferManager = bufferManager; + m_owner = owner; + m_firstSegment = firstSegment; + m_sequence = sequence; + } + + /// + public void Dispose() + { + BufferSegment segment = m_firstSegment; + while (segment != null) + { + m_bufferManager.ReturnBuffer(segment.Array(), m_owner); + segment = (BufferSegment)segment.Next; + } + m_sequence = ReadOnlySequence.Empty; + m_firstSegment = null; + GC.SuppressFinalize(this); + } + + /// + /// Returns a sequence which can be used to access the buffers. + /// + public ReadOnlySequence Sequence => m_sequence; + + #region Private + private BufferManager m_bufferManager; + private BufferSegment m_firstSegment; + private ReadOnlySequence m_sequence; + private string m_owner; + #endregion + } +} diff --git a/Stack/Opc.Ua.Core/Types/Encoders/JsonEncoder.cs b/Stack/Opc.Ua.Core/Types/Encoders/JsonEncoder.cs index a36c06144..cc7fc6f67 100644 --- a/Stack/Opc.Ua.Core/Types/Encoders/JsonEncoder.cs +++ b/Stack/Opc.Ua.Core/Types/Encoders/JsonEncoder.cs @@ -283,41 +283,31 @@ public void SetMappingTables(NamespaceTable namespaceUris, StringTable serverUri /// public string CloseAndReturnText() { - Close(); - if (m_memoryStream == null) + try { - if (m_stream is MemoryStream memoryStream) + InternalClose(false); + if (m_memoryStream == null) { - return Encoding.UTF8.GetString(memoryStream.ToArray()); + if (m_stream is MemoryStream memoryStream) + { + return Encoding.UTF8.GetString(memoryStream.ToArray()); + } + throw new NotSupportedException("Cannot get text from external stream. Use Close or MemoryStream instead."); } - throw new NotSupportedException("Cannot get text from external stream. Use Close or MemoryStream instead."); + return Encoding.UTF8.GetString(m_memoryStream.ToArray()); + } + finally + { + m_writer?.Dispose(); + m_writer = null; } - return Encoding.UTF8.GetString(m_memoryStream.ToArray()); } /// /// Completes writing and returns the text length. + /// The StreamWriter is disposed. /// - public int Close() - { - if (!m_dontWriteClosing) - { - if (m_topLevelIsArray) - { - m_writer.Write(s_rightSquareBracket); - } - else - { - m_writer.Write(s_rightCurlyBrace); - } - } - - m_writer.Flush(); - int length = (int)m_writer.BaseStream.Position; - m_writer.Dispose(); - m_writer = null; - return length; - } + public int Close() => InternalClose(true); #endregion #region IDisposable Members @@ -339,7 +329,7 @@ protected virtual void Dispose(bool disposing) { if (m_writer != null) { - Close(); + InternalClose(true); m_writer = null; } @@ -2580,6 +2570,33 @@ public void WriteArray(string fieldName, object array, int valueRank, BuiltInTyp #endregion #region Private Methods + /// + /// Completes writing and returns the text length. + /// + private int InternalClose(bool dispose) + { + if (!m_dontWriteClosing) + { + if (m_topLevelIsArray) + { + m_writer.Write(s_rightSquareBracket); + } + else + { + m_writer.Write(s_rightCurlyBrace); + } + } + + m_writer.Flush(); + int length = (int)m_writer.BaseStream.Position; + if (dispose) + { + m_writer.Dispose(); + m_writer = null; + } + return length; + } + /// /// Writes a DiagnosticInfo to the stream. /// Ignores InnerDiagnosticInfo field if the nesting level diff --git a/Tests/Opc.Ua.Client.ComplexTypes.Tests/Types/ComplexTypesCommon.cs b/Tests/Opc.Ua.Client.ComplexTypes.Tests/Types/ComplexTypesCommon.cs index 7cf43a05c..75562af6f 100644 --- a/Tests/Opc.Ua.Client.ComplexTypes.Tests/Types/ComplexTypesCommon.cs +++ b/Tests/Opc.Ua.Client.ComplexTypes.Tests/Types/ComplexTypesCommon.cs @@ -276,6 +276,7 @@ public void FillStructWithValues(BaseComplexType structType, bool randomValues) /// protected void EncodeDecodeComplexType( IServiceMessageContext encoderContext, + MemoryStreamType memoryStreamType, EncodingType encoderType, StructureType structureType, ExpandedNodeId nodeId, @@ -291,7 +292,7 @@ object data TestContext.Out.WriteLine(expected); byte[] buffer; - using (var encoderStream = new MemoryStream()) + using (var encoderStream = CreateEncoderMemoryStream(memoryStreamType)) { using (IEncoder encoder = CreateEncoder(encoderType, encoderContext, encoderStream, typeof(DataValue))) { diff --git a/Tests/Opc.Ua.Client.ComplexTypes.Tests/Types/EncoderTests.cs b/Tests/Opc.Ua.Client.ComplexTypes.Tests/Types/EncoderTests.cs index acdc60650..916e27fd3 100644 --- a/Tests/Opc.Ua.Client.ComplexTypes.Tests/Types/EncoderTests.cs +++ b/Tests/Opc.Ua.Client.ComplexTypes.Tests/Types/EncoderTests.cs @@ -30,6 +30,7 @@ using System; using System.Collections.Generic; using NUnit.Framework; +using Opc.Ua.Core.Tests.Types.Encoders; namespace Opc.Ua.Client.ComplexTypes.Tests.Types { @@ -86,6 +87,7 @@ public class ComplexTypesEncoderTests : ComplexTypesCommon [Theory] [Category("ComplexTypes")] public void ReEncodeComplexType( + MemoryStreamType memoryStreamType, EncodingType encoderType, StructureType structureType ) @@ -96,7 +98,7 @@ StructureType structureType object emittedType = Activator.CreateInstance(complexType); var baseType = emittedType as BaseComplexType; FillStructWithValues(baseType, true); - EncodeDecodeComplexType(EncoderContext, encoderType, structureType, nodeId, emittedType); + EncodeDecodeComplexType(EncoderContext, memoryStreamType, encoderType, structureType, nodeId, emittedType); } /// @@ -106,6 +108,7 @@ StructureType structureType [Theory] [Category("ComplexTypes")] public void ReEncodeStructureWithOptionalFieldsComplexType( + MemoryStreamType memoryStreamType, EncodingType encoderType, StructureFieldParameter structureFieldParameter ) @@ -118,17 +121,17 @@ StructureFieldParameter structureFieldParameter var builtInType = structureFieldParameter.BuiltInType; TestContext.Out.WriteLine($"Optional Field: {structureFieldParameter.BuiltInType} is the only value."); baseType[structureFieldParameter.Name] = DataGenerator.GetRandom(builtInType); - EncodeDecodeComplexType(EncoderContext, encoderType, StructureType.StructureWithOptionalFields, nodeId, emittedType); + EncodeDecodeComplexType(EncoderContext, memoryStreamType, encoderType, StructureType.StructureWithOptionalFields, nodeId, emittedType); TestContext.Out.WriteLine($"Optional Field: {structureFieldParameter.BuiltInType} is null."); baseType[structureFieldParameter.Name] = null; - EncodeDecodeComplexType(EncoderContext, encoderType, StructureType.StructureWithOptionalFields, nodeId, emittedType); + EncodeDecodeComplexType(EncoderContext, memoryStreamType, encoderType, StructureType.StructureWithOptionalFields, nodeId, emittedType); TestContext.Out.WriteLine($"Optional Field: {structureFieldParameter.BuiltInType} is null, all other fields have random values."); FillStructWithValues(baseType, true); baseType[structureFieldParameter.Name] = null; - EncodeDecodeComplexType(EncoderContext, encoderType, StructureType.StructureWithOptionalFields, nodeId, emittedType); + EncodeDecodeComplexType(EncoderContext, memoryStreamType, encoderType, StructureType.StructureWithOptionalFields, nodeId, emittedType); TestContext.Out.WriteLine($"Optional Field: {structureFieldParameter.BuiltInType} has random value."); baseType[structureFieldParameter.Name] = DataGenerator.GetRandom(builtInType); - EncodeDecodeComplexType(EncoderContext, encoderType, StructureType.StructureWithOptionalFields, nodeId, emittedType); + EncodeDecodeComplexType(EncoderContext, memoryStreamType, encoderType, StructureType.StructureWithOptionalFields, nodeId, emittedType); } /// @@ -138,6 +141,7 @@ StructureFieldParameter structureFieldParameter [Theory] [Category("ComplexTypes")] public void ReEncodeUnionComplexType( + MemoryStreamType memoryStreamType, EncodingType encoderType, StructureFieldParameter structureFieldParameter ) @@ -150,10 +154,10 @@ StructureFieldParameter structureFieldParameter var builtInType = structureFieldParameter.BuiltInType; TestContext.Out.WriteLine($"Union Field: {structureFieldParameter.BuiltInType} is random."); baseType[structureFieldParameter.Name] = DataGenerator.GetRandom(builtInType); - EncodeDecodeComplexType(EncoderContext, encoderType, StructureType.Union, nodeId, emittedType); + EncodeDecodeComplexType(EncoderContext, memoryStreamType, encoderType, StructureType.Union, nodeId, emittedType); TestContext.Out.WriteLine($"Union Field: {structureFieldParameter.BuiltInType} is null."); baseType[structureFieldParameter.Name] = null; - EncodeDecodeComplexType(EncoderContext, encoderType, StructureType.Union, nodeId, emittedType); + EncodeDecodeComplexType(EncoderContext, memoryStreamType, encoderType, StructureType.Union, nodeId, emittedType); } #endregion Test Methods } diff --git a/Tests/Opc.Ua.Client.ComplexTypes.Tests/Types/JsonEncoderTests.cs b/Tests/Opc.Ua.Client.ComplexTypes.Tests/Types/JsonEncoderTests.cs index b73e471d1..5f12fea60 100644 --- a/Tests/Opc.Ua.Client.ComplexTypes.Tests/Types/JsonEncoderTests.cs +++ b/Tests/Opc.Ua.Client.ComplexTypes.Tests/Types/JsonEncoderTests.cs @@ -120,7 +120,8 @@ public class ComplexTypesJsonEncoderTests : ComplexTypesCommon /// [Theory] public void JsonEncodeStructureRev( - JsonValidationData jsonValidationData) + JsonValidationData jsonValidationData + ) { ExpandedNodeId nodeId; Type complexType; @@ -131,6 +132,7 @@ public void JsonEncodeStructureRev( ExtensionObject extensionObject = CreateExtensionObject(StructureType.Structure, nodeId, emittedType); EncodeJsonComplexTypeVerifyResult( jsonValidationData.BuiltInType, + MemoryStreamType.MemoryStream, extensionObject, true, jsonValidationData.ExpectedNonReversible ?? jsonValidationData.ExpectedReversible, @@ -142,7 +144,8 @@ public void JsonEncodeStructureRev( /// [Theory] public void JsonEncodeStructureNonRev( - JsonValidationData jsonValidationData) + JsonValidationData jsonValidationData + ) { ExpandedNodeId nodeId; Type complexType; @@ -153,6 +156,7 @@ public void JsonEncodeStructureNonRev( ExtensionObject extensionObject = CreateExtensionObject(StructureType.Structure, nodeId, emittedType); EncodeJsonComplexTypeVerifyResult( jsonValidationData.BuiltInType, + MemoryStreamType.ArraySegmentStream, extensionObject, false, jsonValidationData.ExpectedNonReversible ?? jsonValidationData.ExpectedReversible, @@ -165,7 +169,8 @@ public void JsonEncodeStructureNonRev( /// [Theory] public void JsonEncodeOptionalFieldsRev( - JsonValidationData jsonValidationData) + JsonValidationData jsonValidationData + ) { ExpandedNodeId nodeId; Type complexType; @@ -176,6 +181,7 @@ public void JsonEncodeOptionalFieldsRev( ExtensionObject extensionObject = CreateExtensionObject(StructureType.StructureWithOptionalFields, nodeId, emittedType); EncodeJsonComplexTypeVerifyResult( jsonValidationData.BuiltInType, + MemoryStreamType.ArraySegmentStream, extensionObject, true, jsonValidationData.ExpectedNonReversible ?? jsonValidationData.ExpectedReversible, @@ -199,6 +205,7 @@ public void JsonEncodeOptionalFieldsNonRev( ExtensionObject extensionObject = CreateExtensionObject(StructureType.StructureWithOptionalFields, nodeId, emittedType); EncodeJsonComplexTypeVerifyResult( jsonValidationData.BuiltInType, + MemoryStreamType.RecyclableMemoryStream, extensionObject, false, jsonValidationData.ExpectedNonReversible ?? jsonValidationData.ExpectedReversible, @@ -210,7 +217,8 @@ public void JsonEncodeOptionalFieldsNonRev( /// [Theory] public void JsonEncodeUnionRev( - JsonValidationData jsonValidationData) + JsonValidationData jsonValidationData + ) { ExpandedNodeId nodeId; Type complexType; @@ -221,6 +229,7 @@ public void JsonEncodeUnionRev( ExtensionObject extensionObject = CreateExtensionObject(StructureType.Union, nodeId, emittedType); EncodeJsonComplexTypeVerifyResult( jsonValidationData.BuiltInType, + MemoryStreamType.ArraySegmentStream, extensionObject, true, jsonValidationData.ExpectedNonReversible ?? jsonValidationData.ExpectedReversible, @@ -232,7 +241,8 @@ public void JsonEncodeUnionRev( /// [Theory] public void JsonEncodeUnionNonRev( - JsonValidationData jsonValidationData) + JsonValidationData jsonValidationData + ) { ExpandedNodeId nodeId; Type complexType; @@ -243,6 +253,7 @@ public void JsonEncodeUnionNonRev( ExtensionObject extensionObject = CreateExtensionObject(StructureType.Union, nodeId, emittedType); EncodeJsonComplexTypeVerifyResult( jsonValidationData.BuiltInType, + MemoryStreamType.ArraySegmentStream, extensionObject, false, jsonValidationData.ExpectedNonReversible ?? jsonValidationData.ExpectedReversible, @@ -253,6 +264,7 @@ public void JsonEncodeUnionNonRev( #region Private Methods protected void EncodeJsonComplexTypeVerifyResult( BuiltInType builtInType, + MemoryStreamType memoryStreamType, ExtensionObject data, bool useReversibleEncoding, string expected, @@ -269,7 +281,7 @@ bool topLevelIsArray _ = PrettifyAndValidateJson(expected); byte[] buffer; - using (var encoderStream = new MemoryStream()) + using (var encoderStream = CreateEncoderMemoryStream(memoryStreamType)) { using (IEncoder encoder = CreateEncoder( EncodingType.Json, EncoderContext, encoderStream, diff --git a/Tests/Opc.Ua.Client.ComplexTypes.Tests/Types/MockResolverTests.cs b/Tests/Opc.Ua.Client.ComplexTypes.Tests/Types/MockResolverTests.cs index 9bc2e73c0..3b822c94e 100644 --- a/Tests/Opc.Ua.Client.ComplexTypes.Tests/Types/MockResolverTests.cs +++ b/Tests/Opc.Ua.Client.ComplexTypes.Tests/Types/MockResolverTests.cs @@ -36,6 +36,7 @@ using System.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 @@ public void Add(string name, NodeId typeId) /// Test the functionality to create a custom complex type. /// [Theory] - public async Task CreateMockTypeAsync(EncodingType encodingType) + public async Task CreateMockTypeAsync(EncodingType encodingType, MemoryStreamType memoryStreamType) { var mockResolver = new MockResolver(); @@ -255,7 +256,7 @@ public async Task CreateMockTypeAsync(EncodingType encodingType) }; 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 @@ public async Task CreateMockTypeAsync(EncodingType encodingType) _ = 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 @@ public async Task CreateMockTypeAsync(EncodingType encodingType) /// Test the functionality to create a custom complex type. /// [Theory] - public async Task CreateMockArrayTypeAsync(EncodingType encodingType) + public async Task CreateMockArrayTypeAsync(EncodingType encodingType, MemoryStreamType memoryStreamType) { var mockResolver = new MockResolver(); @@ -430,7 +431,7 @@ public async Task CreateMockArrayTypeAsync(EncodingType encodingType) }; 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 @@ public async Task CreateMockArrayTypeAsync(EncodingType encodingType) _ = 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 @@ public async Task CreateMockArrayTypeAsync(EncodingType encodingType) /// Create a complex type with a single scalar or array type, with default and random values . /// [Theory] - public async Task CreateMockSingleTypeAsync(EncodingType encodingType, TestType typeDescription, bool randomValues, TestRanks testRank) + public async Task CreateMockSingleTypeAsync(EncodingType encodingType, MemoryStreamType memoryStreamType, TestType typeDescription, bool randomValues, TestRanks testRank) { SetRepeatedRandomSeed(); @@ -611,7 +612,7 @@ public async Task CreateMockSingleTypeAsync(EncodingType encodingType, TestType }; 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 @@ public async Task CreateMockSingleTypeAsync(EncodingType encodingType, TestType _ = PrettifyAndValidateJson(Encoding.UTF8.GetString(buffer)); // test encoder/decoder - EncodeDecodeComplexType(encoderContext, encodingType, StructureType.Structure, dataTypeNode.NodeId, testType); + EncodeDecodeComplexType(encoderContext, memoryStreamType, encodingType, StructureType.Structure, dataTypeNode.NodeId, testType); // Test extracting type definition diff --git a/Tests/Opc.Ua.Client.Tests/ClientTest.cs b/Tests/Opc.Ua.Client.Tests/ClientTest.cs index 34d440575..3cd771984 100644 --- a/Tests/Opc.Ua.Client.Tests/ClientTest.cs +++ b/Tests/Opc.Ua.Client.Tests/ClientTest.cs @@ -614,13 +614,16 @@ public async Task ReconnectSessionOnAlternateChannel(bool closeChannel) sre = Assert.Throws(() => session1.ReadValue(VariableIds.Server_ServerStatus, typeof(ServerStatusDataType))); // TODO: Both channel should return BadSecureChannelClosed - if (endpoint.EndpointUrl.ToString().StartsWith(Utils.UriSchemeOpcTcp, StringComparison.Ordinal)) + if (!(StatusCodes.BadSecureChannelClosed == sre.StatusCode)) { - Assert.AreEqual(StatusCodes.BadSessionIdInvalid, sre.StatusCode, sre.Message); - } - else - { - Assert.AreEqual(StatusCodes.BadUnknownResponse, sre.StatusCode, sre.Message); + if (endpoint.EndpointUrl.ToString().StartsWith(Utils.UriSchemeOpcTcp, StringComparison.Ordinal)) + { + Assert.AreEqual(StatusCodes.BadSessionIdInvalid, sre.StatusCode, sre.Message); + } + else + { + Assert.AreEqual(StatusCodes.BadUnknownResponse, sre.StatusCode, sre.Message); + } } } diff --git a/Tests/Opc.Ua.Core.Tests/Types/Bindings/BufferManager.cs b/Tests/Opc.Ua.Core.Tests/Types/Bindings/BufferManagerBenchmarks.cs similarity index 98% rename from Tests/Opc.Ua.Core.Tests/Types/Bindings/BufferManager.cs rename to Tests/Opc.Ua.Core.Tests/Types/Bindings/BufferManagerBenchmarks.cs index 719b4d9bc..998f4d7ea 100644 --- a/Tests/Opc.Ua.Core.Tests/Types/Bindings/BufferManager.cs +++ b/Tests/Opc.Ua.Core.Tests/Types/Bindings/BufferManagerBenchmarks.cs @@ -43,7 +43,7 @@ namespace Opc.Ua.Core.Tests.Stack.Bindings public class BufferManagerBenchmarks { //[Params(8192, 65535, 1024 * 1024 - 1)] - public int BufferSize { get; set; } = 65535; + public int BufferSize { get; set; } = TcpMessageLimits.DefaultMaxBufferSize; //[Params( /*8,*/ 64, 256, 1024)] public int Allocations { get; set; } = 256; diff --git a/Tests/Opc.Ua.Core.Tests/Types/Encoders/BinaryDecoderBenchmarks.cs b/Tests/Opc.Ua.Core.Tests/Types/Encoders/BinaryDecoderBenchmarks.cs new file mode 100644 index 000000000..f792af20f --- /dev/null +++ b/Tests/Opc.Ua.Core.Tests/Types/Encoders/BinaryDecoderBenchmarks.cs @@ -0,0 +1,239 @@ +/* ======================================================================== + * Copyright (c) 2005-2024 The OPC Foundation, Inc. All rights reserved. + * + * OPC Foundation MIT License 1.00 + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + * + * The complete license agreement can be found here: + * http://opcfoundation.org/License/MIT/1.00/ + * ======================================================================*/ + +using System.IO; +using BenchmarkDotNet.Attributes; +using NUnit.Framework; +using Opc.Ua.Bindings; + +namespace Opc.Ua.Core.Tests.Types.Encoders +{ + [TestFixture, Category("BinaryEncoder")] + [SetCulture("en-us"), SetUICulture("en-us")] + [NonParallelizable] + [MemoryDiagnoser] + [DisassemblyDiagnoser] + public class BinaryDecoderBenchmarks : EncoderBenchmarks + { + /// + /// Test decoding with internal memory stream. + /// + [Test] + public void BinaryDecoderInternalMemoryStreamTest() + { + using (var binaryDecoder = new BinaryDecoder(m_encodedByteArray, m_context)) + { + TestDecoding(binaryDecoder); + } + } + + /// + /// Test decoding with ArrayPool memory stream. + /// + [Test] + public void BinaryDecoderArraySegmentStreamTest() + { + using (var memoryStream = new ArraySegmentStream(m_encodedBufferList)) + using (var binaryDecoder = new BinaryDecoder(memoryStream, m_context)) + { + TestDecoding(binaryDecoder); + } + } + + /// + /// Test decoding with ArrayPool memory stream that has no span support. + /// + [Test] + public void BinaryDecoderArraySegmentStreamNoSpanTest() + { +#if NET6_0_OR_GREATER + using (var arraySegmentStream = new ArraySegmentStreamNoSpan(m_encodedBufferList)) +#else + using (var arraySegmentStream = new ArraySegmentStream(m_encodedBufferList)) +#endif + using (var binaryDecoder = new BinaryDecoder(arraySegmentStream, m_context)) + { + TestDecoding(binaryDecoder); + } + } + + /// + /// Benchmark decoding with memory stream. + /// + [Benchmark(Baseline = true)] + [Test] + public void BinaryDecoderMemoryStream() + { + using (var memoryStream = new MemoryStream(m_encodedByteArray)) + { + BinaryDecoder_Stream(memoryStream); + } + } + + /// + /// Benchmark decoding with array segment memory stream. + /// + [Benchmark] + [Test] + public void BinaryDecoderArraySegmentStream() + { + using (var arraySegmentStream = new ArraySegmentStream(m_encodedBufferList)) + { + BinaryDecoder_Stream(arraySegmentStream); + } + } + + /// + /// Benchmark decoding with array segment memory stream without span support. + /// + [Benchmark] + [Test] + public void BinaryDecoderArraySegmentStreamNoSpan() + { +#if NET6_0_OR_GREATER + using (var arraySegmentStream = new ArraySegmentStreamNoSpan(m_encodedBufferList)) +#else + using (var arraySegmentStream = new ArraySegmentStream(m_encodedBufferList)) +#endif + { + BinaryDecoder_Stream(arraySegmentStream); + } + } + + #region Private Methods + private void TestStreamDecode(MemoryStream memoryStream) + { + using (var binaryDecoder = new BinaryDecoder(memoryStream, m_context)) + { + TestDecoding(binaryDecoder); + TestDecoding(binaryDecoder); + binaryDecoder.Close(); + } + } + + private void BinaryDecoder_Stream(MemoryStream memoryStream) + { + using (var binaryDecoder = new BinaryDecoder(memoryStream, m_context)) + { + TestDecoding(binaryDecoder); + TestDecoding(binaryDecoder); + binaryDecoder.Close(); + } + } + #endregion + + #region Test Setup + [OneTimeSetUp] + public new void OneTimeSetUp() + { + base.OneTimeSetUp(); + InitializeEncodedTestData(); + } + + [OneTimeTearDown] + public new void OneTimeTearDown() + { + base.OneTimeTearDown(); + } + #endregion + + #region Benchmark Setup + /// + /// Set up some variables for benchmarks. + /// + [GlobalSetup] + public new void GlobalSetup() + { + base.GlobalSetup(); + InitializeEncodedBenchmarkData(); + } + + /// + /// Tear down benchmark variables. + /// + [GlobalCleanup] + public new void GlobalCleanup() + { + base.GlobalCleanup(); + } + #endregion + + #region Private Methods + /// + /// Initialize encoded data. + /// + private void InitializeEncodedTestData() + { + using (var memoryStream = new MemoryStream(StreamBufferSize)) + using (var binaryEncoder = new BinaryEncoder(memoryStream, m_context, true)) + { + TestEncoding(binaryEncoder); + TestEncoding(binaryEncoder); + m_encodedByteArray = binaryEncoder.CloseAndReturnBuffer(); + } + + using (var memoryStream = new ArraySegmentStream(m_bufferManager)) + using (var binaryEncoder = new BinaryEncoder(memoryStream, m_context, true)) + { + TestEncoding(binaryEncoder); + TestEncoding(binaryEncoder); + binaryEncoder.Close(); + m_encodedBufferList = memoryStream.GetBuffers("writer"); + } + } + + /// + /// Initialize encoded data. + /// + private void InitializeEncodedBenchmarkData() + { + using (var memoryStream = new MemoryStream(StreamBufferSize)) + using (var binaryEncoder = new BinaryEncoder(memoryStream, m_context, true)) + { + TestEncoding(binaryEncoder); + TestEncoding(binaryEncoder); + m_encodedByteArray = binaryEncoder.CloseAndReturnBuffer(); + } + + using (var memoryStream = new ArraySegmentStream(m_bufferManager)) + using (var binaryEncoder = new BinaryEncoder(memoryStream, m_context, true)) + { + TestEncoding(binaryEncoder); + TestEncoding(binaryEncoder); + binaryEncoder.Close(); + m_encodedBufferList = memoryStream.GetBuffers("writer"); + } + } + #endregion + + private byte[] m_encodedByteArray; + private BufferCollection m_encodedBufferList; + + } +} diff --git a/Tests/Opc.Ua.Core.Tests/Types/Encoders/BinaryEncoderBenchmarks.cs b/Tests/Opc.Ua.Core.Tests/Types/Encoders/BinaryEncoderBenchmarks.cs index ce10178cd..1dda74441 100644 --- a/Tests/Opc.Ua.Core.Tests/Types/Encoders/BinaryEncoderBenchmarks.cs +++ b/Tests/Opc.Ua.Core.Tests/Types/Encoders/BinaryEncoderBenchmarks.cs @@ -1,5 +1,5 @@ /* ======================================================================== - * Copyright (c) 2005-2023 The OPC Foundation, Inc. All rights reserved. + * Copyright (c) 2005-2024 The OPC Foundation, Inc. All rights reserved. * * OPC Foundation MIT License 1.00 * @@ -27,11 +27,10 @@ * http://opcfoundation.org/License/MIT/1.00/ * ======================================================================*/ -using System; -using System.Collections.Generic; using System.IO; using System.Text; using BenchmarkDotNet.Attributes; +using Microsoft.IO; using NUnit.Framework; using Opc.Ua.Bindings; @@ -42,67 +41,88 @@ namespace Opc.Ua.Core.Tests.Types.Encoders [NonParallelizable] [MemoryDiagnoser] [DisassemblyDiagnoser] - public class BinaryEncoderBenchmarks + public class BinaryEncoderBenchmarks : EncoderBenchmarks { - const int kBufferSize = 4096; - - [Params(1, 2, 8, 64)] - public int PayLoadSize { get; set; } = 64; - /// - /// Benchmark encoding with internal memory stream. + /// Test encoding with internal memory stream. /// - [Benchmark] [Test] - public void BinaryEncoderConstructor2() + public void BinaryEncoderInternalMemoryStreamTest() { using (var binaryEncoder = new BinaryEncoder(m_context)) { TestEncoding(binaryEncoder); - _ = binaryEncoder.CloseAndReturnBuffer(); + var result = binaryEncoder.CloseAndReturnBuffer(); + Assert.NotNull(result); } } + /// - /// Benchmark encoding with ArrayPool memory stream. + /// Test encoding with internal memory stream, + /// uses reflection to get array from memory stream. /// - [Benchmark] [Test] - public void BinaryEncoderConstructor3() + public void BinaryEncoderConstructorStreamwriterReflection2() { - using (var stream = new ArraySegmentStream(m_bufferManager, kBufferSize, 0, kBufferSize)) + using (var memoryStream = new MemoryStream(StreamBufferSize)) + using (var binaryEncoder = new BinaryEncoder(memoryStream, m_context, true)) { - using (var binaryEncoder = new BinaryEncoder(stream, m_context, false)) - { - TestEncoding(binaryEncoder); - _ = binaryEncoder.CloseAndReturnBuffer(); - } + TestEncoding(binaryEncoder); + var result = binaryEncoder.CloseAndReturnBuffer(); + Assert.NotNull(result); } } /// - /// Benchmark encoding with memory stream kept open. + /// Test encoding with ArrayPool memory stream. /// - [Benchmark] - [Test] - public void BinaryEncoderConstructorStreamwriter2() + [Theory] + public void BinaryEncoderArraySegmentStreamTest(bool toArray) { - using (IEncoder binaryEncoder = new BinaryEncoder(m_memoryStream, m_context, true)) + using (var memoryStream = new ArraySegmentStream(m_bufferManager)) { - TestEncoding(binaryEncoder); - int length = binaryEncoder.Close(); - var result = Encoding.UTF8.GetString(m_memoryStream.ToArray()); + TestStreamEncode(memoryStream, toArray); } } /// - /// Encoding test with memory stream kept open. + /// Test encoding with memory stream kept open. /// - [Benchmark] + [Theory] + public void BinaryEncoderMemoryStreamTest(bool toArray) + { + using (var memoryStream = new MemoryStream(StreamBufferSize)) + { + TestStreamEncode(memoryStream, toArray); + } + } + + /// + /// Test encoding with recyclable memory stream kept open. + /// + [Theory] + public void BinaryEncoderRecyclableMemoryStream(bool toArray) + { + using (var memoryStream = new RecyclableMemoryStream(m_memoryManager)) + { + TestStreamEncode(memoryStream, toArray); + } + } + + /// + /// Benchmark encoding with memory stream kept open. + /// + [Benchmark(Baseline = true)] [Test] - public void BinaryEncoderStreamLeaveOpenMemoryStream() + public void BinaryEncoderMemoryStream() { - BinaryEncoder_StreamLeaveOpen(m_memoryStream); + using (var memoryStream = new MemoryStream(StreamBufferSize)) + { + BinaryEncoder_StreamLeaveOpen(memoryStream); + // get buffer for write + _ = memoryStream.ToArray(); + } } /// @@ -110,99 +130,117 @@ public void BinaryEncoderStreamLeaveOpenMemoryStream() /// [Benchmark] [Test] - public void BinaryEncoderStreamLeaveOpenRecyclableMemoryStream() + public void BinaryEncoderRecyclableMemoryStream() { - BinaryEncoder_StreamLeaveOpen(m_recyclableMemoryStream); + using (var recyclableMemoryStream = new RecyclableMemoryStream(m_memoryManager)) + { + BinaryEncoder_StreamLeaveOpen(recyclableMemoryStream); + // get buffers for write + _ = recyclableMemoryStream.GetReadOnlySequence(); + } } /// - /// Encoding test with memory stream kept open. + /// Benchmark encoding with array segment memory stream kept open. /// [Benchmark] [Test] - public void BinaryEncoderStreamLeaveOpenArraySegmentStream() + public void BinaryEncoderArraySegmentStream() { - BinaryEncoder_StreamLeaveOpen(m_arraySegmentStream); + using (var arraySegmentStream = new ArraySegmentStream(m_bufferManager)) + { + BinaryEncoder_StreamLeaveOpen(arraySegmentStream); + // get buffers and return them to buffer manager + var buffers = arraySegmentStream.GetBuffers("writer"); + foreach (var buffer in buffers) + { + m_bufferManager.ReturnBuffer(buffer.Array, "testreturn"); + } + } } /// - /// Benchmark encoding with memory stream kept open, - /// use internal reflection to get string from memory stream. + /// Benchmark encoding with array segment memory stream kept open, + /// to compare the version without span support. /// [Benchmark] [Test] - public void BinaryEncoderConstructorStreamwriterReflection2() + public void BinaryEncoderArraySegmentStreamNoSpan() { - using (var binaryEncoder = new BinaryEncoder(m_memoryStream, m_context, true)) +#if NET6_0_OR_GREATER + using (var arraySegmentStream = new ArraySegmentStreamNoSpan(m_bufferManager)) +#else + using (var arraySegmentStream = new ArraySegmentStream(m_bufferManager)) +#endif { - TestEncoding(binaryEncoder); - var result = binaryEncoder.CloseAndReturnBuffer(); + 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 @@ public void OneTimeTearDown() /// Set up some variables for benchmarks. /// [GlobalSetup] - public void GlobalSetup() - { - // for validating benchmark tests - m_context = new ServiceMessageContext(); - m_memoryStream = new MemoryStream(); - m_memoryManager = new Microsoft.IO.RecyclableMemoryStreamManager(); - m_recyclableMemoryStream = new Microsoft.IO.RecyclableMemoryStream(m_memoryManager); - m_bufferManager = new BufferManager(nameof(BinaryEncoder), kBufferSize); - m_arraySegmentStream = new ArraySegmentStream(m_bufferManager, kBufferSize, 0, kBufferSize); - } + public new void GlobalSetup() => base.GlobalSetup(); /// /// Tear down benchmark variables. /// [GlobalCleanup] - public void GlobalCleanup() - { - m_context = null; - m_memoryStream.Dispose(); - m_memoryStream = null; - m_recyclableMemoryStream.Dispose(); - m_recyclableMemoryStream = null; - m_memoryManager = null; - m_bufferManager = null; - m_arraySegmentStream.Dispose(); - m_arraySegmentStream = null; - } - #endregion - - #region Private Fields - private static NodeId s_nodeId = new NodeId(1234); - private static IList s_list = new List() { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; - private IServiceMessageContext m_context; - private MemoryStream m_memoryStream; - private Microsoft.IO.RecyclableMemoryStreamManager m_memoryManager; - private Microsoft.IO.RecyclableMemoryStream m_recyclableMemoryStream; - private BufferManager m_bufferManager; - private ArraySegmentStream m_arraySegmentStream; + public new void GlobalCleanup() => base.GlobalCleanup(); #endregion } } diff --git a/Tests/Opc.Ua.Core.Tests/Types/Encoders/EncodeableTests.cs b/Tests/Opc.Ua.Core.Tests/Types/Encoders/EncodeableTests.cs index b61557856..9adbfdee2 100644 --- a/Tests/Opc.Ua.Core.Tests/Types/Encoders/EncodeableTests.cs +++ b/Tests/Opc.Ua.Core.Tests/Types/Encoders/EncodeableTests.cs @@ -57,6 +57,7 @@ public class EncodeableTypesTests : EncoderCommon [Category("EncodeableTypes")] public void ActivateEncodeableType( EncodingType encoderType, + MemoryStreamType memoryStreamType, Type systemType ) { @@ -68,13 +69,14 @@ Type systemType 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 @@ Type systemType 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 @@ Type systemType [Category("EncodeableTypes")] public void ActivateEncodeableTypeMatrix( EncodingType encoderType, + MemoryStreamType memoryStreamType, bool encodeAsMatrix, Type systemType ) @@ -167,7 +170,7 @@ Type systemType Matrix matrix = new Matrix(array, builtInType, dimensions); byte[] buffer; - using (var encoderStream = new MemoryStream()) + using (var encoderStream = CreateEncoderMemoryStream(memoryStreamType)) { using (IEncoder encoder = CreateEncoder(encoderType, Context, encoderStream, systemType)) { diff --git a/Tests/Opc.Ua.Core.Tests/Types/Encoders/EncoderBenchmarks.cs b/Tests/Opc.Ua.Core.Tests/Types/Encoders/EncoderBenchmarks.cs new file mode 100644 index 000000000..ff9a60ae4 --- /dev/null +++ b/Tests/Opc.Ua.Core.Tests/Types/Encoders/EncoderBenchmarks.cs @@ -0,0 +1,201 @@ +/* ======================================================================== + * Copyright (c) 2005-2024 The OPC Foundation, Inc. All rights reserved. + * + * OPC Foundation MIT License 1.00 + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + * + * The complete license agreement can be found here: + * http://opcfoundation.org/License/MIT/1.00/ + * ======================================================================*/ + +#pragma warning disable CA5394 // Do not use insecure randomness + +using System; +using System.Collections.Generic; +using BenchmarkDotNet.Attributes; +using Microsoft.IO; +using Opc.Ua.Bindings; + +namespace Opc.Ua.Core.Tests.Types.Encoders +{ + /// + /// Common code for encoder/decoder benchmarks. + /// + public class EncoderBenchmarks + { + public const int StreamBufferSize = 4096; + public const int DataValueCount = 10; + + public EncoderBenchmarks() + { + m_random = new Random(0x62541); + m_nodeId = new NodeId((uint)m_random.Next(50000)); + m_list = new List(); + for (int i = 0; i < DataValueCount; i++) + { + m_list.Add(m_random.Next()); + } + m_values = new List(); + DateTime now = new DateTime(2024, 03, 01, 06, 05, 59, DateTimeKind.Utc); + now += TimeSpan.FromTicks(456789); + for (int i = 0; i < DataValueCount; i++) + { + m_values.Add(new DataValue(new Variant((m_random.NextDouble() - 0.5) * 1000.0), m_random.NextDouble() > 0.1 ? StatusCodes.Good : StatusCodes.BadDataLost, now, now)); + } + } + + [Params(64, 1024)] + public int PayLoadSize { get; set; } = 64; + + #region Private Methods + protected void TestEncoding(IEncoder encoder) + { + var now = DateTime.UtcNow; + int payLoadSize = PayLoadSize; + encoder.WriteInt32("PayloadSize", payLoadSize); + while (payLoadSize-- > 0) + { + encoder.WriteBoolean("Boolean", true); + encoder.WriteByte("Byte", 123); + encoder.WriteUInt16("UInt16", 1234); + encoder.WriteUInt32("UInt32", 123456); + encoder.WriteUInt64("UInt64", 1234566890); + encoder.WriteSByte("Int8", -123); + encoder.WriteInt16("Int16", -1234); + encoder.WriteInt32("Int32", -123456); + encoder.WriteInt64("Int64", -1234566890); + encoder.WriteFloat("Float", 123.456f); + encoder.WriteDouble("Double", 123456.789); + encoder.WriteDateTime("DateTime", now); + encoder.WriteString("String", "The quick brown fox jumps over the lazy dog."); + encoder.WriteNodeId("NodeId", m_nodeId); + encoder.WriteNodeId("ExpandedNodeId", m_nodeId); + encoder.WriteInt32Array("Array", m_list); + encoder.WriteDataValueArray("DataValues", m_values); + } + } + + protected void TestDecoding(IDecoder decoder) + { + var payLoadSize = decoder.ReadInt32("PayloadSize"); + while (payLoadSize-- > 0) + { + _ = decoder.ReadBoolean("Boolean"); + _ = decoder.ReadByte("Byte"); + _ = decoder.ReadUInt16("UInt16"); + _ = decoder.ReadUInt32("UInt32"); + _ = decoder.ReadUInt64("UInt64"); + _ = decoder.ReadSByte("Int8"); + _ = decoder.ReadInt16("Int16"); + _ = decoder.ReadInt32("Int32"); + _ = decoder.ReadInt64("Int64"); + _ = decoder.ReadFloat("Float"); + _ = decoder.ReadDouble("Double"); + _ = decoder.ReadDateTime("DateTime"); + _ = decoder.ReadString("String"); + _ = decoder.ReadNodeId("NodeId"); + _ = decoder.ReadNodeId("ExpandedNodeId"); + _ = decoder.ReadInt32Array("Array"); + _ = decoder.ReadDataValueArray("DataValues"); + } + } + #endregion + + #region Test Setup + public void OneTimeSetUp() + { + // for validating benchmark tests + m_context = new ServiceMessageContext(); + m_memoryManager = new RecyclableMemoryStreamManager(new RecyclableMemoryStreamManager.Options { BlockSize = StreamBufferSize }); + m_bufferManager = new BufferManager(nameof(BinaryEncoder), StreamBufferSize); + } + + public void OneTimeTearDown() + { + m_context = null; + m_memoryManager = null; + m_bufferManager = null; + } + #endregion + + #region Benchmark Setup + /// + /// Set up some variables for benchmarks. + /// + public void GlobalSetup() + { + // for validating benchmark tests + m_context = new ServiceMessageContext(); + m_memoryManager = new RecyclableMemoryStreamManager(new RecyclableMemoryStreamManager.Options { BlockSize = StreamBufferSize }); + m_bufferManager = new BufferManager(nameof(BinaryEncoder), StreamBufferSize); + } + + /// + /// Tear down benchmark variables. + /// + public void GlobalCleanup() + { + m_context = null; + m_memoryManager = null; + m_bufferManager = null; + } + #endregion + + #region Protected Fields + protected Random m_random; + protected NodeId m_nodeId = new NodeId(1234); + protected List m_list; + protected List m_values; + protected IServiceMessageContext m_context; + protected RecyclableMemoryStreamManager m_memoryManager; + protected BufferManager m_bufferManager; + #endregion + } + +#if NET6_0_OR_GREATER + /// + /// Helper class to test ArraySegmentStream without Span support. + /// + public class ArraySegmentStreamNoSpan : ArraySegmentStream + { + public ArraySegmentStreamNoSpan(BufferManager bufferManager) + : base(bufferManager) + { + } + + public ArraySegmentStreamNoSpan(BufferCollection buffers) + : base(buffers) + { + } + + public override int Read(Span buffer) + { + return base.ReadMemoryStream(buffer); + } + + public override void Write(ReadOnlySpan buffer) + { + base.WriteMemoryStream(buffer); + } + } +#endif +} diff --git a/Tests/Opc.Ua.Core.Tests/Types/Encoders/EncoderCommon.cs b/Tests/Opc.Ua.Core.Tests/Types/Encoders/EncoderCommon.cs index c50b97908..3ef4f09e1 100644 --- a/Tests/Opc.Ua.Core.Tests/Types/Encoders/EncoderCommon.cs +++ b/Tests/Opc.Ua.Core.Tests/Types/Encoders/EncoderCommon.cs @@ -37,13 +37,25 @@ using System.Text; using System.Threading; using System.Xml; +using Microsoft.IO; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using NUnit.Framework; +using Opc.Ua.Bindings; using Opc.Ua.Test; namespace Opc.Ua.Core.Tests.Types.Encoders { + /// + /// Supported memory stream types. + /// + public enum MemoryStreamType + { + MemoryStream, + ArraySegmentStream, + RecyclableMemoryStream + } + /// /// Base class for the encoder tests. /// @@ -55,12 +67,16 @@ public class EncoderCommon 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 @@ protected void OneTimeSetUp() 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 @@ protected void SetRandomSeed(int randomSeed) protected string EncodeDataValue( EncodingType encoderType, BuiltInType builtInType, + MemoryStreamType memoryStreamType, object data, bool useReversibleEncoding = true ) @@ -146,7 +165,7 @@ protected string EncodeDataValue( 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 @@ protected string EncodeDataValue( protected void EncodeDecodeDataValue( EncodingType encoderType, BuiltInType builtInType, + MemoryStreamType memoryStreamType, object data ) { @@ -176,7 +196,7 @@ object data 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 @@ object data protected void EncodeDecode( EncodingType encoderType, BuiltInType builtInType, + MemoryStreamType memoryStreamType, object expected ) { @@ -227,7 +248,7 @@ object expected 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 @@ object expected /// protected void EncodeJsonVerifyResult( BuiltInType builtInType, + MemoryStreamType memoryStreamType, object data, bool useReversibleEncoding, string expected, @@ -300,7 +322,7 @@ bool includeDefaults 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 @@ protected string PrettifyAndValidateJson(string json) 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 @@ protected string PrettifyAndValidateJson(string json) return json; } + /// + /// Returns various implementations of a memory stream. + /// + /// + /// A MemoryStream + /// + protected MemoryStream CreateEncoderMemoryStream(MemoryStreamType memoryStreamType) + { + switch (memoryStreamType) + { + case MemoryStreamType.MemoryStream: + return new MemoryStream(kTestBlockSize); + case MemoryStreamType.ArraySegmentStream: + return new ArraySegmentStream(BufferManager); + case MemoryStreamType.RecyclableMemoryStream: + return new RecyclableMemoryStream(RecyclableMemoryManager); + default: + throw new ArgumentOutOfRangeException(nameof(memoryStreamType), memoryStreamType, "Invalid MemoryStreamType specified."); + } + } + /// /// Encoder factory for all encoding types. /// @@ -418,13 +463,13 @@ protected IEncoder CreateEncoder( { case EncodingType.Binary: Assume.That(useReversibleEncoding, "Binary encoding only supports reversible option."); - return new BinaryEncoder(stream, context, false); + return new BinaryEncoder(stream, context, true); case EncodingType.Xml: Assume.That(useReversibleEncoding, "Xml encoding only supports reversible option."); var xmlWriter = XmlWriter.Create(stream); return new XmlEncoder(systemType, xmlWriter, context); case EncodingType.Json: - return new JsonEncoder(context, useReversibleEncoding, topLevelIsArray, stream) { + return new JsonEncoder(context, useReversibleEncoding, topLevelIsArray, stream, true) { IncludeDefaultValues = includeDefaultValues, IncludeDefaultNumberValues = includeDefaultNumbers }; diff --git a/Tests/Opc.Ua.Core.Tests/Types/Encoders/EncoderTests.cs b/Tests/Opc.Ua.Core.Tests/Types/Encoders/EncoderTests.cs index 840b4fced..94fd0518b 100644 --- a/Tests/Opc.Ua.Core.Tests/Types/Encoders/EncoderTests.cs +++ b/Tests/Opc.Ua.Core.Tests/Types/Encoders/EncoderTests.cs @@ -60,7 +60,7 @@ BuiltInType builtInType ) { object defaultValue = TypeInfo.GetDefaultValue(builtInType); - EncodeDecodeDataValue(encoderType, builtInType, defaultValue); + EncodeDecodeDataValue(encoderType, builtInType, MemoryStreamType.MemoryStream, defaultValue); } /// @@ -76,7 +76,7 @@ BuiltInType builtInType { Assume.That(builtInType != BuiltInType.DiagnosticInfo); object randomData = DataGenerator.GetRandom(builtInType); - EncodeDecodeDataValue(encoderType, builtInType, randomData); + EncodeDecodeDataValue(encoderType, builtInType, MemoryStreamType.ArraySegmentStream, randomData); } /// @@ -120,7 +120,7 @@ BuiltInType builtInType break; } }; - EncodeDecode(encoderType, builtInType, randomData); + EncodeDecode(encoderType, builtInType, MemoryStreamType.ArraySegmentStream, randomData); } /// @@ -140,7 +140,7 @@ BuiltInType builtInType // or encoding of extension objects fails. randomData = ExtensionObject.Null; } - EncodeDecode(encoderType, builtInType, randomData); + EncodeDecode(encoderType, builtInType, MemoryStreamType.RecyclableMemoryStream, randomData); } /// @@ -156,7 +156,7 @@ BuiltInType builtInType 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 @@ int arrayLength // ensure different sized arrays contain different data set SetRandomSeed(arrayLength); object randomData = DataGenerator.GetRandomArray(builtInType, useBoundaryValues, arrayLength, true); - EncodeDecodeDataValue(encoderType, builtInType, randomData); + EncodeDecodeDataValue(encoderType, builtInType, MemoryStreamType.ArraySegmentStream, randomData); } /// @@ -191,7 +191,7 @@ BuiltInType builtInType ) { object randomData = DataGenerator.GetRandomArray(builtInType, false, 0, true); - EncodeDecodeDataValue(encoderType, builtInType, randomData); + EncodeDecodeDataValue(encoderType, builtInType, MemoryStreamType.RecyclableMemoryStream, randomData); } /// @@ -206,7 +206,7 @@ EncodingType encoderType { SetRepeatedRandomSeed(); object randomData = DataGenerator.GetRandom(BuiltInType.Variant); - EncodeDecodeDataValue(encoderType, BuiltInType.Variant, randomData); + EncodeDecodeDataValue(encoderType, BuiltInType.Variant, MemoryStreamType.MemoryStream, randomData); } /// @@ -224,11 +224,11 @@ BuiltInType builtInType { 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 @@ int arrayLength { 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 @@ BuiltInType builtInType ) { 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 @@ EncodingType encoderType //new Variant(new TestEnumType[] { TestEnumType.One, TestEnumType.Two, TestEnumType.Hundred }), new Variant(new Int32[] { 2, 3, 10 }, new TypeInfo(BuiltInType.Enumeration, 1)) }; - EncodeDecodeDataValue(encoderType, BuiltInType.Variant, variant); + EncodeDecodeDataValue(encoderType, BuiltInType.Variant, MemoryStreamType.ArraySegmentStream, variant); } /// @@ -302,7 +302,7 @@ BuiltInType builtInType int arrayDimension = RandomSource.NextInt32(99) + 1; Array randomData = DataGenerator.GetRandomArray(builtInType, false, arrayDimension, true); var variant = new Variant(randomData, new TypeInfo(builtInType, 1)); - EncodeDecodeDataValue(encoderType, BuiltInType.Variant, variant); + EncodeDecodeDataValue(encoderType, BuiltInType.Variant, MemoryStreamType.RecyclableMemoryStream, variant); } /// @@ -327,7 +327,7 @@ BuiltInType builtInType 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 @@ BuiltInType builtInType int elements = ElementsFromDimension(dimensions); Array randomData = DataGenerator.GetRandomArray(builtInType, false, elements, true); var variant = new Variant(new Matrix(randomData, builtInType, dimensions)); - EncodeDecodeDataValue(encoderType, BuiltInType.Variant, variant); + EncodeDecodeDataValue(encoderType, BuiltInType.Variant, MemoryStreamType.RecyclableMemoryStream, variant); } /// @@ -403,7 +403,7 @@ BuiltInType builtInType 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 @@ public void EncodeMatrixInArray( 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 @@ BuiltInType builtInType 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 @@ BuiltInType builtInType 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 @@ BuiltInType builtInType [Theory] [Category("Matrix")] public void EncodeMatrixInArrayOverflow( - EncodingType encoderType, - BuiltInType builtInType + EncodingType encoderType, + BuiltInType builtInType ) { Assume.That(builtInType != BuiltInType.Null); @@ -687,7 +687,7 @@ BuiltInType builtInType TestContext.Out.WriteLine(matrix); byte[] buffer; - using (var encoderStream = new MemoryStream()) + using (var encoderStream = CreateEncoderMemoryStream(MemoryStreamType.RecyclableMemoryStream)) { using (IEncoder encoder = CreateEncoder(encoderType, Context, encoderStream, type)) { diff --git a/Tests/Opc.Ua.Core.Tests/Types/Encoders/JsonEncoderBenchmarks.cs b/Tests/Opc.Ua.Core.Tests/Types/Encoders/JsonEncoderBenchmarks.cs index c55495a07..e35d97709 100644 --- a/Tests/Opc.Ua.Core.Tests/Types/Encoders/JsonEncoderBenchmarks.cs +++ b/Tests/Opc.Ua.Core.Tests/Types/Encoders/JsonEncoderBenchmarks.cs @@ -27,13 +27,8 @@ * http://opcfoundation.org/License/MIT/1.00/ * ======================================================================*/ -using System; -using System.Collections.Generic; -using System.Globalization; using System.IO; -using System.Runtime.CompilerServices; using System.Text; -using System.Threading; using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Diagnosers; using Microsoft.IO; @@ -47,57 +42,55 @@ namespace Opc.Ua.Core.Tests.Types.Encoders [NonParallelizable] [MemoryDiagnoser] [DisassemblyDiagnoser] - public class JsonEncoderBenchmarks + public class JsonEncoderBenchmarks : EncoderBenchmarks { - const int kBufferSize = 4096; - - [Params(1, 2, 8, 64)] - public int PayLoadSize { get; set; } = 64; - [Params(128, 512, 1024, 4096, 8192, 65536)] public int StreamSize { get; set; } = 1024; /// /// Benchmark overhead to create StreamWriter, MemoryStream is kept open. /// - [Benchmark] [Test] public void StreamWriter() { - using (var test = new StreamWriter(m_memoryStream, Encoding.UTF8, StreamSize, true)) + using (var memoryStream = new MemoryStream()) + using (var test = new StreamWriter(memoryStream, Encoding.UTF8, StreamSize, true)) + { test.Flush(); + } } /// /// Benchmark overhead to create StreamWriter and MemoryStream. /// - [Benchmark] [Test] public void StreamWriterRecyclableMemoryStream() { using (var memoryStream = new RecyclableMemoryStream(m_memoryManager)) using (var test = new StreamWriter(memoryStream, Encoding.UTF8, StreamSize)) + { test.Flush(); + } } /// /// Benchmark overhead to create StreamWriter and MemoryStream. /// - [Benchmark] [Test] public void StreamWriterMemoryStream() { using (var memoryStream = new MemoryStream()) using (var test = new StreamWriter(memoryStream, Encoding.UTF8, StreamSize)) + { test.Flush(); + } } /// /// Benchmark encoding with internal memory stream. /// - [Benchmark] [Test] - public void JsonEncoderConstructor2() + public void JsonEncoderConstructor() { using (var jsonEncoder = new JsonEncoder(m_context, false)) { @@ -107,1097 +100,185 @@ public void JsonEncoderConstructor2() } /// - /// Benchmark encoding with memory stream kept open. - /// - [Benchmark] - [Test] - public void JsonEncoderStreamLeaveOpenMemoryStream() - { - JsonEncoder_StreamLeaveOpen(m_memoryStream); - } - - /// - /// Benchmark encoding with recyclable memory stream kept open. - /// - [Benchmark] - [Test] - public void JsonEncoderStreamLeaveOpenRecyclableMemoryStream() - { - JsonEncoder_StreamLeaveOpen(m_recyclableMemoryStream); - } - - /// - /// Benchmark encoding with recyclable memory stream kept open. - /// - [Benchmark] - [Test] - public void JsonEncoderStreamLeaveOpenArraySegmentStream() - { - JsonEncoder_StreamLeaveOpen(m_arraySegmentStream); - } - - /// - /// Benchmark encoding with memory stream kept open, - /// use internal reflection to get string from memory stream. - /// - [Benchmark] - [Test] - public void JsonEncoderConstructorStreamwriterReflection2() - { - using (var jsonEncoder = new JsonEncoder(m_context, false, false, m_memoryStream, true, StreamSize)) - { - TestEncoding(jsonEncoder); - var result = jsonEncoder.CloseAndReturnText(); - } - } - - #region Private Methods - private void TestEncoding(IEncoder encoder) - { - int payLoadSize = PayLoadSize; - encoder.WriteByte("Byte", 0); - while (--payLoadSize > 0) - { - 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); - } - } - - private void JsonEncoder_StreamLeaveOpen(MemoryStream stream) - { - int length1; - int length2; - stream.Position = 0; - using (var jsonEncoder = new JsonEncoder(m_context, false, false, stream, true, StreamSize)) - { - TestEncoding(jsonEncoder); - length1 = jsonEncoder.Close(); - } - using (var jsonEncoder = new JsonEncoder(m_context, false, false, stream, true, StreamSize)) - { - TestEncoding(jsonEncoder); - length2 = jsonEncoder.Close(); - } - var result = Encoding.UTF8.GetString(stream.ToArray()); - Assert.NotNull(result); - Assert.AreEqual(length1 * 2, length2); - Assert.AreEqual(length2, result.Length); - } - #endregion - - #region Test Setup - [OneTimeSetUp] - public void OneTimeSetUp() - { - // for validating benchmark tests - m_context = new ServiceMessageContext(); - m_memoryStream = new MemoryStream(); - m_memoryManager = new RecyclableMemoryStreamManager(); - m_recyclableMemoryStream = new RecyclableMemoryStream(m_memoryManager); - m_bufferManager = new BufferManager(nameof(BinaryEncoder), kBufferSize); - m_arraySegmentStream = new ArraySegmentStream(m_bufferManager, kBufferSize, 0, kBufferSize); - } - - [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; - } - #endregion - - #region Benchmark Setup - /// - /// Set up some variables for benchmarks. + /// Test encoding with ArrayPool memory stream. /// - [GlobalSetup] - public void GlobalSetup() - { - // for validating benchmark tests - m_context = new ServiceMessageContext(); - m_memoryStream = new MemoryStream(); - m_memoryManager = new RecyclableMemoryStreamManager(); - m_recyclableMemoryStream = new RecyclableMemoryStream(m_memoryManager); - m_bufferManager = new BufferManager(nameof(BinaryEncoder), kBufferSize); - m_arraySegmentStream = new ArraySegmentStream(m_bufferManager, kBufferSize, 0, kBufferSize); - } - - /// - /// Tear down benchmark variables. - /// - [GlobalCleanup] - public void GlobalCleanup() - { - m_context = null; - m_memoryStream.Dispose(); - m_memoryStream = null; - m_recyclableMemoryStream.Dispose(); - m_recyclableMemoryStream = null; - m_memoryManager = null; - m_bufferManager = null; - m_arraySegmentStream.Dispose(); - m_arraySegmentStream = null; - } - #endregion - - #region Private Fields - private static NodeId s_nodeId = new NodeId(1234); - private static IList s_list = new List() { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; - private IServiceMessageContext m_context; - private MemoryStream m_memoryStream; - private RecyclableMemoryStreamManager m_memoryManager; - private RecyclableMemoryStream m_recyclableMemoryStream; - private BufferManager m_bufferManager; - private ArraySegmentStream m_arraySegmentStream; - #endregion - } - - [TestFixture, Category("JsonEncoder")] - [SetCulture("en-us"), SetUICulture("en-us")] - [NonParallelizable] - [MemoryDiagnoser] - [DisassemblyDiagnoser] - public class JsonEncoderDateTimeBenchmark - { - [Params(0, 4, 7)] - public int DateTimeOmittedZeros { get; set; } = 0; - - [Benchmark] - [Test] - public void DateTimeEncodeToString() - { - _ = m_dateTime.ToUniversalTime().ToString("yyyy-MM-dd'T'HH:mm:ss.FFFFFFFK", CultureInfo.InvariantCulture); - } - - [Benchmark] - [Test] - public void ConvertToUniversalTime() - { - _ = JsonEncoder.ConvertUniversalTimeToString(m_dateTime); - } - - #region Test Setup - [OneTimeSetUp] - public void OneTimeSetUp() - { - // for validating benchmark tests - m_dateTime = DateTime.UtcNow; - } - - [OneTimeTearDown] - public void OneTimeTearDown() - { - } - #endregion - - #region Benchmark Setup - /// - /// Set up some variables for benchmarks. - /// - [GlobalSetup] - public void GlobalSetup() - { - // for validating benchmark tests - switch (DateTimeOmittedZeros) - { - case 4: m_dateTime = new DateTime(2011, 11, 11, 11, 11, 11, 999, DateTimeKind.Utc); break; - case 7: m_dateTime = new DateTime(2011, 11, 11, 11, 11, 11, DateTimeKind.Utc); break; - default: - do - { - m_dateTime = DateTime.UtcNow; - } while (m_dateTime.Ticks % 10 == 0); - break; - } - } - - /// - /// Tear down benchmark variables. - /// - [GlobalCleanup] - public void GlobalCleanup() - { - } - #endregion - - #region Private Fields - private DateTime m_dateTime; - #endregion - } - - [TestFixture, Category("JsonEncoder")] - [SetCulture("en-us"), SetUICulture("en-us")] - [NonParallelizable] - [MemoryDiagnoser] - [DisassemblyDiagnoser(printSource: true)] - public class JsonEncoderEscapeStringBenchmarks - { - public const int InnerLoops = 100; - [DatapointSource] - [Params(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)] - public int StringVariantIndex { get; set; } = 1; - - [DatapointSource] - // for benchmarking with different escaped strings - public static readonly string[] EscapeTestStrings = - { - // The use case without escape characters, plain text - "The quick brown fox jumps over the lazy dog.", - // The use case with many control characters escaped, 1 char spaces - "\" \n \r \t \b \f \\ ", - // The use case with many control characters escaped, 2 char spaces - " \" \n \r \t \b \f \\ ", - // The use case with many control characters escaped, 3 char spaces - " \" \n \r \t \b \f \\ ", - // The use case with many control characters escaped, 5 char spaces - " \" \n \r \t \b \f \\ ", - // The use case with many binary characters escaped, 1 char spaces - "\0 \x01 \x02 \x03 \x04 ", - // The use case with many binary characters escaped, 2 char spaces - " \0 \x01 \x02 \x03 \x04 ", - // The use case with many binary characters escaped, 3 char spaces - " \0 \x01 \x02 \x03 \x04 ", - // The use case with many binary characters escaped, 5 char spaces - " \0 \x01 \x02 \x03 \x04 ", - // The use case with all escape characters and a long string - "Ascii characters, special characters \n \b & control characters \0 \x04 ␀ ␁ ␂ ␃ ␄. This is a test.", - }; - - /// - /// Benchmark encoding of the previous implementation. - /// - [Benchmark(Baseline = true)] - public void EscapeStringLegacy() - { - m_memoryStream.Position = 0; - int repeats = InnerLoops; - while (repeats-- > 0) - { - EscapedStringLegacy(m_testString); - } - m_streamWriter.Flush(); - } - - /// - /// Benchmark encoding of the previous implementation with snall improvement for binary encoding. - /// - [Benchmark] - public void EscapeStringLegacyPlus() - { - m_memoryStream.Position = 0; - int repeats = InnerLoops; - while (repeats-- > 0) - { - EscapedStringLegacyPlus(m_testString); - } - m_streamWriter.Flush(); - } - - /// - /// A new implementation using StringBuilder. - /// - [Benchmark] - public void EscapeStringStringBuilder() - { - m_memoryStream.Position = 0; - int repeats = InnerLoops; - while (repeats-- > 0) - { - EscapeString(m_testString); - } - m_streamWriter.Flush(); - } - - /// - /// A new implementation using ThreadLocal StringBuilder. - /// - [Benchmark] - public void EscapeStringThreadLocal() - { - m_memoryStream.Position = 0; - int repeats = InnerLoops; - while (repeats-- > 0) - { - EscapeStringThreadLocal(m_testString); - } - m_streamWriter.Flush(); - } - - /// - /// A new implementation using ReadOnlySpan. - /// - [Benchmark] - public void EscapeStringSpan() + [Theory] + public void JsonEncoderArraySegmentStreamTest(bool toText) { - m_memoryStream.Position = 0; - int repeats = InnerLoops; - while (repeats-- > 0) + using (var memoryStream = new ArraySegmentStream(m_bufferManager, StreamBufferSize, 0, StreamBufferSize)) { - EscapeStringSpan(m_testString); + TestStreamEncode(memoryStream, toText); } - m_streamWriter.Flush(); } /// - /// A new implementation using ReadOnlySpan and char write. + /// Test encoding with memory stream kept open. /// - [Benchmark] - public void EscapeStringSpanChars() + [Theory] + public void JsonEncoderMemoryStreamTest(bool toText) { - m_memoryStream.Position = 0; - int repeats = InnerLoops; - while (repeats-- > 0) + using (var memoryStream = new MemoryStream(StreamBufferSize)) { - EscapeStringSpanChars(m_testString); + TestStreamEncode(memoryStream, toText); } - m_streamWriter.Flush(); } /// - /// A new implementation using ReadOnlySpan and char write. + /// Test encoding with recyclable memory stream kept open. /// - [Benchmark] - public void EscapeStringSpanCharsInline() + [Theory] + public void JsonEncoderRecyclableMemoryStream(bool toText) { - m_memoryStream.Position = 0; - int repeats = InnerLoops; - while (repeats-- > 0) + using (var memoryStream = new RecyclableMemoryStream(m_memoryManager)) { - EscapeStringSpanCharsInline(m_testString); + TestStreamEncode(memoryStream, toText); } - m_streamWriter.Flush(); } /// - /// A new implementation using ReadOnlySpan and char write with const arrays. + /// Benchmark encoding with memory stream kept open. /// [Benchmark] - public void EscapeStringSpanCharsInlineConst() + [Test] + public void JsonEncoderMemoryStream() { - m_memoryStream.Position = 0; - int repeats = InnerLoops; - while (repeats-- > 0) + using (var memoryStream = new MemoryStream(StreamBufferSize)) { - EscapeStringSpanCharsInlineConst(m_testString); + JsonEncoder_StreamLeaveOpen(memoryStream); + // get buffer for write + _ = memoryStream.ToArray(); } - m_streamWriter.Flush(); } /// - /// A new implementation using ReadOnlySpan and IndexOf. + /// Benchmark encoding with recyclable memory stream kept open. /// [Benchmark] - public void EscapeStringSpanIndex() + [Test] + public void JsonEncoderRecyclableMemoryStream() { - m_memoryStream.Position = 0; - int repeats = InnerLoops; - while (repeats-- > 0) + using (var recyclableMemoryStream = new RecyclableMemoryStream(m_memoryManager)) { - EscapeStringSpanIndex(m_testString); + JsonEncoder_StreamLeaveOpen(recyclableMemoryStream); + // get buffers for write + _ = recyclableMemoryStream.GetReadOnlySequence(); } - m_streamWriter.Flush(); } /// - /// A new implementation using ReadOnlySpan and Dictionary. + /// Benchmark encoding with recyclable memory stream kept open. /// [Benchmark] - public void EscapeStringSpanDict() - { - m_memoryStream.Position = 0; - int repeats = InnerLoops; - while (repeats-- > 0) - { - EscapeStringSpanDict(m_testString); - } - m_streamWriter.Flush(); - } - - [Theory] - [TestCase("No Escape chars", 0)] - [TestCase("control chars escaped, 1 char space", 1)] - [TestCase("control chars escaped, 2 char spaces", 2)] - [TestCase("control chars escaped, 3 char spaces", 3)] - [TestCase("control chars escaped, 5 char spaces", 4)] - [TestCase("binary chars escaped, 1 char space", 5)] - [TestCase("binary chars escaped, 2 char spaces", 6)] - [TestCase("binary chars escaped, 3 char spaces", 7)] - [TestCase("binary chars escaped, 5 char spaces", 8)] - [TestCase("all escape chars and long string", 9)] - public void EscapeStringValidation(string name, int index) - { - m_testString = EscapeTestStrings[index]; - TestContext.Out.WriteLine(m_testString); - var testArray = m_testString.ToCharArray(); - - m_memoryStream.Position = 0; - EscapeStringLegacy(); - m_streamWriter.Flush(); - byte[] resultLegacy = m_memoryStream.ToArray(); - TestContext.Out.WriteLine(Encoding.UTF8.GetString(resultLegacy)); - - m_memoryStream.Position = 0; - EscapeStringLegacyPlus(); - m_streamWriter.Flush(); - byte[] resultLegacyPlus = m_memoryStream.ToArray(); - TestContext.Out.WriteLine(Encoding.UTF8.GetString(resultLegacyPlus)); - - m_memoryStream.Position = 0; - EscapeStringStringBuilder(); - m_streamWriter.Flush(); - byte[] result = m_memoryStream.ToArray(); - TestContext.Out.WriteLine(Encoding.UTF8.GetString(result)); - - m_memoryStream.Position = 0; - EscapeStringThreadLocal(); - m_streamWriter.Flush(); - byte[] resultThreadLocal = m_memoryStream.ToArray(); - TestContext.Out.WriteLine(Encoding.UTF8.GetString(resultThreadLocal)); - - m_memoryStream.Position = 0; - EscapeStringSpan(m_testString); - m_streamWriter.Flush(); - byte[] resultSpan = m_memoryStream.ToArray(); - TestContext.Out.WriteLine(Encoding.UTF8.GetString(resultSpan)); - - m_memoryStream.Position = 0; - EscapeStringSpanChars(m_testString); - m_streamWriter.Flush(); - byte[] resultSpanChars = m_memoryStream.ToArray(); - TestContext.Out.WriteLine(Encoding.UTF8.GetString(resultSpanChars)); - - m_memoryStream.Position = 0; - EscapeStringSpanCharsInline(m_testString); - m_streamWriter.Flush(); - byte[] resultSpanCharsInline = m_memoryStream.ToArray(); - TestContext.Out.WriteLine(Encoding.UTF8.GetString(resultSpanCharsInline)); - - m_memoryStream.Position = 0; - EscapeStringSpanCharsInlineConst(m_testString); - m_streamWriter.Flush(); - byte[] resultSpanCharsInlineConst = m_memoryStream.ToArray(); - TestContext.Out.WriteLine(Encoding.UTF8.GetString(resultSpanCharsInlineConst)); - - m_memoryStream.Position = 0; - EscapeStringSpanIndex(m_testString); - m_streamWriter.Flush(); - byte[] resultSpanIndex = m_memoryStream.ToArray(); - TestContext.Out.WriteLine(Encoding.UTF8.GetString(resultSpanIndex)); - - m_memoryStream.Position = 0; - EscapeStringSpanDict(m_testString); - m_streamWriter.Flush(); - byte[] resultSpanDict = m_memoryStream.ToArray(); - TestContext.Out.WriteLine(Encoding.UTF8.GetString(resultSpanDict)); - - Assert.IsTrue(Utils.IsEqual(resultLegacy, result)); - Assert.IsTrue(Utils.IsEqual(resultLegacy, resultLegacyPlus)); - Assert.IsTrue(Utils.IsEqual(resultLegacy, resultThreadLocal)); - Assert.IsTrue(Utils.IsEqual(resultLegacy, resultSpan)); - Assert.IsTrue(Utils.IsEqual(resultLegacy, resultSpanChars)); - Assert.IsTrue(Utils.IsEqual(resultLegacy, resultSpanCharsInline)); - Assert.IsTrue(Utils.IsEqual(resultLegacy, resultSpanCharsInlineConst)); - Assert.IsTrue(Utils.IsEqual(resultLegacy, resultSpanIndex)); - Assert.IsTrue(Utils.IsEqual(resultLegacy, resultSpanDict)); - } - - #region Test Setup - [OneTimeSetUp] - public void OneTimeSetUp() - { - m_memoryManager = new RecyclableMemoryStreamManager(); - m_memoryStream = new RecyclableMemoryStream(m_memoryManager); - m_streamWriter = new StreamWriter(m_memoryStream, new UTF8Encoding(false), m_streamSize, false); - } - - [OneTimeTearDown] - public void OneTimeTearDown() - { - m_streamWriter?.Dispose(); - m_streamWriter = null; - m_memoryStream?.Dispose(); - m_memoryStream = null; - m_memoryManager = null; - } - #endregion - - #region Benchmark Setup - /// 4 - /// Set up some variables for benchmarks. - /// - [GlobalSetup] - public void GlobalSetup() - { - m_memoryManager = new RecyclableMemoryStreamManager(); - m_memoryStream = new RecyclableMemoryStream(m_memoryManager); - m_streamWriter = new StreamWriter(m_memoryStream, new UTF8Encoding(false), m_streamSize, false); - m_testString = EscapeTestStrings[StringVariantIndex - 1]; - } - - [GlobalCleanup] - public void GlobalCleanup() - { - m_streamWriter?.Dispose(); - m_streamWriter = null; - m_memoryStream?.Dispose(); - m_memoryStream = null; - m_memoryManager = null; - } - #endregion - - #region Private Methods - /// - /// Version used previously in JsonEncoder. - /// - private void EscapedStringLegacy(string value) - { - foreach (char ch in value) - { - bool found = false; - - for (int ii = 0; ii < m_specialChars.Length; ii++) - { - if (m_specialChars[ii] == ch) - { - m_streamWriter.Write('\\'); - m_streamWriter.Write(m_substitution[ii]); - found = true; - break; - } - } - - if (!found) - { - if (ch < 32) - { - m_streamWriter.Write("\\u"); - m_streamWriter.Write("{0:X4}", (int)ch); - continue; - } - - m_streamWriter.Write(ch); - } - } - } - - /// - /// Version used previously in JsonEncoder plus improvement of binary encoding. - /// - /// - /// For the underlying stream writer it is faster to write two chars than a 2 char string. - /// - private void EscapedStringLegacyPlus(string value) + [Test] + public void JsonEncoderArraySegmentStream() { - foreach (char ch in value) + using (var arraySegmentStream = new ArraySegmentStream(m_bufferManager)) { - bool found = false; - - for (int ii = 0; ii < m_specialChars.Length; ii++) + JsonEncoder_StreamLeaveOpen(arraySegmentStream); + // get buffers and return them to buffer manager + var buffers = arraySegmentStream.GetBuffers("writer"); + foreach (var buffer in buffers) { - 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); + m_bufferManager.ReturnBuffer(buffer.Array, "testreturn"); } } } /// - /// Using a span to escape the string, write strings to stream writer if possible. - /// - private void EscapeStringSpan(string value) - { - ReadOnlySpan charSpan = value.AsSpan(); - int lastOffset = 0; - - for (int i = 0; i < charSpan.Length; i++) - { - bool found = false; - char ch = charSpan[i]; - - for (int ii = 0; ii < m_specialChars.Length; ii++) - { - if (m_specialChars[ii] == ch) - { - if (lastOffset < i) - { -#if NETCOREAPP2_1_OR_GREATER - m_streamWriter.Write(charSpan.Slice(lastOffset, i - lastOffset)); -#else - m_streamWriter.Write(charSpan.Slice(lastOffset, i - lastOffset).ToString()); -#endif - } - lastOffset = i + 1; - m_streamWriter.Write(m_substitutionStrings[ii]); - found = true; - break; - } - } - - if (!found && ch < 32) - { - if (lastOffset < i) - { -#if NETCOREAPP2_1_OR_GREATER - m_streamWriter.Write(charSpan.Slice(lastOffset, i - lastOffset)); -#else - m_streamWriter.Write(charSpan.Slice(lastOffset, i - lastOffset).ToString()); -#endif - } - lastOffset = i + 1; - m_streamWriter.Write("\\u"); - m_streamWriter.Write(((int)ch).ToString("X4", CultureInfo.InvariantCulture)); - } - } - if (lastOffset == 0) - { - m_streamWriter.Write(value); - } - else if (lastOffset < charSpan.Length) - { -#if NETCOREAPP2_1_OR_GREATER - m_streamWriter.Write(charSpan.Slice(lastOffset, charSpan.Length - lastOffset)); -#else - m_streamWriter.Write(charSpan.Slice(lastOffset, charSpan.Length - lastOffset).ToString()); -#endif - } - } - - /// - /// Use span to escape the string, write only chars to stream writer. + /// Benchmark encoding with recyclable memory stream kept open. /// - /// - private void EscapeStringSpanChars(string value) - { - ReadOnlySpan charSpan = value.AsSpan(); - - int lastOffset = 0; - for (int i = 0; i < charSpan.Length; i++) - { - bool found = false; - char ch = charSpan[i]; - - for (int ii = 0; ii < m_specialChars.Length; ii++) - { - if (m_specialChars[ii] == ch) - { - if (lastOffset < i) - { -#if NETCOREAPP2_1_OR_GREATER - m_streamWriter.Write(charSpan.Slice(lastOffset, i - lastOffset)); -#else - m_streamWriter.Write(charSpan.Slice(lastOffset, i - lastOffset).ToString()); -#endif - } - lastOffset = i + 1; - m_streamWriter.Write('\\'); - m_streamWriter.Write(m_substitution[ii]); - found = true; - break; - } - } - - if (!found && ch < 32) - { - if (lastOffset < i - 1) - { -#if NETCOREAPP2_1_OR_GREATER - m_streamWriter.Write(charSpan.Slice(lastOffset, i - lastOffset)); -#else - m_streamWriter.Write(charSpan.Slice(lastOffset, i - lastOffset).ToString()); -#endif - } - else - { - while (lastOffset < i) - { - m_streamWriter.Write(charSpan[lastOffset++]); - } - } - lastOffset = i + 1; - m_streamWriter.Write('\\'); - m_streamWriter.Write('u'); - m_streamWriter.Write(((int)ch).ToString("X4", CultureInfo.InvariantCulture)); - } - } - - if (lastOffset == 0) - { -#if NETCOREAPP2_1_OR_GREATER - m_streamWriter.Write(charSpan); -#else - m_streamWriter.Write(value); -#endif - } - else if (lastOffset < charSpan.Length) - { -#if NETCOREAPP2_1_OR_GREATER - m_streamWriter.Write(charSpan.Slice(lastOffset, charSpan.Length - lastOffset)); -#else - m_streamWriter.Write(charSpan.Slice(lastOffset, charSpan.Length - lastOffset).ToString()); -#endif - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void WriteSpan(ref int lastOffset, ReadOnlySpan valueSpan, int index) + [Benchmark] + [Test] + public void JsonEncoderArraySegmentStreamNoSpan() { - if (lastOffset < index - 2) - { -#if NETCOREAPP2_1_OR_GREATER - m_streamWriter.Write(valueSpan.Slice(lastOffset, index - lastOffset)); +#if NET6_0_OR_GREATER + using (var arraySegmentStream = new ArraySegmentStreamNoSpan(m_bufferManager)) #else - m_streamWriter.Write(valueSpan.Slice(lastOffset, index - lastOffset).ToString()); + using (var arraySegmentStream = new ArraySegmentStream(m_bufferManager)) #endif - } - else { - while (lastOffset < index) - { - m_streamWriter.Write(valueSpan[lastOffset++]); - } - } - lastOffset = index + 1; - } - - /// - /// Write only chars to stream writer, inline the write sequence for readability. - /// - /// - private void EscapeStringSpanCharsInline(string value) - { - ReadOnlySpan charSpan = value.AsSpan(); - int lastOffset = 0; - - for (int i = 0; i < charSpan.Length; i++) - { - bool found = false; - char ch = charSpan[i]; - - for (int ii = 0; ii < m_specialChars.Length; ii++) + JsonEncoder_StreamLeaveOpen(arraySegmentStream); + // get buffers and return them to buffer manager + var buffers = arraySegmentStream.GetBuffers("writer"); + foreach (var buffer in buffers) { - if (m_specialChars[ii] == ch) - { - WriteSpan(ref lastOffset, charSpan, i); - m_streamWriter.Write('\\'); - m_streamWriter.Write(m_substitution[ii]); - found = true; - break; - } + m_bufferManager.ReturnBuffer(buffer.Array, "testreturn"); } - - 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) + #region Private Methods + private void TestStreamEncode(MemoryStream memoryStream, bool toArray) { - ReadOnlySpan charSpan = value.AsSpan(); - int lastOffset = 0; - - for (int i = 0; i < charSpan.Length; i++) - { - bool found = false; - char ch = charSpan[i]; - - for (int ii = 0; ii < m_specialCharsConst.Length; ii++) - { - if (m_specialCharsConst[ii] == ch) - { - WriteSpan(ref lastOffset, charSpan, i); - m_streamWriter.Write('\\'); - m_streamWriter.Write(m_substitutionConst[ii]); - found = true; - break; - } - } - - if (!found && ch < 32) - { - WriteSpan(ref lastOffset, charSpan, i); - m_streamWriter.Write('\\'); - m_streamWriter.Write('u'); - m_streamWriter.Write(((int)ch).ToString("X4", CultureInfo.InvariantCulture)); - } - } - - if (lastOffset == 0) + using (var jsonEncoder = new JsonEncoder(m_context, false, false, memoryStream, true, StreamSize)) { - m_streamWriter.Write(value); + TestEncoding(jsonEncoder); + _ = jsonEncoder.Close(); } - else + using (var jsonEncoder = new JsonEncoder(m_context, false, false, memoryStream, true, StreamSize)) { - WriteSpan(ref lastOffset, charSpan, charSpan.Length); - } - } - - - private void EscapeStringSpanIndex(string value) - { - ReadOnlySpan charSpan = value.AsSpan(); - - int lastOffset = 0; - for (int i = 0; i < charSpan.Length; i++) - { - char ch = charSpan[i]; - - int index = m_specialString.IndexOf(ch); - if (index >= 0) + TestEncoding(jsonEncoder); + if (toArray) { - 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; + int length = jsonEncoder.Close(); + Assert.AreEqual(length, memoryStream.Position); + var result = memoryStream.ToArray(); + Assert.NotNull(result); + Assert.AreEqual(length, result.Length); } - - if (ch < 32) + else { - 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)); + var result = jsonEncoder.CloseAndReturnText(); + Assert.NotNull(result); } } - 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) + private void JsonEncoder_StreamLeaveOpen(MemoryStream stream, bool testResult = false) { - ReadOnlySpan charSpan = value.AsSpan(); - - int lastOffset = 0; - for (int i = 0; i < charSpan.Length; i++) - { - char ch = charSpan[i]; - - if (m_replace.TryGetValue(ch, out string escapeSequence)) - { - if (lastOffset < i) - { -#if NETCOREAPP2_1_OR_GREATER - m_streamWriter.Write(charSpan.Slice(lastOffset, i - lastOffset)); -#else - m_streamWriter.Write(charSpan.Slice(lastOffset, i - lastOffset).ToString()); -#endif - } - lastOffset = i + 1; - m_streamWriter.Write(escapeSequence); - continue; - } - - if (ch < 32) - { - if (lastOffset < i) - { -#if NETCOREAPP2_1_OR_GREATER - m_streamWriter.Write(charSpan.Slice(lastOffset, i - lastOffset)); -#else - m_streamWriter.Write(charSpan.Slice(lastOffset, i - lastOffset).ToString()); -#endif - } - lastOffset = i + 1; - m_streamWriter.Write('\\'); - m_streamWriter.Write('u'); - m_streamWriter.Write(((int)ch).ToString("X4", CultureInfo.InvariantCulture)); - } - } - if (lastOffset == 0) - { - m_streamWriter.Write(value); - } - else if (lastOffset < charSpan.Length) + int length1; + int length2; + stream.Position = 0; + using (var jsonEncoder = new JsonEncoder(m_context, false, false, stream, true, StreamSize)) { -#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 + TestEncoding(jsonEncoder); + length1 = jsonEncoder.Close(); } - } - - private void EscapeString(string value) - { - StringBuilder stringBuilder = new StringBuilder(value.Length * 2); - - foreach (char ch in value) + using (var jsonEncoder = new JsonEncoder(m_context, false, false, stream, true, StreamSize)) { - 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); - } + TestEncoding(jsonEncoder); + length2 = jsonEncoder.Close(); } - m_streamWriter.Write(stringBuilder); - } - - private void EscapeStringThreadLocal(string value) - { - StringBuilder stringBuilder = m_stringBuilderPool.Value; - stringBuilder.Clear(); - stringBuilder.EnsureCapacity(value.Length * 2); - - foreach (char ch in value) + if (testResult) { - 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); - } + var result = Encoding.UTF8.GetString(stream.ToArray()); + Assert.NotNull(result); + Assert.AreEqual(length1 * 2, length2); + Assert.AreEqual(length2, result.Length); } - m_streamWriter.Write(stringBuilder); } #endregion - #region Private Fields - private ThreadLocal m_stringBuilderPool = new ThreadLocal(() => new StringBuilder()); - private static string m_testString; - private RecyclableMemoryStreamManager m_memoryManager; - private RecyclableMemoryStream m_memoryStream; - private StreamWriter m_streamWriter; - private int m_streamSize = 1024; - private static readonly string m_specialString = "\"\\\n\r\t\b\f"; - - // Declare static readonly characters for the special characters - private static readonly char sro_quotation = '\"'; - private static readonly char sro_backslash = '\\'; - private static readonly char sro_newline = '\n'; - private static readonly char sro_return = '\r'; - private static readonly char sro_tab = '\t'; - private static readonly char sro_backspace = '\b'; - private static readonly char sro_formfeed = '\f'; - private static readonly char[] m_specialChars = new char[] { sro_quotation, sro_backslash, sro_newline, sro_return, sro_tab, sro_backspace, sro_formfeed }; - - // Declare static readonly characters for the substitution characters - private static readonly char sro_quotationSub = '\"'; - private static readonly char sro_backslashSub = '\\'; - private static readonly char sro_newlineSub = 'n'; - private static readonly char sro_returnSub = 'r'; - private static readonly char sro_tabSub = 't'; - private static readonly char sro_backspaceSub = 'b'; - private static readonly char sro_formfeedSub = 'f'; - private static readonly char[] m_substitution = new char[] { sro_quotationSub, sro_backslashSub, sro_newlineSub, sro_returnSub, sro_tabSub, sro_backspaceSub, sro_formfeedSub }; - - // Special characters as const - private const char s_quotation = '\"'; - private const char s_backslash = '\\'; - private const char s_newline = '\n'; - private const char s_return = '\r'; - private const char s_tab = '\t'; - private const char s_backspace = '\b'; - private const char s_formfeed = '\f'; - - private static readonly char[] m_specialCharsConst = new char[] { s_quotation, s_backslash, s_newline, s_return, s_tab, s_backspace, s_formfeed }; + #region Test Setup + [OneTimeSetUp] + public new void OneTimeSetUp() => base.OneTimeSetUp(); - // 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'; + [OneTimeTearDown] + public new void OneTimeTearDown() => base.OneTimeTearDown(); + #endregion - private static readonly char[] m_substitutionConst = new char[] { s_quotationSub, s_backslashSub, s_newlineSub, s_returnSub, s_tabSub, s_backspaceSub, s_formfeedSub }; + #region Benchmark Setup + /// + /// Set up some variables for benchmarks. + /// + [GlobalSetup] + public new void GlobalSetup() => base.GlobalSetup(); - private static readonly string[] m_substitutionStrings = new string[] { "\\\"", "\\\\", "\\n", "\\r", "\\t", "\\b", "\\f" }; - private static readonly Dictionary m_replace = new Dictionary - { - { '\"', "\\\"" }, - { '\\', "\\\\" }, - { '\n', "\\n" }, - { '\r', "\\r" }, - { '\t', "\\t" }, - { '\b', "\\b" }, - { '\f', "\\f" } - }; + /// + /// Tear down benchmark variables. + /// + [GlobalCleanup] + public new void GlobalCleanup() => base.GlobalCleanup(); #endregion } } diff --git a/Tests/Opc.Ua.Core.Tests/Types/Encoders/JsonEncoderDateTimeBenchmark.cs b/Tests/Opc.Ua.Core.Tests/Types/Encoders/JsonEncoderDateTimeBenchmark.cs new file mode 100644 index 000000000..08a97d438 --- /dev/null +++ b/Tests/Opc.Ua.Core.Tests/Types/Encoders/JsonEncoderDateTimeBenchmark.cs @@ -0,0 +1,110 @@ +/* ======================================================================== + * Copyright (c) 2005-2024 The OPC Foundation, Inc. All rights reserved. + * + * OPC Foundation MIT License 1.00 + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + * + * The complete license agreement can be found here: + * http://opcfoundation.org/License/MIT/1.00/ + * ======================================================================*/ + +using System; +using System.Globalization; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Diagnosers; +using NUnit.Framework; + +namespace Opc.Ua.Core.Tests.Types.Encoders +{ + [TestFixture, Category("JsonEncoder")] + [SetCulture("en-us"), SetUICulture("en-us")] + [NonParallelizable] + [MemoryDiagnoser] + [DisassemblyDiagnoser] + public class JsonEncoderDateTimeBenchmark + { + [Params(0, 4, 7)] + public int DateTimeOmittedZeros { get; set; } = 0; + + [Benchmark] + [Test] + public void DateTimeEncodeToString() + { + _ = m_dateTime.ToUniversalTime().ToString("yyyy-MM-dd'T'HH:mm:ss.FFFFFFFK", CultureInfo.InvariantCulture); + } + + [Benchmark] + [Test] + public void ConvertToUniversalTime() + { + _ = JsonEncoder.ConvertUniversalTimeToString(m_dateTime); + } + + #region Test Setup + [OneTimeSetUp] + public void OneTimeSetUp() + { + // for validating benchmark tests + m_dateTime = DateTime.UtcNow; + } + + [OneTimeTearDown] + public void OneTimeTearDown() + { + } + #endregion + + #region Benchmark Setup + /// + /// Set up some variables for benchmarks. + /// + [GlobalSetup] + public void GlobalSetup() + { + // for validating benchmark tests + switch (DateTimeOmittedZeros) + { + case 4: m_dateTime = new DateTime(2011, 11, 11, 11, 11, 11, 999, DateTimeKind.Utc); break; + case 7: m_dateTime = new DateTime(2011, 11, 11, 11, 11, 11, DateTimeKind.Utc); break; + default: + do + { + m_dateTime = DateTime.UtcNow; + } while (m_dateTime.Ticks % 10 == 0); + break; + } + } + + /// + /// Tear down benchmark variables. + /// + [GlobalCleanup] + public void GlobalCleanup() + { + } + #endregion + + #region Private Fields + private DateTime m_dateTime; + #endregion + } +} diff --git a/Tests/Opc.Ua.Core.Tests/Types/Encoders/JsonEncoderEscapeStringBenchmarks.cs b/Tests/Opc.Ua.Core.Tests/Types/Encoders/JsonEncoderEscapeStringBenchmarks.cs new file mode 100644 index 000000000..c1746fd4d --- /dev/null +++ b/Tests/Opc.Ua.Core.Tests/Types/Encoders/JsonEncoderEscapeStringBenchmarks.cs @@ -0,0 +1,860 @@ +/* ======================================================================== + * Copyright (c) 2005-2024 The OPC Foundation, Inc. All rights reserved. + * + * OPC Foundation MIT License 1.00 + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + * + * The complete license agreement can be found here: + * http://opcfoundation.org/License/MIT/1.00/ + * ======================================================================*/ + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Runtime.CompilerServices; +using System.Text; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Diagnosers; +using Microsoft.IO; +using NUnit.Framework; + +namespace Opc.Ua.Core.Tests.Types.Encoders +{ + [TestFixture, Category("JsonEncoder")] + [SetCulture("en-us"), SetUICulture("en-us")] + [NonParallelizable] + [MemoryDiagnoser] + [DisassemblyDiagnoser(printSource: true)] + public class JsonEncoderEscapeStringBenchmarks + { + public const int InnerLoops = 100; + [DatapointSource] + [Params(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)] + public int StringVariantIndex { get; set; } = 1; + + [DatapointSource] + // for benchmarking with different escaped strings + public static readonly string[] EscapeTestStrings = + { + // The use case without escape characters, plain text + "The quick brown fox jumps over the lazy dog.", + // The use case with many control characters escaped, 1 char spaces + "\" \n \r \t \b \f \\ ", + // The use case with many control characters escaped, 2 char spaces + " \" \n \r \t \b \f \\ ", + // The use case with many control characters escaped, 3 char spaces + " \" \n \r \t \b \f \\ ", + // The use case with many control characters escaped, 5 char spaces + " \" \n \r \t \b \f \\ ", + // The use case with many binary characters escaped, 1 char spaces + "\0 \x01 \x02 \x03 \x04 ", + // The use case with many binary characters escaped, 2 char spaces + " \0 \x01 \x02 \x03 \x04 ", + // The use case with many binary characters escaped, 3 char spaces + " \0 \x01 \x02 \x03 \x04 ", + // The use case with many binary characters escaped, 5 char spaces + " \0 \x01 \x02 \x03 \x04 ", + // The use case with all escape characters and a long string + "Ascii characters, special characters \n \b & control characters \0 \x04 ␀ ␁ ␂ ␃ ␄. This is a test.", + }; + + /// + /// Benchmark encoding of the previous implementation. + /// + [Benchmark(Baseline = true)] + public void EscapeStringLegacy() + { + m_memoryStream.Position = 0; + int repeats = InnerLoops; + while (repeats-- > 0) + { + EscapedStringLegacy(m_testString); + } + m_streamWriter.Flush(); + } + + /// + /// Benchmark encoding of the previous implementation with snall improvement for binary encoding. + /// + [Benchmark] + public void EscapeStringLegacyPlus() + { + m_memoryStream.Position = 0; + int repeats = InnerLoops; + while (repeats-- > 0) + { + EscapedStringLegacyPlus(m_testString); + } + m_streamWriter.Flush(); + } + + /// + /// A new implementation using StringBuilder. + /// + [Benchmark] + public void EscapeStringStringBuilder() + { + m_memoryStream.Position = 0; + int repeats = InnerLoops; + while (repeats-- > 0) + { + EscapeString(m_testString); + } + m_streamWriter.Flush(); + } + + /// + /// A new implementation using ReadOnlySpan. + /// + [Benchmark] + public void EscapeStringSpan() + { + m_memoryStream.Position = 0; + int repeats = InnerLoops; + while (repeats-- > 0) + { + EscapeStringSpan(m_testString); + } + m_streamWriter.Flush(); + } + + /// + /// A new implementation using ReadOnlySpan and char write. + /// + [Benchmark] + public void EscapeStringSpanChars() + { + m_memoryStream.Position = 0; + int repeats = InnerLoops; + while (repeats-- > 0) + { + EscapeStringSpanChars(m_testString); + } + m_streamWriter.Flush(); + } + + /// + /// A new implementation using ReadOnlySpan and char write. + /// + [Benchmark] + public void EscapeStringSpanCharsInline() + { + m_memoryStream.Position = 0; + int repeats = InnerLoops; + while (repeats-- > 0) + { + EscapeStringSpanCharsInline(m_testString); + } + m_streamWriter.Flush(); + } + + /// + /// A new implementation using ReadOnlySpan and char write with const arrays. + /// + [Benchmark] + public void EscapeStringSpanCharsInlineConst() + { + m_memoryStream.Position = 0; + int repeats = InnerLoops; + while (repeats-- > 0) + { + EscapeStringSpanCharsInlineConst(m_testString); + } + m_streamWriter.Flush(); + } + + /// + /// A new implementation using ReadOnlySpan and IndexOf. + /// + [Benchmark] + public void EscapeStringSpanIndex() + { + m_memoryStream.Position = 0; + int repeats = InnerLoops; + while (repeats-- > 0) + { + EscapeStringSpanIndex(m_testString); + } + m_streamWriter.Flush(); + } + + /// + /// A new implementation using ReadOnlySpan and Dictionary. + /// + [Benchmark] + public void EscapeStringSpanDict() + { + m_memoryStream.Position = 0; + int repeats = InnerLoops; + while (repeats-- > 0) + { + EscapeStringSpanDict(m_testString); + } + m_streamWriter.Flush(); + } + + [Theory] + [TestCase("No Escape chars", 0)] + [TestCase("control chars escaped, 1 char space", 1)] + [TestCase("control chars escaped, 2 char spaces", 2)] + [TestCase("control chars escaped, 3 char spaces", 3)] + [TestCase("control chars escaped, 5 char spaces", 4)] + [TestCase("binary chars escaped, 1 char space", 5)] + [TestCase("binary chars escaped, 2 char spaces", 6)] + [TestCase("binary chars escaped, 3 char spaces", 7)] + [TestCase("binary chars escaped, 5 char spaces", 8)] + [TestCase("all escape chars and long string", 9)] + public void EscapeStringValidation(string name, int index) + { + m_testString = EscapeTestStrings[index]; + TestContext.Out.WriteLine(m_testString); + var testArray = m_testString.ToCharArray(); + + m_memoryStream.Position = 0; + EscapeStringLegacy(); + m_streamWriter.Flush(); + byte[] resultLegacy = m_memoryStream.ToArray(); + TestContext.Out.WriteLine(Encoding.UTF8.GetString(resultLegacy)); + + m_memoryStream.Position = 0; + EscapeStringLegacyPlus(); + m_streamWriter.Flush(); + byte[] resultLegacyPlus = m_memoryStream.ToArray(); + TestContext.Out.WriteLine(Encoding.UTF8.GetString(resultLegacyPlus)); + + m_memoryStream.Position = 0; + EscapeStringStringBuilder(); + m_streamWriter.Flush(); + byte[] result = m_memoryStream.ToArray(); + TestContext.Out.WriteLine(Encoding.UTF8.GetString(result)); + + m_memoryStream.Position = 0; + EscapeStringSpan(m_testString); + m_streamWriter.Flush(); + byte[] resultSpan = m_memoryStream.ToArray(); + TestContext.Out.WriteLine(Encoding.UTF8.GetString(resultSpan)); + + m_memoryStream.Position = 0; + EscapeStringSpanChars(m_testString); + m_streamWriter.Flush(); + byte[] resultSpanChars = m_memoryStream.ToArray(); + TestContext.Out.WriteLine(Encoding.UTF8.GetString(resultSpanChars)); + + m_memoryStream.Position = 0; + EscapeStringSpanCharsInline(m_testString); + m_streamWriter.Flush(); + byte[] resultSpanCharsInline = m_memoryStream.ToArray(); + TestContext.Out.WriteLine(Encoding.UTF8.GetString(resultSpanCharsInline)); + + m_memoryStream.Position = 0; + EscapeStringSpanCharsInlineConst(m_testString); + m_streamWriter.Flush(); + byte[] resultSpanCharsInlineConst = m_memoryStream.ToArray(); + TestContext.Out.WriteLine(Encoding.UTF8.GetString(resultSpanCharsInlineConst)); + + m_memoryStream.Position = 0; + EscapeStringSpanIndex(m_testString); + m_streamWriter.Flush(); + byte[] resultSpanIndex = m_memoryStream.ToArray(); + TestContext.Out.WriteLine(Encoding.UTF8.GetString(resultSpanIndex)); + + m_memoryStream.Position = 0; + EscapeStringSpanDict(m_testString); + m_streamWriter.Flush(); + byte[] resultSpanDict = m_memoryStream.ToArray(); + TestContext.Out.WriteLine(Encoding.UTF8.GetString(resultSpanDict)); + + Assert.IsTrue(Utils.IsEqual(resultLegacy, result)); + Assert.IsTrue(Utils.IsEqual(resultLegacy, resultLegacyPlus)); + Assert.IsTrue(Utils.IsEqual(resultLegacy, resultSpan)); + Assert.IsTrue(Utils.IsEqual(resultLegacy, resultSpanChars)); + Assert.IsTrue(Utils.IsEqual(resultLegacy, resultSpanCharsInline)); + Assert.IsTrue(Utils.IsEqual(resultLegacy, resultSpanCharsInlineConst)); + Assert.IsTrue(Utils.IsEqual(resultLegacy, resultSpanIndex)); + Assert.IsTrue(Utils.IsEqual(resultLegacy, resultSpanDict)); + } + + #region Test Setup + [OneTimeSetUp] + public void OneTimeSetUp() + { + m_memoryManager = new RecyclableMemoryStreamManager(); + m_memoryStream = new RecyclableMemoryStream(m_memoryManager); + m_streamWriter = new StreamWriter(m_memoryStream, new UTF8Encoding(false), m_streamSize, false); + } + + [OneTimeTearDown] + public void OneTimeTearDown() + { + m_streamWriter?.Dispose(); + m_streamWriter = null; + m_memoryStream?.Dispose(); + m_memoryStream = null; + m_memoryManager = null; + } + #endregion + + #region Benchmark Setup + /// 4 + /// Set up some variables for benchmarks. + /// + [GlobalSetup] + public void GlobalSetup() + { + m_memoryManager = new RecyclableMemoryStreamManager(); + m_memoryStream = new RecyclableMemoryStream(m_memoryManager); + m_streamWriter = new StreamWriter(m_memoryStream, Encoding.UTF8, m_streamSize, false); + m_testString = EscapeTestStrings[StringVariantIndex - 1]; + } + + [GlobalCleanup] + public void GlobalCleanup() + { + m_streamWriter?.Dispose(); + m_streamWriter = null; + m_memoryStream?.Dispose(); + m_memoryStream = null; + m_memoryManager = null; + } + #endregion + + #region Private Methods + /// + /// Version used previously in JsonEncoder. + /// + private void EscapedStringLegacy(string value) + { + foreach (char ch in value) + { + bool found = false; + + for (int ii = 0; ii < m_specialChars.Length; ii++) + { + if (m_specialChars[ii] == ch) + { + m_streamWriter.Write('\\'); + m_streamWriter.Write(m_substitution[ii]); + found = true; + break; + } + } + + if (!found) + { + if (ch < 32) + { + m_streamWriter.Write("\\u"); + m_streamWriter.Write("{0:X4}", (int)ch); + continue; + } + + m_streamWriter.Write(ch); + } + } + } + + /// + /// Version used previously in JsonEncoder plus improvement of binary encoding. + /// + /// + /// For the underlying stream writer it is faster to write two chars than a 2 char string. + /// + private void EscapedStringLegacyPlus(string value) + { + foreach (char ch in value) + { + bool found = false; + + for (int ii = 0; ii < m_specialChars.Length; ii++) + { + if (m_specialChars[ii] == ch) + { + m_streamWriter.Write('\\'); + m_streamWriter.Write(m_substitution[ii]); + found = true; + break; + } + } + + if (!found) + { + if (ch < 32) + { + m_streamWriter.Write('\\'); + m_streamWriter.Write('u'); + m_streamWriter.Write(((int)ch).ToString("X4", CultureInfo.InvariantCulture)); + continue; + } + + m_streamWriter.Write(ch); + } + } + } + + /// + /// Using a span to escape the string, write strings to stream writer if possible. + /// + private void EscapeStringSpan(string value) + { + ReadOnlySpan charSpan = value.AsSpan(); + int lastOffset = 0; + + for (int i = 0; i < charSpan.Length; i++) + { + bool found = false; + char ch = charSpan[i]; + + for (int ii = 0; ii < m_specialChars.Length; ii++) + { + if (m_specialChars[ii] == ch) + { + if (lastOffset < i) + { +#if NETCOREAPP2_1_OR_GREATER + m_streamWriter.Write(charSpan.Slice(lastOffset, i - lastOffset)); +#else + m_streamWriter.Write(charSpan.Slice(lastOffset, i - lastOffset).ToString()); +#endif + } + lastOffset = i + 1; + m_streamWriter.Write(m_substitutionStrings[ii]); + found = true; + break; + } + } + + if (!found && ch < 32) + { + if (lastOffset < i) + { +#if NETCOREAPP2_1_OR_GREATER + m_streamWriter.Write(charSpan.Slice(lastOffset, i - lastOffset)); +#else + m_streamWriter.Write(charSpan.Slice(lastOffset, i - lastOffset).ToString()); +#endif + } + lastOffset = i + 1; + m_streamWriter.Write("\\u"); + m_streamWriter.Write(((int)ch).ToString("X4", CultureInfo.InvariantCulture)); + } + } + if (lastOffset == 0) + { + m_streamWriter.Write(value); + } + else if (lastOffset < charSpan.Length) + { +#if NETCOREAPP2_1_OR_GREATER + m_streamWriter.Write(charSpan.Slice(lastOffset, charSpan.Length - lastOffset)); +#else + m_streamWriter.Write(charSpan.Slice(lastOffset, charSpan.Length - lastOffset).ToString()); +#endif + } + } + + /// + /// Use span to escape the string, write only chars to stream writer. + /// + /// + private void EscapeStringSpanChars(string value) + { + ReadOnlySpan charSpan = value.AsSpan(); + + int lastOffset = 0; + for (int i = 0; i < charSpan.Length; i++) + { + bool found = false; + char ch = charSpan[i]; + + for (int ii = 0; ii < m_specialChars.Length; ii++) + { + if (m_specialChars[ii] == ch) + { + if (lastOffset < i) + { +#if NETCOREAPP2_1_OR_GREATER + m_streamWriter.Write(charSpan.Slice(lastOffset, i - lastOffset)); +#else + m_streamWriter.Write(charSpan.Slice(lastOffset, i - lastOffset).ToString()); +#endif + } + lastOffset = i + 1; + m_streamWriter.Write('\\'); + m_streamWriter.Write(m_substitution[ii]); + found = true; + break; + } + } + + if (!found && ch < 32) + { + if (lastOffset < i - 1) + { +#if NETCOREAPP2_1_OR_GREATER + m_streamWriter.Write(charSpan.Slice(lastOffset, i - lastOffset)); +#else + m_streamWriter.Write(charSpan.Slice(lastOffset, i - lastOffset).ToString()); +#endif + } + else + { + while (lastOffset < i) + { + m_streamWriter.Write(charSpan[lastOffset++]); + } + } + lastOffset = i + 1; + m_streamWriter.Write('\\'); + m_streamWriter.Write('u'); + m_streamWriter.Write(((int)ch).ToString("X4", CultureInfo.InvariantCulture)); + } + } + + if (lastOffset == 0) + { +#if NETCOREAPP2_1_OR_GREATER + m_streamWriter.Write(charSpan); +#else + m_streamWriter.Write(value); +#endif + } + else if (lastOffset < charSpan.Length) + { +#if NETCOREAPP2_1_OR_GREATER + m_streamWriter.Write(charSpan.Slice(lastOffset, charSpan.Length - lastOffset)); +#else + m_streamWriter.Write(charSpan.Slice(lastOffset, charSpan.Length - lastOffset).ToString()); +#endif + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void WriteSpan(ref int lastOffset, ReadOnlySpan valueSpan, int index) + { + if (lastOffset < index - 2) + { +#if NETCOREAPP2_1_OR_GREATER + m_streamWriter.Write(valueSpan.Slice(lastOffset, index - lastOffset)); +#else + m_streamWriter.Write(valueSpan.Slice(lastOffset, index - lastOffset).ToString()); +#endif + } + else + { + while (lastOffset < index) + { + m_streamWriter.Write(valueSpan[lastOffset++]); + } + } + lastOffset = index + 1; + } + + /// + /// Write only chars to stream writer, inline the write sequence for readability. + /// + /// + private void EscapeStringSpanCharsInline(string value) + { + ReadOnlySpan charSpan = value.AsSpan(); + int lastOffset = 0; + + for (int i = 0; i < charSpan.Length; i++) + { + bool found = false; + char ch = charSpan[i]; + + for (int ii = 0; ii < m_specialChars.Length; ii++) + { + if (m_specialChars[ii] == ch) + { + WriteSpan(ref lastOffset, charSpan, i); + m_streamWriter.Write('\\'); + m_streamWriter.Write(m_substitution[ii]); + found = true; + break; + } + } + + if (!found && ch < 32) + { + WriteSpan(ref lastOffset, charSpan, i); + m_streamWriter.Write('\\'); + m_streamWriter.Write('u'); + m_streamWriter.Write(((int)ch).ToString("X4", CultureInfo.InvariantCulture)); + } + } + + if (lastOffset == 0) + { +#if NETCOREAPP2_1_OR_GREATER + m_streamWriter.Write(charSpan); +#else + m_streamWriter.Write(value); +#endif + } + else + { + WriteSpan(ref lastOffset, charSpan, charSpan.Length); + } + } + + // create version of EscapeStringSpanCharsInline that references cosnt arrays + private void EscapeStringSpanCharsInlineConst(string value) + { + ReadOnlySpan charSpan = value.AsSpan(); + int lastOffset = 0; + + for (int i = 0; i < charSpan.Length; i++) + { + bool found = false; + char ch = charSpan[i]; + + for (int ii = 0; ii < m_specialCharsConst.Length; ii++) + { + if (m_specialCharsConst[ii] == ch) + { + WriteSpan(ref lastOffset, charSpan, i); + m_streamWriter.Write('\\'); + m_streamWriter.Write(m_substitutionConst[ii]); + found = true; + break; + } + } + + if (!found && ch < 32) + { + WriteSpan(ref lastOffset, charSpan, i); + m_streamWriter.Write('\\'); + m_streamWriter.Write('u'); + m_streamWriter.Write(((int)ch).ToString("X4", CultureInfo.InvariantCulture)); + } + } + + if (lastOffset == 0) + { + m_streamWriter.Write(value); + } + else + { + WriteSpan(ref lastOffset, charSpan, charSpan.Length); + } + } + + + private void EscapeStringSpanIndex(string value) + { + ReadOnlySpan charSpan = value.AsSpan(); + + int lastOffset = 0; + for (int i = 0; i < charSpan.Length; i++) + { + char ch = charSpan[i]; + + int index = m_specialString.IndexOf(ch); + if (index >= 0) + { + if (lastOffset < i) + { +#if NETCOREAPP2_1_OR_GREATER + m_streamWriter.Write(charSpan.Slice(lastOffset, i - lastOffset)); +#else + m_streamWriter.Write(charSpan.Slice(lastOffset, i - lastOffset).ToString()); +#endif + } + lastOffset = i + 1; + m_streamWriter.Write(m_substitutionStrings[index]); + continue; + } + + if (ch < 32) + { + if (lastOffset < i) + { +#if NETCOREAPP2_1_OR_GREATER + m_streamWriter.Write(charSpan.Slice(lastOffset, i - lastOffset)); +#else + m_streamWriter.Write(charSpan.Slice(lastOffset, i - lastOffset).ToString()); +#endif + } + lastOffset = i + 1; + m_streamWriter.Write('\\'); + m_streamWriter.Write('u'); + m_streamWriter.Write(((int)ch).ToString("X4", CultureInfo.InvariantCulture)); + } + } + if (lastOffset == 0) + { + m_streamWriter.Write(value); + } + else if (lastOffset < charSpan.Length) + { +#if NETCOREAPP2_1_OR_GREATER + m_streamWriter.Write(charSpan.Slice(lastOffset, charSpan.Length - lastOffset)); +#else + m_streamWriter.Write(charSpan.Slice(lastOffset, charSpan.Length - lastOffset).ToString()); +#endif + } + } + + private void EscapeStringSpanDict(string value) + { + ReadOnlySpan charSpan = value.AsSpan(); + + int lastOffset = 0; + for (int i = 0; i < charSpan.Length; i++) + { + char ch = charSpan[i]; + + if (m_replace.TryGetValue(ch, out string escapeSequence)) + { + if (lastOffset < i) + { +#if NETCOREAPP2_1_OR_GREATER + m_streamWriter.Write(charSpan.Slice(lastOffset, i - lastOffset)); +#else + m_streamWriter.Write(charSpan.Slice(lastOffset, i - lastOffset).ToString()); +#endif + } + lastOffset = i + 1; + m_streamWriter.Write(escapeSequence); + continue; + } + + if (ch < 32) + { + if (lastOffset < i) + { +#if NETCOREAPP2_1_OR_GREATER + m_streamWriter.Write(charSpan.Slice(lastOffset, i - lastOffset)); +#else + m_streamWriter.Write(charSpan.Slice(lastOffset, i - lastOffset).ToString()); +#endif + } + lastOffset = i + 1; + m_streamWriter.Write('\\'); + m_streamWriter.Write('u'); + m_streamWriter.Write(((int)ch).ToString("X4", CultureInfo.InvariantCulture)); + } + } + if (lastOffset == 0) + { + m_streamWriter.Write(value); + } + else if (lastOffset < charSpan.Length) + { +#if NETCOREAPP2_1_OR_GREATER + m_streamWriter.Write(charSpan.Slice(lastOffset, charSpan.Length - lastOffset)); +#else + m_streamWriter.Write(charSpan.Slice(lastOffset, charSpan.Length - lastOffset).ToString()); +#endif + } + } + + private void EscapeString(string value) + { + StringBuilder stringBuilder = new StringBuilder(value.Length * 2); + + foreach (char ch in value) + { + if (m_replace.TryGetValue(ch, out string escapeSequence)) + { + stringBuilder.Append(escapeSequence); + } + else if (ch < 32) + { + stringBuilder.Append("\\u"); + stringBuilder.Append(((int)ch).ToString("X4", CultureInfo.InvariantCulture)); + } + else + { + stringBuilder.Append(ch); + } + } + m_streamWriter.Write(stringBuilder); + } + #endregion + + #region Private Fields + private static string m_testString; + private RecyclableMemoryStreamManager m_memoryManager; + private RecyclableMemoryStream m_memoryStream; + private StreamWriter m_streamWriter; + private int m_streamSize = 1024; + private static readonly string m_specialString = "\"\\\n\r\t\b\f"; + + // Declare static readonly characters for the special characters + private static readonly char sro_quotation = '\"'; + private static readonly char sro_backslash = '\\'; + private static readonly char sro_newline = '\n'; + private static readonly char sro_return = '\r'; + private static readonly char sro_tab = '\t'; + private static readonly char sro_backspace = '\b'; + private static readonly char sro_formfeed = '\f'; + private static readonly char[] m_specialChars = new char[] { sro_quotation, sro_backslash, sro_newline, sro_return, sro_tab, sro_backspace, sro_formfeed }; + + // Declare static readonly characters for the substitution characters + private static readonly char sro_quotationSub = '\"'; + private static readonly char sro_backslashSub = '\\'; + private static readonly char sro_newlineSub = 'n'; + private static readonly char sro_returnSub = 'r'; + private static readonly char sro_tabSub = 't'; + private static readonly char sro_backspaceSub = 'b'; + private static readonly char sro_formfeedSub = 'f'; + private static readonly char[] m_substitution = new char[] { sro_quotationSub, sro_backslashSub, sro_newlineSub, sro_returnSub, sro_tabSub, sro_backspaceSub, sro_formfeedSub }; + + // Special characters as const + private const char s_quotation = '\"'; + private const char s_backslash = '\\'; + private const char s_newline = '\n'; + private const char s_return = '\r'; + private const char s_tab = '\t'; + private const char s_backspace = '\b'; + private const char s_formfeed = '\f'; + + private static readonly char[] m_specialCharsConst = new char[] { s_quotation, s_backslash, s_newline, s_return, s_tab, s_backspace, s_formfeed }; + + // Substitution as const + private const char s_quotationSub = '\"'; + private const char s_backslashSub = '\\'; + private const char s_newlineSub = 'n'; + private const char s_returnSub = 'r'; + private const char s_tabSub = 't'; + private const char s_backspaceSub = 'b'; + private const char s_formfeedSub = 'f'; + + private static readonly char[] m_substitutionConst = new char[] { s_quotationSub, s_backslashSub, s_newlineSub, s_returnSub, s_tabSub, s_backspaceSub, s_formfeedSub }; + + private static readonly string[] m_substitutionStrings = new string[] { "\\\"", "\\\\", "\\n", "\\r", "\\t", "\\b", "\\f" }; + private static readonly Dictionary m_replace = new Dictionary + { + { '\"', "\\\"" }, + { '\\', "\\\\" }, + { '\n', "\\n" }, + { '\r', "\\r" }, + { '\t', "\\t" }, + { '\b', "\\b" }, + { '\f', "\\f" } + }; + #endregion + } +} diff --git a/Tests/Opc.Ua.Core.Tests/Types/Encoders/JsonEncoderTests.cs b/Tests/Opc.Ua.Core.Tests/Types/Encoders/JsonEncoderTests.cs index 4cb3ae150..e35b1acfe 100644 --- a/Tests/Opc.Ua.Core.Tests/Types/Encoders/JsonEncoderTests.cs +++ b/Tests/Opc.Ua.Core.Tests/Types/Encoders/JsonEncoderTests.cs @@ -35,7 +35,9 @@ using System.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 @@ public void ConstructorDefault(bool useReversible, bool topLevelIsArray) } /// - /// Use a constructor with external Stream, + /// Use a MemoryStream constructor with external Stream, + /// keep the stream open for more encodings. + /// + [Test] + public void ConstructorMemoryStream() + { + using (var memoryStream = new MemoryStream()) + { + ConstructorStream(memoryStream); + } + } + + /// + /// Use a ArraySegmentStream constructor with external Stream, /// keep the stream open for more encodings. /// [Test] - public void ConstructorStream() + public void ConstructorArraySegmentStream() + { + using (var memoryStream = new ArraySegmentStream(BufferManager)) + { + ConstructorStream(memoryStream); + } + } + + /// + /// Use a RecylableMemoryStream constructor with external Stream, + /// keep the stream open for more encodings. + /// + [Test] + public void ConstructorRecyclableMemoryStream() + { + var recyclableMemoryStreamManager = new RecyclableMemoryStreamManager(new RecyclableMemoryStreamManager.Options { + BlockSize = BufferManager.MaxSuggestedBufferSize, + }); + using (var memoryStream = new RecyclableMemoryStream(recyclableMemoryStreamManager)) + { + ConstructorStream(memoryStream); + } + } + + /// + /// Use a constructor with external Stream, + /// keep the stream open for more encodings. + /// + public void ConstructorStream(MemoryStream memoryStream) { var context = new ServiceMessageContext(); - var memoryStream = new MemoryStream(); using (var jsonEncoder = new JsonEncoder(context, true, false, memoryStream, true)) { TestEncoding(jsonEncoder); @@ -384,7 +426,7 @@ public void ConstructorStream() // 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 @@ public void ConstructorStream() Assert.Throws(() => _ = new StreamWriter(memoryStream)); } + /// + /// Use a constructor with external ArraySegmentStream, + /// keep the stream open for more encodings. + /// Alternate use of sequence. + /// + [Test] + public void ConstructorArraySegmentStreamSequence() + { + var context = new ServiceMessageContext(); + using (var memoryStream = new ArraySegmentStream(BufferManager)) + { + using (var jsonEncoder = new JsonEncoder(context, true, false, memoryStream, true)) + { + TestEncoding(jsonEncoder); + } + + // get the buffers and save the result +#if NET5_0_OR_GREATER + string result1; + using (var sequence = memoryStream.GetSequence(nameof(ConstructorStream))) + { + result1 = Encoding.UTF8.GetString(sequence.Sequence); + Assert.IsNotEmpty(result1); + TestContext.Out.WriteLine("Result1:"); + _ = PrettifyAndValidateJson(result1); + } +#else + var result1 = Encoding.UTF8.GetString(memoryStream.ToArray()); + Assert.IsNotEmpty(result1); + TestContext.Out.WriteLine("Result1:"); + _ = PrettifyAndValidateJson(result1); +#endif + + // recycle the memory stream, ensure the result is equal + memoryStream.Position = 0; + using (var jsonEncoder = new JsonEncoder(context, true, false, memoryStream, true)) + { + TestEncoding(jsonEncoder); + } + var result2 = Encoding.UTF8.GetString(memoryStream.ToArray()); + Assert.IsNotEmpty(result2); + TestContext.Out.WriteLine("Result2:"); + _ = PrettifyAndValidateJson(result2); + Assert.AreEqual(result1, result2); + + // recycle the StreamWriter, ensure the result is equal, + // use reflection to return result in external stream + memoryStream.Position = 0; + using (IJsonEncoder jsonEncoder = new JsonEncoder(context, true, false, memoryStream, false)) + { + TestEncoding(jsonEncoder); + var result3 = jsonEncoder.CloseAndReturnText(); + Assert.IsNotEmpty(result3); + TestContext.Out.WriteLine("Result3:"); + _ = PrettifyAndValidateJson(result3); + Assert.AreEqual(result1, result3); + } + + // ensure the memory stream was closed + Assert.Throws(() => _ = new StreamWriter(memoryStream)); + } + } + + /// + /// Use a constructor with external RecyclableMemoryStream, + /// keep the stream open for more encodings. + /// Alternate use of sequence. + /// + [Test] + public void ConstructorRecyclableMemoryStreamSequence() + { + var context = new ServiceMessageContext(); + using (var memoryStream = new RecyclableMemoryStream(RecyclableMemoryManager)) + { + using (var jsonEncoder = new JsonEncoder(context, true, false, memoryStream, true)) + { + TestEncoding(jsonEncoder); + } + + // get the buffers and save the result +#if NET5_0_OR_GREATER + string result1; + { + var sequence = memoryStream.GetReadOnlySequence(); + result1 = Encoding.UTF8.GetString(sequence); + Assert.IsNotEmpty(result1); + TestContext.Out.WriteLine("Result1:"); + _ = PrettifyAndValidateJson(result1); + } +#else + var result1 = Encoding.UTF8.GetString(memoryStream.ToArray()); + Assert.IsNotEmpty(result1); + TestContext.Out.WriteLine("Result1:"); + _ = PrettifyAndValidateJson(result1); +#endif + + // recycle the memory stream, ensure the result is equal + memoryStream.Position = 0; + using (var jsonEncoder = new JsonEncoder(context, true, false, memoryStream, true)) + { + TestEncoding(jsonEncoder); + } + var result2 = Encoding.UTF8.GetString(memoryStream.ToArray()); + Assert.IsNotEmpty(result2); + TestContext.Out.WriteLine("Result2:"); + _ = PrettifyAndValidateJson(result2); + Assert.AreEqual(result1, result2); + + // recycle the StreamWriter, ensure the result is equal, + // use reflection to return result in external stream + memoryStream.Position = 0; + using (IJsonEncoder jsonEncoder = new JsonEncoder(context, true, false, memoryStream, false)) + { + TestEncoding(jsonEncoder); + var result3 = jsonEncoder.CloseAndReturnText(); + Assert.IsNotEmpty(result3); + TestContext.Out.WriteLine("Result3:"); + _ = PrettifyAndValidateJson(result3); + Assert.AreEqual(result1, result3); + } + + // ensure the memory stream was closed + Assert.Throws(() => _ = new StreamWriter(memoryStream)); + } + } + /// /// Verify reversible Json encoding. /// [Theory] - public void JsonEncodeRev(JsonValidationData jsonValidationData) + public void JsonEncodeRev(JsonValidationData jsonValidationData, MemoryStreamType memoryStreamType) { EncodeJsonVerifyResult( jsonValidationData.BuiltInType, + memoryStreamType, jsonValidationData.Instance, true, jsonValidationData.ExpectedReversible, @@ -430,10 +599,11 @@ public void JsonEncodeRev(JsonValidationData jsonValidationData) /// Verify non reversible Json encoding. /// [Theory] - public void JsonEncodeNonRev(JsonValidationData jsonValidationData) + public void JsonEncodeNonRev(JsonValidationData jsonValidationData, MemoryStreamType memoryStreamType) { EncodeJsonVerifyResult( jsonValidationData.BuiltInType, + memoryStreamType, jsonValidationData.Instance, false, jsonValidationData.ExpectedNonReversible ?? jsonValidationData.ExpectedReversible, diff --git a/common.props b/common.props index bbfba2af1..84f110532 100644 --- a/common.props +++ b/common.props @@ -21,9 +21,14 @@ - true + true + false false + + + true + images/logo.jpg From ca848af3668c96e4f8e23e1212b2375a30937dbe Mon Sep 17 00:00:00 2001 From: Martin Regen Date: Wed, 3 Apr 2024 06:43:44 +0200 Subject: [PATCH 8/9] Reenable .NET Standard 2.0 builds for core library (#2570) - add netstandard2.0 support back to the core and cert library, to allow to build .NET analyzers - add custom build steps for .NET 4.7.2 --- .azurepipelines/signlistDebug.txt | 2 ++ .azurepipelines/signlistRelease.txt | 2 ++ Docs/PlatformBuild.md | 6 ++--- .../Opc.Ua.Security.Certificates.csproj | 2 +- .../Opc.Ua.Bindings.Https.csproj | 2 +- Stack/Opc.Ua.Core/Opc.Ua.Core.csproj | 2 +- .../Types/Encoders/BinaryDecoderBenchmarks.cs | 4 ++-- .../Types/Encoders/BinaryEncoderBenchmarks.cs | 2 +- .../Types/Encoders/EncoderBenchmarks.cs | 2 +- .../Types/Encoders/JsonEncoderBenchmarks.cs | 3 ++- Tests/customtest.bat | 8 +++---- UA Core Library.sln | 1 - azure-pipelines.yml | 24 +++++++++++++++++++ targets.props | 12 +++++++++- 14 files changed, 55 insertions(+), 17 deletions(-) diff --git a/.azurepipelines/signlistDebug.txt b/.azurepipelines/signlistDebug.txt index e1871d3bf..5c9998add 100644 --- a/.azurepipelines/signlistDebug.txt +++ b/.azurepipelines/signlistDebug.txt @@ -1,3 +1,4 @@ +Stack\Opc.Ua.Core\bin\Debug\netstandard2.0\Opc.Ua.Core.dll Stack\Opc.Ua.Core\bin\Debug\netstandard2.1\Opc.Ua.Core.dll Stack\Opc.Ua.Core\bin\Debug\net48\Opc.Ua.Core.dll Stack\Opc.Ua.Core\bin\Debug\net6.0\Opc.Ua.Core.dll @@ -30,6 +31,7 @@ Libraries\Opc.Ua.Gds.Server.Common\bin\Debug\netstandard2.1\Opc.Ua.Gds.Server.Co Libraries\Opc.Ua.Gds.Server.Common\bin\Debug\net48\Opc.Ua.Gds.Server.Common.dll Libraries\Opc.Ua.Gds.Server.Common\bin\Debug\net6.0\Opc.Ua.Gds.Server.Common.dll Libraries\Opc.Ua.Gds.Server.Common\bin\Debug\net8.0\Opc.Ua.Gds.Server.Common.dll +Libraries\Opc.Ua.Security.Certificates\bin\Debug\netstandard2.0\Opc.Ua.Security.Certificates.dll Libraries\Opc.Ua.Security.Certificates\bin\Debug\netstandard2.1\Opc.Ua.Security.Certificates.dll Libraries\Opc.Ua.Security.Certificates\bin\Debug\net48\Opc.Ua.Security.Certificates.dll Libraries\Opc.Ua.Security.Certificates\bin\Debug\net6.0\Opc.Ua.Security.Certificates.dll diff --git a/.azurepipelines/signlistRelease.txt b/.azurepipelines/signlistRelease.txt index 0251c5798..8a844e347 100644 --- a/.azurepipelines/signlistRelease.txt +++ b/.azurepipelines/signlistRelease.txt @@ -1,3 +1,4 @@ +Stack\Opc.Ua.Core\bin\Release\netstandard2.0\Opc.Ua.Core.dll Stack\Opc.Ua.Core\bin\Release\netstandard2.1\Opc.Ua.Core.dll Stack\Opc.Ua.Core\bin\Release\net48\Opc.Ua.Core.dll Stack\Opc.Ua.Core\bin\Release\net6.0\Opc.Ua.Core.dll @@ -30,6 +31,7 @@ Libraries\Opc.Ua.Gds.Server.Common\bin\Release\netstandard2.1\Opc.Ua.Gds.Server. Libraries\Opc.Ua.Gds.Server.Common\bin\Release\net48\Opc.Ua.Gds.Server.Common.dll Libraries\Opc.Ua.Gds.Server.Common\bin\Release\net6.0\Opc.Ua.Gds.Server.Common.dll Libraries\Opc.Ua.Gds.Server.Common\bin\Release\net8.0\Opc.Ua.Gds.Server.Common.dll +Libraries\Opc.Ua.Security.Certificates\bin\Release\netstandard2.0\Opc.Ua.Security.Certificates.dll Libraries\Opc.Ua.Security.Certificates\bin\Release\netstandard2.1\Opc.Ua.Security.Certificates.dll Libraries\Opc.Ua.Security.Certificates\bin\Release\net48\Opc.Ua.Security.Certificates.dll Libraries\Opc.Ua.Security.Certificates\bin\Release\net6.0\Opc.Ua.Security.Certificates.dll diff --git a/Docs/PlatformBuild.md b/Docs/PlatformBuild.md index 82412d898..634aeba15 100644 --- a/Docs/PlatformBuild.md +++ b/Docs/PlatformBuild.md @@ -50,17 +50,17 @@ The following platform is deprecated but can still be built and tested: To reduce the ci build overhead and the number of tests to be run in Visual Studio, only the tagged versions (* and **) are part of a qualifying ci build to pass a pull request. All other platforms are only tested in weekly scheduled or manual ci builds. -By default, in Visual Studio only the platforms tagged with (*) are tested. In order to test the other platforms in a command line window or in VS, there is a custom build variable defined to target a specific build. E.g. to target a .NETStandard2.1 build, the test runners are compiled with .NET 6.0 but the class libraries target only netstandard2.1, to force the use of that target. +By default, in Visual Studio only the platforms tagged with (*) are tested. In order to test the other platforms in a command line window or in VS, there is a custom build variable defined to target a specific build. E.g. to target a .NETStandard2.0 build, the test runners are compiled with .NET 6.0 but the class libraries target only netstandard2.0, to force the use of that target. Another option is to test run such a custom target in a command window with a batch file [CustomTest.bat](../Tests/customtest.bat) which is provided to clean up, restore the project and to run the tests. To run the custom tests in Visual Studio a section in [target.props](../targets.props) needs to be uncommented and the target platform value must be set. ```xml - netstandard2.1 + netstandard2.0 ``` diff --git a/Libraries/Opc.Ua.Security.Certificates/Opc.Ua.Security.Certificates.csproj b/Libraries/Opc.Ua.Security.Certificates/Opc.Ua.Security.Certificates.csproj index ce5446355..47b533cc3 100644 --- a/Libraries/Opc.Ua.Security.Certificates/Opc.Ua.Security.Certificates.csproj +++ b/Libraries/Opc.Ua.Security.Certificates/Opc.Ua.Security.Certificates.csproj @@ -2,7 +2,7 @@ Opc.Ua.Security.Certificates - $(LibTargetFrameworks) + $(LibCoreTargetFrameworks) OPCFoundation.NetStandard.Opc.Ua.Security.Certificates Opc.Ua.Security.Certificates OPC UA Security X509 Certificates Class Library diff --git a/Stack/Opc.Ua.Bindings.Https/Opc.Ua.Bindings.Https.csproj b/Stack/Opc.Ua.Bindings.Https/Opc.Ua.Bindings.Https.csproj index f91439637..2ce01329f 100644 --- a/Stack/Opc.Ua.Bindings.Https/Opc.Ua.Bindings.Https.csproj +++ b/Stack/Opc.Ua.Bindings.Https/Opc.Ua.Bindings.Https.csproj @@ -49,7 +49,7 @@ - + diff --git a/Stack/Opc.Ua.Core/Opc.Ua.Core.csproj b/Stack/Opc.Ua.Core/Opc.Ua.Core.csproj index 0c5fa65e7..4874bd731 100644 --- a/Stack/Opc.Ua.Core/Opc.Ua.Core.csproj +++ b/Stack/Opc.Ua.Core/Opc.Ua.Core.csproj @@ -2,7 +2,7 @@ $(DefineConstants);NET_STANDARD;NET_STANDARD_ASYNC - $(LibTargetFrameworks) + $(LibCoreTargetFrameworks) Opc.Ua.Core OPCFoundation.NetStandard.Opc.Ua.Core Opc.Ua diff --git a/Tests/Opc.Ua.Core.Tests/Types/Encoders/BinaryDecoderBenchmarks.cs b/Tests/Opc.Ua.Core.Tests/Types/Encoders/BinaryDecoderBenchmarks.cs index f792af20f..a4ea14784 100644 --- a/Tests/Opc.Ua.Core.Tests/Types/Encoders/BinaryDecoderBenchmarks.cs +++ b/Tests/Opc.Ua.Core.Tests/Types/Encoders/BinaryDecoderBenchmarks.cs @@ -72,7 +72,7 @@ public void BinaryDecoderArraySegmentStreamTest() [Test] public void BinaryDecoderArraySegmentStreamNoSpanTest() { -#if NET6_0_OR_GREATER +#if NET6_0_OR_GREATER && ECC_SUPPORT using (var arraySegmentStream = new ArraySegmentStreamNoSpan(m_encodedBufferList)) #else using (var arraySegmentStream = new ArraySegmentStream(m_encodedBufferList)) @@ -116,7 +116,7 @@ public void BinaryDecoderArraySegmentStream() [Test] public void BinaryDecoderArraySegmentStreamNoSpan() { -#if NET6_0_OR_GREATER +#if NET6_0_OR_GREATER && ECC_SUPPORT using (var arraySegmentStream = new ArraySegmentStreamNoSpan(m_encodedBufferList)) #else using (var arraySegmentStream = new ArraySegmentStream(m_encodedBufferList)) diff --git a/Tests/Opc.Ua.Core.Tests/Types/Encoders/BinaryEncoderBenchmarks.cs b/Tests/Opc.Ua.Core.Tests/Types/Encoders/BinaryEncoderBenchmarks.cs index 1dda74441..fff30f452 100644 --- a/Tests/Opc.Ua.Core.Tests/Types/Encoders/BinaryEncoderBenchmarks.cs +++ b/Tests/Opc.Ua.Core.Tests/Types/Encoders/BinaryEncoderBenchmarks.cs @@ -167,7 +167,7 @@ public void BinaryEncoderArraySegmentStream() [Test] public void BinaryEncoderArraySegmentStreamNoSpan() { -#if NET6_0_OR_GREATER +#if NET6_0_OR_GREATER && ECC_SUPPORT using (var arraySegmentStream = new ArraySegmentStreamNoSpan(m_bufferManager)) #else using (var arraySegmentStream = new ArraySegmentStream(m_bufferManager)) diff --git a/Tests/Opc.Ua.Core.Tests/Types/Encoders/EncoderBenchmarks.cs b/Tests/Opc.Ua.Core.Tests/Types/Encoders/EncoderBenchmarks.cs index ff9a60ae4..4f834b320 100644 --- a/Tests/Opc.Ua.Core.Tests/Types/Encoders/EncoderBenchmarks.cs +++ b/Tests/Opc.Ua.Core.Tests/Types/Encoders/EncoderBenchmarks.cs @@ -171,7 +171,7 @@ public void GlobalCleanup() #endregion } -#if NET6_0_OR_GREATER +#if NET6_0_OR_GREATER && ECC_SUPPORT /// /// Helper class to test ArraySegmentStream without Span support. /// diff --git a/Tests/Opc.Ua.Core.Tests/Types/Encoders/JsonEncoderBenchmarks.cs b/Tests/Opc.Ua.Core.Tests/Types/Encoders/JsonEncoderBenchmarks.cs index e35d97709..2ac94acae 100644 --- a/Tests/Opc.Ua.Core.Tests/Types/Encoders/JsonEncoderBenchmarks.cs +++ b/Tests/Opc.Ua.Core.Tests/Types/Encoders/JsonEncoderBenchmarks.cs @@ -191,7 +191,8 @@ public void JsonEncoderArraySegmentStream() [Test] public void JsonEncoderArraySegmentStreamNoSpan() { -#if NET6_0_OR_GREATER + // ECC_SUPPORT is used to distinguish also from platforms which do not support Span +#if NET6_0_OR_GREATER && ECC_SUPPORT using (var arraySegmentStream = new ArraySegmentStreamNoSpan(m_bufferManager)) #else using (var arraySegmentStream = new ArraySegmentStream(m_bufferManager)) diff --git a/Tests/customtest.bat b/Tests/customtest.bat index 366a3e37e..4ffc64b19 100644 --- a/Tests/customtest.bat +++ b/Tests/customtest.bat @@ -2,19 +2,19 @@ setlocal enabledelayedexpansion echo This script is used to run custom platform tests for the UA Core Library -echo Supported parameters: net462, netstandard2.1, net48, net6.0, net8.0 +echo Supported parameters: net462, netstandard2.0, netstandard2.1, net48, net6.0, net8.0 REM Check if the target framework parameter is provided if "%1"=="" ( echo Usage: %0 [TargetFramework] - echo Allowed values for TargetFramework: net462, netstandard2.1, net48, net6.0, net8.0, default + echo Allowed values for TargetFramework: net462, netstandard2.0, netstandard2.1, net48, net6.0, net8.0, default goto :eof ) REM Check if the provided TargetFramework is valid -set "validFrameworks= default net462 netstandard2.1 net48 net6.0 net8.0" +set "validFrameworks= default net462 net472 netstandard2.0 netstandard2.1 net48 net6.0 net8.0 " if "!validFrameworks: %1 =!"=="%validFrameworks%" ( - echo Invalid TargetFramework specified. Allowed values are: default, net462, netstandard2.1, net48, net6.0, net8.0 + echo Invalid TargetFramework specified. Allowed values are: default, net462, net472 netstandard2.0, netstandard2.1, net48, net6.0, net8.0 goto :eof ) diff --git a/UA Core Library.sln b/UA Core Library.sln index 4f68a3306..85c776fc5 100644 --- a/UA Core Library.sln +++ b/UA Core Library.sln @@ -47,7 +47,6 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "azurepipelines", "azurepipelines", "{1C25BE72-C337-42AE-9F7C-D6B45F7B7079}" ProjectSection(SolutionItems) = preProject .azurepipelines\ci.yml = .azurepipelines\ci.yml - .azurepipelines\customtest.yml = .azurepipelines\customtest.yml .azurepipelines\get-matrix.ps1 = .azurepipelines\get-matrix.ps1 .azurepipelines\get-root.ps1 = .azurepipelines\get-root.ps1 .azurepipelines\get-version.ps1 = .azurepipelines\get-version.ps1 diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 9b228f911..a97686ef4 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -114,6 +114,30 @@ stages: jobnamesuffix: net462 agents: '@{ windows = "windows-2022" }' customtestarget: net462 +- stage: testnet472 + dependsOn: [build] + displayName: 'Test .NET 4.7.2' + condition: and(succeeded(), ne(variables.ScheduledBuild, 'False')) + jobs: + - template: .azurepipelines/test.yml + parameters: + framework: net472 + configuration: Release + jobnamesuffix: net472 + agents: '@{ windows = "windows-2022" }' + customtestarget: net472 +- stage: testnetstandard20 + dependsOn: [build] + displayName: 'Test .NETStandard 2.0' + condition: and(succeeded(), ne(variables.ScheduledBuild, 'False')) + jobs: + - template: .azurepipelines/test.yml + parameters: + framework: net6.0 + configuration: Release + jobnamesuffix: netstandard20 + agents: '@{ windows = "windows-2019"; linux="ubuntu-20.04"}' + customtestarget: netstandard2.0 - stage: testnetstandard21 dependsOn: [build] displayName: 'Test .NETStandard 2.1' diff --git a/targets.props b/targets.props index 7d41ba1c2..f13e72980 100644 --- a/targets.props +++ b/targets.props @@ -30,6 +30,7 @@ net462 net462 net462 + net462 net462 net462 @@ -41,6 +42,7 @@ net472 net472 net472 + net472 net472 net472 @@ -53,6 +55,7 @@ net6.0 net6.0 netstandard2.0 + netstandard2.0 netstandard2.1 netstandard2.0 @@ -64,6 +67,7 @@ net6.0 net6.0 netstandard2.1 + netstandard2.1 netstandard2.1 netstandard2.1 @@ -75,6 +79,7 @@ net48 net48 net48 + net48 net48 net48 @@ -86,6 +91,7 @@ net6.0 net6.0 net6.0 + net6.0 net6.0 net6.0 @@ -97,13 +103,14 @@ net8.0 net8.0 net8.0 + net8.0 net8.0 net8.0 - + @@ -112,6 +119,7 @@ net6.0 net48;net6.0 net48;netstandard2.1;net6.0;net8.0 + net48;netstandard2.0;netstandard2.1;net6.0;net8.0 net48;netstandard2.1;net6.0;net8.0 net48;netcoreapp3.1;net6.0;net8.0 @@ -124,6 +132,7 @@ netcoreapp3.1 net48;netcoreapp3.1 net48;netstandard2.1 + net48;netstandard2.0;netstandard2.1 net48;netstandard2.1 net48;netcoreapp3.1 @@ -135,6 +144,7 @@ net48 net48 net48 + net48 net48 net48 From d4f1d401494a7bd38e3cbd46fa23237c5ecd50b5 Mon Sep 17 00:00:00 2001 From: romanett Date: Fri, 5 Apr 2024 08:13:04 +0200 Subject: [PATCH 9/9] add Nuget Package Readme and file (#2575) * add Nuget Package Readme and file --- Docs/NugetREADME.md | 36 ++++++++++++++++++++++++++++++++++++ common.props | 5 +++++ 2 files changed, 41 insertions(+) create mode 100644 Docs/NugetREADME.md diff --git a/Docs/NugetREADME.md b/Docs/NugetREADME.md new file mode 100644 index 000000000..311df01d7 --- /dev/null +++ b/Docs/NugetREADME.md @@ -0,0 +1,36 @@ +# OPC UA .NET Standard stack documentation + +## Overview + +The OPC UA .NET Standard stack enables you to build multi-platform OPC UA Applications with rich functionality including: + - Server + - Client + - PubSub + + +### For more information and license terms, see the projects official [Website](http://opcfoundation.github.io/UA-.NETStandard). + +## Getting started + +The reference [Client](https://github.com/OPCFoundation/UA-.NETStandard/tree/master/Applications/ConsoleReferenceClient) & [Server](https://github.com/OPCFoundation/UA-.NETStandard/tree/master/Applications/ReferenceServer) projects provide a starting point in implementing your own application. + + + + +## Packages Overview + +Caution, there are multiple packages available with different functional scopes, for a detailed overview take a look at the [Information about the different Packages](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/Docs/PlatformBuild.md#further-information-on-the-supported-nuget-packages). + +## Additional documentation + +Additional information about the OPC UA .NET Standard stack is available on the GitHub Repo of the project in the [detailed Documentation](https://github.com/OPCFoundation/UA-.NETStandard/tree/master/Docs#opc-ua-net-standard-stack-documentation). \ No newline at end of file diff --git a/common.props b/common.props index 84f110532..7041993c9 100644 --- a/common.props +++ b/common.props @@ -37,6 +37,7 @@ licenses/LICENSE.txt $(RepositoryUrl)/releases + NugetREADME.md OPCFoundation OPC UA netstandard ios linux dotnet net netcore uwp @@ -52,6 +53,10 @@ + + + +