/* ======================================================================== * Copyright (c) 2005-2025 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.Diagnostics; using System.Globalization; using System.Threading; using Opc.Ua.Server; namespace Opc.Ua.Perf.ServerLoadHarness { /// /// Counter variables advanced by their read callback, so only sampling drives them. /// Mirrors the subscription capacity workload of the o6-automation opcua-benchmarks /// (UInt64 counter per monitored item, sampling groups, read-callback increments). /// internal sealed class CounterNodeManager : CustomNodeManager2 { public const string Namespace = "urn:opcfoundation.org:perf:counters"; public CounterNodeManager(IServerInternal server, ApplicationConfiguration configuration, int count) : base(server, configuration, useSamplingGroups: true, Namespace) { m_values = new ulong[count]; m_marked = new ulong[count]; } public override void CreateAddressSpace(IDictionary> externalReferences) { if (!externalReferences.TryGetValue(ObjectIds.ObjectsFolder, out IList? references)) { externalReferences[ObjectIds.ObjectsFolder] = references = new List(); } for (int i = 0; i < m_values.Length; i++) { int index = i; var node = new BaseDataVariableState(null) { NodeId = new NodeId((uint)i + 1, NamespaceIndex), BrowseName = new QualifiedName("Counter" + i.ToString(CultureInfo.InvariantCulture), NamespaceIndex), DisplayName = new LocalizedText("Counter" + i.ToString(CultureInfo.InvariantCulture)), TypeDefinitionId = VariableTypeIds.BaseDataVariableType, ReferenceTypeId = ReferenceTypeIds.Organizes, DataType = DataTypeIds.UInt64, ValueRank = ValueRanks.Scalar, MinimumSamplingInterval = 1, AccessLevel = AccessLevels.CurrentRead, UserAccessLevel = AccessLevels.CurrentRead, Value = new Variant((ulong)0), StatusCode = StatusCodes.Good, Timestamp = DateTimeUtc.Now }; node.OnReadValue = (ISystemContext context, NodeState state, NumericRange range, QualifiedName encoding, ref Variant value, ref StatusCode status, ref DateTimeUtc timestamp) => { value = new Variant(Interlocked.Increment(ref m_values[index])); status = StatusCodes.Good; timestamp = DateTimeUtc.Now; return ServiceResult.Good; }; node.AddReference(ReferenceTypeIds.Organizes, true, ObjectIds.ObjectsFolder); references.Add(new NodeStateReference(ReferenceTypeIds.Organizes, false, node.NodeId)); AddPredefinedNode(SystemContext, node); } } /// /// Starts a measurement window. /// public void Mark() { for (int i = 0; i < m_values.Length; i++) { m_marked[i] = Interlocked.Read(ref m_values[i]); } m_markTimestamp = Stopwatch.GetTimestamp(); } /// /// Reports the sampling progress of the window: completed read callbacks per item /// against the number the sampling interval calls for. /// public string Report(double samplingIntervalMs) { double elapsedMs = Stopwatch.GetElapsedTime(m_markTimestamp).TotalMilliseconds; double expected = elapsedMs / samplingIntervalMs; double min = double.MaxValue; double sum = 0; int belowTarget = 0; for (int i = 0; i < m_values.Length; i++) { double progress = (Interlocked.Read(ref m_values[i]) - m_marked[i]) / expected; min = Math.Min(min, progress); sum += progress; if (progress < 0.99) { belowTarget++; } } return FormattableString.Invariant( $"counters={m_values.Length} samplesExpected={expected:F0} progressMin={min:P2} ") + FormattableString.Invariant( $"progressMean={sum / m_values.Length:P2} itemsBelow99={belowTarget}"); } private readonly ulong[] m_values; private readonly ulong[] m_marked; private long m_markTimestamp = Stopwatch.GetTimestamp(); } /// /// A server exposing only the counter node manager in addition to the standard nodes. /// internal sealed class CounterServer : StandardServer { public CounterServer(ITelemetryContext telemetry, int count) : base(telemetry) { m_count = count; } public CounterNodeManager? Counters { get; private set; } protected override IMasterNodeManager CreateMasterNodeManager( IServerInternal server, ApplicationConfiguration configuration) { Counters = new CounterNodeManager(server, configuration, m_count); return new MasterNodeManager(server, configuration, null, Counters); } private readonly int m_count; } }