/* ========================================================================
* 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 System.Threading.Tasks;
using Opc.Ua.Server;
namespace Opc.Ua.Perf.ServerLoadHarness
{
///
/// The counters of the subscription capacity workload of the o6-automation opcua-benchmarks
/// (UInt64 counter per monitored item, advanced by its read callback, so only sampling drives
/// them), shared by the node manager variants.
///
internal sealed class CounterValues
{
public CounterValues(int count, bool lockedCallback)
{
m_lockedCallback = lockedCallback;
m_values = new ulong[count];
m_marked = new ulong[count];
}
///
/// Creates the counter variables ns=namespaceIndex;i=1..count below the Objects folder.
///
public IEnumerable CreateNodes(ushort namespaceIndex)
{
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(Increment(index));
status = StatusCodes.Good;
timestamp = DateTimeUtc.Now;
return ServiceResult.Good;
};
node.AddReference(ReferenceTypeIds.Organizes, true, ObjectIds.ObjectsFolder);
yield return 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 ulong Increment(int index)
{
if (m_lockedCallback)
{
// the o6-automation .NET worker increments under a lock it owns
lock (m_lock)
{
return ++m_values[index];
}
}
return Interlocked.Increment(ref m_values[index]);
}
private readonly bool m_lockedCallback;
private readonly Lock m_lock = new();
private readonly ulong[] m_values;
private readonly ulong[] m_marked;
private long m_markTimestamp = Stopwatch.GetTimestamp();
}
///
/// The counters on with sampling groups.
///
internal sealed class CounterNodeManager : CustomNodeManager2
{
public const string Namespace = "urn:opcfoundation.org:perf:counters";
public CounterNodeManager(
IServerInternal server,
ApplicationConfiguration configuration,
CounterValues counters,
string namespaceUri)
: base(server, configuration, useSamplingGroups: true, namespaceUri)
{
m_counters = counters;
}
public override void CreateAddressSpace(IDictionary> externalReferences)
{
if (!externalReferences.TryGetValue(ObjectIds.ObjectsFolder, out IList? references))
{
externalReferences[ObjectIds.ObjectsFolder] = references = new List();
}
foreach (BaseDataVariableState node in m_counters.CreateNodes(NamespaceIndex))
{
references.Add(new NodeStateReference(ReferenceTypeIds.Organizes, false, node.NodeId));
AddPredefinedNode(SystemContext, node);
}
}
private readonly CounterValues m_counters;
}
///
/// The counters on with sampling groups, as the
/// o6-automation .NET worker for UA-.NETStandard 2.0 builds them.
///
internal sealed class AsyncCounterNodeManager : AsyncCustomNodeManager
{
public AsyncCounterNodeManager(
IServerInternal server,
ApplicationConfiguration configuration,
CounterValues counters,
string namespaceUri)
: base(server, configuration, useSamplingGroups: true, namespaceUri)
{
m_counters = counters;
}
public override async ValueTask CreateAddressSpaceAsync(
IDictionary> externalReferences,
CancellationToken cancellationToken = default)
{
await base.CreateAddressSpaceAsync(externalReferences, cancellationToken).ConfigureAwait(false);
if (!externalReferences.TryGetValue(ObjectIds.ObjectsFolder, out IList? references))
{
externalReferences[ObjectIds.ObjectsFolder] = references = new List();
}
foreach (BaseDataVariableState node in m_counters.CreateNodes(NamespaceIndex))
{
references.Add(new NodeStateReference(ReferenceTypeIds.Organizes, false, node.NodeId));
await AddPredefinedNodeAsync(SystemContext, node, cancellationToken).ConfigureAwait(false);
}
}
private readonly CounterValues m_counters;
}
///
/// 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,
string namespaceUri = CounterNodeManager.Namespace,
bool lockedCallback = false,
bool asyncNodeManager = false)
: base(telemetry)
{
Counters = new CounterValues(count, lockedCallback);
m_namespaceUri = namespaceUri;
m_asyncNodeManager = asyncNodeManager;
}
public CounterValues Counters { get; }
protected override IMasterNodeManager CreateMasterNodeManager(
IServerInternal server,
ApplicationConfiguration configuration)
{
return m_asyncNodeManager
? new MasterNodeManager(
server, configuration, null, new AsyncCounterNodeManager(server, configuration, Counters, m_namespaceUri))
: new MasterNodeManager(
server, configuration, null, new CounterNodeManager(server, configuration, Counters, m_namespaceUri));
}
private readonly string m_namespaceUri;
private readonly bool m_asyncNodeManager;
}
}