Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use single array as content for all VeryFastByteString nodes to save memory #385

Merged
merged 9 commits into from
Aug 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,7 @@ More information about this feature can be found [here](deterministic-alarms.md)
- Nodes with 1 kB (ByteString) values: `--vf1k`. The first byte cycles from 0 to 255 in a configurable rate in ms: `--vf1kr`. The values are deterministic but scrambled to ensure that they are not efficiently compressed.
- Load binary *.PredefinedNodes.uanodes file(s) compiled from an XML NodeSet: `--unf=<PredefinedNodes_uanodes>`
- Load *.NodeSet2.xml file(s): `--ns2=<NodeSet2_xml>`
- Node that shows working set memory consumption in MB: Root/Objects/OpcPlc/Telemetry/Special/WorkingSetMB

## OPC UA Methods
| Name | Description | Prerequisite |
Expand Down
1 change: 0 additions & 1 deletion src/AlarmCondition/AlarmConditionNodeManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@

using System;
using System.Collections.Generic;
using System.Reflection.Emit;
using System.Threading;
using Opc.Ua;
using Opc.Ua.Server;
Expand Down
14 changes: 6 additions & 8 deletions src/PlcNodeManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,11 @@ public PlcNodeManager(IServerInternal server, OpcPlcConfiguration config, Applic
/// </summary>
public override NodeId New(ISystemContext context, NodeState node)
{
if (node is BaseInstanceState instance && instance.Parent != null &&
instance.Parent.NodeId.Identifier is string id)
{
return new NodeId(id + "_" + instance.SymbolicName, instance.Parent.NodeId.NamespaceIndex);
}

return node.NodeId;
return node is BaseInstanceState instance &&
instance.Parent != null &&
instance.Parent.NodeId.Identifier is string id
? new NodeId(id + "_" + instance.SymbolicName, instance.Parent.NodeId.NamespaceIndex)
: node.NodeId;
}

/// <summary>
Expand Down Expand Up @@ -105,7 +103,7 @@ public FolderState CreateFolder(NodeState parent, string path, string name, Name

ushort namespaceIndex = NamespaceIndexes[(int)namespaceType];

var folder = new FolderState(parent)
var folder = new FolderState(parent)
{
SymbolicName = name,
ReferenceTypeId = ReferenceTypes.Organizes,
Expand Down
73 changes: 38 additions & 35 deletions src/PlcServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,9 @@ public partial class PlcServer : StandardServer
private uint _countCreateSession;
private uint _countCreateSubscription;
private uint _countCreateMonitoredItems;
private uint _countPublish;
private uint _countRead;
private uint _countWrite;
private uint _countPublish;

public PlcServer(OpcPlcConfiguration config, PlcSimulation plcSimulation, TimeService timeService, ImmutableList<IPluginNodes> pluginNodes, ILogger logger)
{
Expand All @@ -64,37 +64,38 @@ public PlcServer(OpcPlcConfiguration config, PlcSimulation plcSimulation, TimeSe
{
var curProc = Process.GetCurrentProcess();

ThreadPool.GetAvailableThreads(out int availWorkerThreads, out int availCompletionPortThreads);
ThreadPool.GetAvailableThreads(out int availWorkerThreads, out _);

int sessionCount = ServerInternal.SessionManager.GetSessions().Count;
uint sessionCount = ServerInternal.ServerDiagnostics.CurrentSessionCount;
IList<Subscription> subscriptions = ServerInternal.SubscriptionManager.GetSubscriptions();
int monitoredItemsCount = subscriptions.Sum(s => s.MonitoredItemCount);

_autoDisablePublishMetrics = sessionCount > 40 || monitoredItemsCount > 500;

LogPeriodicInfo(
sessionCount,
subscriptions.Count,
ServerInternal.ServerDiagnostics.CurrentSubscriptionCount,
monitoredItemsCount,
ServerInternal.ServerDiagnostics.CumulatedSessionCount,
ServerInternal.ServerDiagnostics.CumulatedSubscriptionCount,
curProc.WorkingSet64 / 1024 / 1024,
availWorkerThreads,
availCompletionPortThreads,
curProc.Threads.Count,
PeriodicLoggingTimerSeconds,
_countCreateSession,
_countCreateSubscription,
_countCreateMonitoredItems,
_countPublish,
_countRead,
_countWrite,
_countPublish,
PublishMetricsEnabled);

_countCreateSession = 0;
_countCreateSubscription = 0;
_countCreateMonitoredItems = 0;
_countPublish = 0;
_countRead = 0;
_countWrite = 0;
_countPublish = 0;
}
catch
{
Expand Down Expand Up @@ -178,15 +179,14 @@ public override ResponseHeader CreateSubscription(

try
{
OperationContext context = ValidateRequest(requestHeader, RequestType.CreateSubscription);

var responseHeader = base.CreateSubscription(requestHeader, requestedPublishingInterval, requestedLifetimeCount, requestedMaxKeepAliveCount, maxNotificationsPerPublish, publishingEnabled, priority, out subscriptionId, out revisedPublishingInterval, out revisedLifetimeCount, out revisedMaxKeepAliveCount);

MetricsHelper.AddSubscriptionCount(context.SessionId.ToString(), subscriptionId.ToString());
NodeId sessionId = GetSessionId(requestHeader.AuthenticationToken);
MetricsHelper.AddSubscriptionCount(sessionId.ToString(), subscriptionId.ToString());

LogSuccessWithSessionIdAndSubscriptionId(
nameof(CreateSubscription),
context.SessionId,
sessionId,
subscriptionId);

return responseHeader;
Expand Down Expand Up @@ -215,17 +215,18 @@ public override ResponseHeader CreateMonitoredItems(

try
{
OperationContext context = ValidateRequest(requestHeader, RequestType.CreateMonitoredItems);

var responseHeader = base.CreateMonitoredItems(requestHeader, subscriptionId, timestampsToReturn, itemsToCreate, out results, out diagnosticInfos);

MetricsHelper.AddMonitoredItemCount(itemsToCreate.Count);

LogSuccessWithSessionIdAndSubscriptionIdAndCount(
nameof(CreateMonitoredItems),
context.SessionId,
subscriptionId,
itemsToCreate.Count);
if(_logger.IsEnabled(LogLevel.Debug))
{
LogSuccessWithSessionIdAndSubscriptionIdAndCount(
nameof(CreateMonitoredItems),
GetSessionId(requestHeader.AuthenticationToken),
subscriptionId,
itemsToCreate.Count);
}

return responseHeader;
}
Expand Down Expand Up @@ -259,19 +260,20 @@ public override ResponseHeader Publish(

try
{
OperationContext context = ValidateRequest(requestHeader, RequestType.Publish);

var responseHeader = base.Publish(requestHeader, subscriptionAcknowledgements, out subscriptionId, out availableSequenceNumbers, out moreNotifications, out notificationMessage, out results, out diagnosticInfos);

if (PublishMetricsEnabled)
{
AddPublishMetrics(notificationMessage);
}

LogSuccessWithSessionIdAndSubscriptionId(
nameof(Publish),
context.SessionId,
subscriptionId);
if (_logger.IsEnabled(LogLevel.Debug))
{
LogSuccessWithSessionIdAndSubscriptionId(
nameof(Publish),
GetSessionId(requestHeader.AuthenticationToken),
subscriptionId);
}

return responseHeader;
}
Expand Down Expand Up @@ -599,38 +601,39 @@ private void AddPublishMetrics(NotificationMessage notificationMessage)
MetricsHelper.AddPublishedCount(dataChanges, events);
}

private NodeId GetSessionId(NodeId authenticationToken) => ServerInternal.SessionManager.GetSession(authenticationToken).Id;

[LoggerMessage(
Level = LogLevel.Information,
Message = "\n\t# Open sessions: {Sessions}\n" +
"\t# Open subscriptions: {Subscriptions}\n" +
Message = "\n\t# Open/total sessions: {Sessions}/{TotalSessions}\n" +
"\t# Open/total subscriptions: {Subscriptions}/{TotalSubscriptions}\n" +
"\t# Monitored items: {MonitoredItems:N0}\n" +
"\t# Working set: {WorkingSet:N0} MB\n" +
"\t# Available worker threads: {AvailWorkerThreads:N0}\n" +
"\t# Available completion port threads: {AvailCompletionPortThreads:N0}\n" +
"\t# Thread count: {ThreadCount:N0}\n" +
"\t# Statistics for the last {PeriodicLoggingTimerSeconds} s\n" +
"\t# Used/available worker threads: {ThreadCount:N0}/{AvailWorkerThreads:N0}\n" +
"\t# Stats for the last {PeriodicLoggingTimerSeconds} s\n" +
"\t# Sessions created: {CountCreateSession}\n" +
"\t# Subscriptions created: {CountCreateSubscription}\n" +
"\t# Monitored items created: {CountCreateMonitoredItems}\n" +
"\t# Publish requests: {CountPublish}\n" +
"\t# Read requests: {CountRead}\n" +
"\t# Write requests: {CountWrite}\n" +
"\t# Publish requests: {CountPublish}\n" +
"\t# Publish metrics enabled: {PublishMetricsEnabled:N0}")]
partial void LogPeriodicInfo(
int sessions,
int subscriptions,
uint sessions,
uint subscriptions,
int monitoredItems,
uint totalSessions,
uint totalSubscriptions,
long workingSet,
int availWorkerThreads,
int availCompletionPortThreads,
int threadCount,
int periodicLoggingTimerSeconds,
uint countCreateSession,
uint countCreateSubscription,
uint countCreateMonitoredItems,
uint countPublish,
uint countRead,
uint countWrite,
uint countPublish,
bool publishMetricsEnabled);

[LoggerMessage(
Expand Down
2 changes: 1 addition & 1 deletion src/PluginNodes/VeryFast1KBPluginNodes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ public void StartSimulation()
// Only use the fast timers when we need to go really fast,
// since they consume more resources and create an own thread.
_nodeGenerator = NodeRate >= 50 || !Stopwatch.IsHighResolution
? _timeService.NewTimer((s, e) => UpdateNodes(), NodeRate)
? _timeService.NewTimer((s, e) => UpdateNodes(), intervalInMilliseconds: NodeRate)
: _timeService.NewFastTimer((s, e) => UpdateNodes(), intervalInMilliseconds: NodeRate);
}

Expand Down
43 changes: 21 additions & 22 deletions src/PluginNodes/VeryFastByteStringPluginNodes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ public class VeryFastByteStringPluginNodes(TimeService timeService, ILogger logg
private readonly DeterministicGuid _deterministicGuid = new();
private PlcNodeManager _plcNodeManager;
private BaseDataVariableState[] _veryFastByteStringNodes;
private byte[] _byteString;
private ITimer _nodeGenerator;

public void AddOptions(Mono.Options.OptionSet optionSet)
Expand Down Expand Up @@ -65,7 +66,7 @@ public void StartSimulation()
// Only use the fast timers when we need to go really fast,
// since they consume more resources and create an own thread.
_nodeGenerator = NodeRate >= 50 || !Stopwatch.IsHighResolution
? _timeService.NewTimer((s, e) => UpdateNodes(), NodeRate)
? _timeService.NewTimer((s, e) => UpdateNodes(), intervalInMilliseconds: NodeRate)
: _timeService.NewFastTimer((s, e) => UpdateNodes(), intervalInMilliseconds: NodeRate);
}

Expand All @@ -75,22 +76,23 @@ public void StopSimulation()
{
_nodeGenerator.Enabled = false;
}
}
private void AddNodes(FolderState folder)
}

private void AddNodes(FolderState folder)
{
var nodes = new List<NodeWithIntervals>();
var nodes = new List<NodeWithIntervals>();
_veryFastByteStringNodes = new BaseDataVariableState[NodeCount];

// Use min. node size of 1 byte.
int nodeSize = NodeSize < 1
? 1
: (int)NodeSize;


string nodeSizeGuid = GetLongDeterministicGuid(nodeSize);
_byteString = Encoding.UTF8.GetBytes(nodeSizeGuid);

for (int i = 0; i < NodeCount; i++)
{
string nodeSizeGuid = GetLongDeterministicGuid(nodeSize);
var initialByteArray = Encoding.UTF8.GetBytes(nodeSizeGuid);

string name = $"VeryFastByteString{(i + 1)}";

_veryFastByteStringNodes[i] = _plcNodeManager.CreateBaseVariable(
Expand All @@ -102,7 +104,7 @@ private void AddNodes(FolderState folder)
AccessLevels.CurrentReadOrWrite,
"Very fast changing ByteString node",
NamespaceType.OpcPlcApplications,
initialByteArray);
_byteString);

// Update pn.json output.
nodes.Add(new NodeWithIntervals {
Expand All @@ -113,26 +115,23 @@ private void AddNodes(FolderState folder)

Nodes = nodes;
}
}

}
private void UpdateNodes()
{
// Update first byte in the range 0 to 255.
_byteString[0] = _byteString[0] == 255
? (byte)0
: (byte)(_byteString[0] + 1);

for (int i = 0; i < _veryFastByteStringNodes.Length; i++)
{
byte[] arrayValue = (byte[])_veryFastByteStringNodes[i].Value;

// Update first byte in the range 0 to 255.
arrayValue[0] = arrayValue[0] == 255
? (byte)0
: (byte)(arrayValue[0] + 1);

SetValue(_veryFastByteStringNodes[i], arrayValue);
UpdateValue(_veryFastByteStringNodes[i]);
}
}

private void SetValue<T>(BaseVariableState variable, T value)
private void UpdateValue(BaseVariableState variable)
{
variable.Value = value;
variable.Timestamp = _timeService.Now();
variable.ClearChangeMasks(_plcNodeManager.SystemContext, includeChildren: false);
}
Expand Down
68 changes: 68 additions & 0 deletions src/PluginNodes/WorkingSetPluginNode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
namespace OpcPlc.PluginNodes;

using Microsoft.Extensions.Logging;
using Opc.Ua;
using OpcPlc.Helpers;
using OpcPlc.PluginNodes.Models;
using System.Diagnostics;

/// <summary>
/// Node that shows current working set memory consumption in MB.
/// </summary>
public class WorkingSetPluginNode(TimeService timeService, ILogger logger) : PluginNodeBase(timeService, logger), IPluginNodes
{
private PlcNodeManager _plcNodeManager;
private SimulatedVariableNode<uint> _node;

public void AddOptions(Mono.Options.OptionSet optionSet)
{
// Enabled by default.
}

public void AddToAddressSpace(FolderState telemetryFolder, FolderState methodsFolder, PlcNodeManager plcNodeManager)
{
_plcNodeManager = plcNodeManager;

FolderState folder = _plcNodeManager.CreateFolder(
telemetryFolder,
path: "Special",
name: "Special",
NamespaceType.OpcPlcApplications);

AddNodes(folder);
}

public void StartSimulation()
{
_node.Start(value => GetWorkingSetMB(), periodMs: 1000);
}

public void StopSimulation()
{
_node.Stop();
}

private void AddNodes(FolderState folder)
{
BaseDataVariableState variable = _plcNodeManager.CreateBaseVariable(
folder,
path: "WorkingSetMB",
name: "WorkingSetMB",
new NodeId((uint)BuiltInType.UInt32),
ValueRanks.Scalar,
AccessLevels.CurrentReadOrWrite,
"Working set memory consumption in MB",
NamespaceType.OpcPlcApplications,
defaultValue: GetWorkingSetMB());

_node = _plcNodeManager.CreateVariableNode<uint>(variable);

// Add to node list for creation of pn.json.
Nodes =
[
PluginNodesHelper.GetNodeWithIntervals(variable.NodeId, _plcNodeManager),
];
}

private uint GetWorkingSetMB() => (uint)(Process.GetCurrentProcess().WorkingSet64 / 1024 / 1024);
}
Loading
Loading