mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-19 15:22:15 +08:00
dbc09b8e22
# Description Adopt [`System.TimeProvider`](https://learn.microsoft.com/dotnet/api/system.timeprovider) across the stack so that every place that creates timers, runs duration math, or schedules periodic work is mockable, deterministic in tests, and immune to wall-clock changes. The migration touches Core, Core.Types, Server, Client, PubSub, Configuration, Gds (server + client), Lds, and the Quickstarts/Console reference apps. Per-component changes follow the same pattern: 1. Add a `private readonly TimeProvider m_timeProvider;` field. 2. Accept a nullable `TimeProvider? timeProvider = null` as the **last** constructor parameter (new overload; legacy constructor stays for binary compatibility and delegates with `timeProvider: null`). 3. Initialise the field with `m_timeProvider = timeProvider ??= TimeProvider.System;`. 4. Replace `new Timer(...)`, `Stopwatch`, `HiResClock.*`, `DateTime.UtcNow`-based duration math, and `Task.Delay(delay, ct)` in timing loops with the matching `m_timeProvider` API (`CreateTimer`, `GetTimestamp` / `GetElapsedTime`, `GetUtcNow().UtcDateTime`, `Task.Delay(delay, m_timeProvider, ct)`). 5. Wire the provider through DI: `AddOpcUaServerBuilder` / `AddOpcUaClientBuilder` register `TimeProvider.System` via `TryAddSingleton<TimeProvider>()` so tests can swap in `Microsoft.Extensions.Time.Testing.FakeTimeProvider`. `HiResClock` itself is kept (so the `ApplicationConfiguration.DisableHiResClock` schema flag still round-trips), but **every public member** is now `[Obsolete]` with a single shared message pointing to the new migration-guide section. The few internal call sites that still need to set/read `HiResClock` are wrapped with `#pragma warning disable CS0618`. ### Highlights per component - **Core / Stack** — `Opc.Ua.Core`, `Opc.Ua.Core.Types`, channel pipeline (`UaSCBinaryClientChannel`, `ChannelToken`, `UaSCBinaryChannel`), `ApplicationConfiguration`, certificate validators, `JwksIssuerKeyResolver`, `TimeProviderExtensions` polyfills (`GetTimestampMilliseconds`, `GetTickCount`, `CancellationTokenSource(timeout, timeProvider)`, `Task.Delay(delay, provider, ct)` polyfill for older TFMs). - **Server** — session lockout, subscription pipeline (publish/sample/keep-alive/lifetime), `MonitoredItemQueue`, alarm sampling rate, `IntervalRunner`, server diagnostics, configuration state machines, hosted services. - **Client** — `Session`, managed/V2 subscription engines, reconnect handlers, channel keep-alive, periodic publishing, complex-type discovery polling. - **PubSub** — `IntervalRunner` pipeline, message-receive timers, MQTT/UDP keep-alive, publishers/readers. - **Gds / Lds** — `LdsServer`, `LinqApplicationsDatabase` expiry housekeeping, GDS clients (push, pull), `DirectoryCertificateStore`, `ConnectionStateMachine`, classic subscription engine. - **Quickstarts / Console samples** — `SampleNodeManager`, `ReferenceNodeManager`, `AlarmNodeManager`, `BoilerState`, `MemoryBufferState`, `PublishedValuesWrites`. Samples resolve via `(Server as ITimeProviderProvider)?.TimeProvider ?? TimeProvider.System` so existing ctor signatures stay binary-compatible. - **Tests** — `Opc.Ua.PubSub.Tests` adds `IntervalRunnerTests`; existing `SubscriptionTests`, `UaPublisherTests`, and `TestDataSystem` move off `HiResClock`. Added `Microsoft.Extensions.TimeProvider.Testing` package for `FakeTimeProvider` (project-load warning surfaces on net48/net472 only — runtime is .NET 8+). ### Migration guide New section [**Time and Timer abstraction (`TimeProvider`)**](Docs/MigrationGuide.md#time-and-timer-abstraction-timeprovider) documents: - The canonical replacement table (`HiResClock.UtcNow` → `timeProvider.GetUtcNow().UtcDateTime`, `HiResClock.TickCount64` → `timeProvider.GetTimestamp()`, `new Timer` → `timeProvider.CreateTimer`, `Task.Delay(delay, ct)` → `Task.Delay(delay, timeProvider, ct)`, …). - The `TimeProvider? timeProvider = null` + `??=` constructor pattern. - DI wiring and how to swap in `FakeTimeProvider` in tests. - Before/after snippets for `HiResClock.TickCount64`, `HiResClock.UtcNow`, and `new Timer(...)`. ## Related Issues - Fixes #2458 ## Checklist - [x] I have signed the [CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf) and read the [CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md) doc. - [x] I have added tests that prove my fix is effective or that my feature works and increased code coverage. - [x] I have added all necessary documentation. - [x] I have verified that my changes do not introduce (new) build or analyzer warnings. - [ ] I ran **all** tests locally using the **UA.slnx** solution against at least .net **framework** and .net **10**, and all passed. - [ ] I fixed **all** failing and flaky tests in the CI pipelines and **all** CodeQL warnings. - [ ] I have addressed **all** PR feedback received.
702 lines
23 KiB
C#
702 lines
23 KiB
C#
/* ========================================================================
|
|
* 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.Threading;
|
|
using Microsoft.Extensions.Logging;
|
|
using Opc.Ua;
|
|
using Opc.Ua.Server;
|
|
|
|
namespace MemoryBuffer
|
|
{
|
|
#pragma warning disable CA1001 // Using timers that are disposed in OnAfterDelete
|
|
public partial class MemoryBufferState
|
|
{
|
|
#pragma warning restore CA1001 // Using timers that are disposed in OnAfterDelete
|
|
/// <summary>
|
|
/// Initializes the buffer from the configuration.
|
|
/// </summary>
|
|
public MemoryBufferState(
|
|
ISystemContext context,
|
|
MemoryBufferInstance? configuration)
|
|
: base(null)
|
|
{
|
|
m_logger = context.Telemetry.CreateLogger<MemoryBufferState>();
|
|
|
|
Initialize(context);
|
|
|
|
string dataType = "UInt32";
|
|
string name = dataType;
|
|
int count = 10;
|
|
|
|
if (configuration != null)
|
|
{
|
|
count = configuration.TagCount;
|
|
|
|
if (!string.IsNullOrEmpty(configuration.DataType))
|
|
{
|
|
dataType = configuration.DataType;
|
|
}
|
|
|
|
if (!string.IsNullOrEmpty(configuration.Name))
|
|
{
|
|
name = dataType;
|
|
}
|
|
}
|
|
|
|
SymbolicName = name;
|
|
|
|
BuiltInType elementType = dataType != "Double" ? BuiltInType.UInt32 : BuiltInType.Double;
|
|
|
|
CreateBuffer(elementType, count);
|
|
}
|
|
|
|
/// <summary>
|
|
/// The server that the buffer belongs to.
|
|
/// </summary>
|
|
public IServerInternal Server { get; private set; } = null!;
|
|
|
|
/// <summary>
|
|
/// The node manager that the buffer belongs to.
|
|
/// </summary>
|
|
public INodeManager NodeManager { get; private set; } = null!;
|
|
|
|
/// <summary>
|
|
/// The built-in type for the values stored in the buffer.
|
|
/// </summary>
|
|
public BuiltInType ElementType { get; private set; }
|
|
|
|
/// <summary>
|
|
/// The size of each element in the buffer.
|
|
/// </summary>
|
|
public uint ElementSize => (uint)m_elementSize;
|
|
|
|
/// <summary>
|
|
/// The rate at which the buffer is scanned.
|
|
/// </summary>
|
|
public int MaximumScanRate { get; private set; }
|
|
|
|
/// <inheritdoc/>
|
|
protected override void OnAfterDelete(ISystemContext context)
|
|
{
|
|
base.OnAfterDelete(context);
|
|
m_scanTimer?.Dispose();
|
|
m_scanTimer = null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes the buffer with enough space to hold the specified number of elements.
|
|
/// </summary>
|
|
/// <param name="elementName">The type of element.</param>
|
|
/// <param name="noOfElements">The number of elements.</param>
|
|
public void CreateBuffer(string elementName, int noOfElements)
|
|
{
|
|
if (string.IsNullOrEmpty(elementName))
|
|
{
|
|
elementName = "UInt32";
|
|
}
|
|
|
|
BuiltInType elementType = elementName != "Double" ? BuiltInType.UInt32 : BuiltInType.Double;
|
|
|
|
CreateBuffer(elementType, noOfElements);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes the buffer with enough space to hold the specified number of elements.
|
|
/// </summary>
|
|
/// <param name="elementType">The type of element.</param>
|
|
/// <param name="noOfElements">The number of elements.</param>
|
|
/// <exception cref="ServiceResultException"></exception>
|
|
public void CreateBuffer(BuiltInType elementType, int noOfElements)
|
|
{
|
|
lock (m_dataLock)
|
|
{
|
|
ElementType = elementType;
|
|
switch (ElementType)
|
|
{
|
|
case BuiltInType.UInt32:
|
|
m_elementSize = 4;
|
|
break;
|
|
case BuiltInType.Double:
|
|
m_elementSize = 8;
|
|
break;
|
|
case >= BuiltInType.Null and <= BuiltInType.Enumeration:
|
|
m_elementSize = 1;
|
|
break;
|
|
default:
|
|
throw ServiceResultException.Unexpected($"Unexpected BuiltInType {ElementType}");
|
|
}
|
|
|
|
m_lastScanTime = DateTimeUtc.Now;
|
|
MaximumScanRate = 1000;
|
|
|
|
m_buffer = new byte[m_elementSize * noOfElements];
|
|
SizeInBytes!.Value = (uint)m_buffer.Length;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates an object which can browse the tags in the buffer.
|
|
/// </summary>
|
|
public override INodeBrowser CreateBrowser(
|
|
ISystemContext context,
|
|
ViewDescription? view,
|
|
NodeId referenceType,
|
|
bool includeSubtypes,
|
|
BrowseDirection browseDirection,
|
|
QualifiedName browseName,
|
|
IEnumerable<IReference>? additionalReferences,
|
|
bool internalOnly)
|
|
{
|
|
NodeBrowser browser = new MemoryBufferBrowser(
|
|
context,
|
|
view!,
|
|
referenceType,
|
|
includeSubtypes,
|
|
browseDirection,
|
|
browseName,
|
|
additionalReferences!,
|
|
internalOnly,
|
|
this);
|
|
|
|
PopulateBrowser(context, browser);
|
|
|
|
return browser;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles the read operation for an invidual tag.
|
|
/// </summary>
|
|
public ServiceResult ReadTagValue(
|
|
ISystemContext context,
|
|
NodeState node,
|
|
NumericRange indexRange,
|
|
QualifiedName dataEncoding,
|
|
ref Variant value,
|
|
ref StatusCode statusCode,
|
|
ref DateTimeUtc timestamp)
|
|
{
|
|
if (node is not MemoryTagState tag)
|
|
{
|
|
return StatusCodes.BadNodeIdUnknown;
|
|
}
|
|
|
|
if (!indexRange.IsNull)
|
|
{
|
|
return StatusCodes.BadIndexRangeInvalid;
|
|
}
|
|
|
|
if (!dataEncoding.IsNull)
|
|
{
|
|
return StatusCodes.BadDataEncodingUnsupported;
|
|
}
|
|
|
|
int offset = (int)tag.Offset;
|
|
|
|
lock (m_dataLock)
|
|
{
|
|
if (offset < 0 || offset >= m_buffer.Length)
|
|
{
|
|
return StatusCodes.BadNodeIdUnknown;
|
|
}
|
|
|
|
if (m_buffer == null)
|
|
{
|
|
return StatusCodes.BadOutOfService;
|
|
}
|
|
|
|
value = GetValueAtOffset(offset);
|
|
}
|
|
|
|
statusCode = StatusCodes.Good;
|
|
timestamp = m_lastScanTime;
|
|
|
|
return ServiceResult.Good;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles a write operation for an individual tag.
|
|
/// </summary>
|
|
/// <exception cref="ServiceResultException"></exception>
|
|
public ServiceResult WriteTagValue(
|
|
ISystemContext context,
|
|
NodeState node,
|
|
NumericRange indexRange,
|
|
QualifiedName dataEncoding,
|
|
ref Variant value,
|
|
ref StatusCode statusCode,
|
|
ref DateTimeUtc timestamp)
|
|
{
|
|
if (node is not MemoryTagState tag)
|
|
{
|
|
return StatusCodes.BadNodeIdUnknown;
|
|
}
|
|
|
|
if (!indexRange.IsNull)
|
|
{
|
|
return StatusCodes.BadIndexRangeInvalid;
|
|
}
|
|
|
|
if (!dataEncoding.IsNull)
|
|
{
|
|
return StatusCodes.BadDataEncodingUnsupported;
|
|
}
|
|
|
|
if (statusCode != StatusCodes.Good)
|
|
{
|
|
return StatusCodes.BadWriteNotSupported;
|
|
}
|
|
|
|
if (timestamp != DateTimeUtc.MinValue)
|
|
{
|
|
return StatusCodes.BadWriteNotSupported;
|
|
}
|
|
|
|
bool changed = false;
|
|
int offset = (int)tag.Offset;
|
|
|
|
lock (m_dataLock)
|
|
{
|
|
if (offset < 0 || offset >= m_buffer.Length)
|
|
{
|
|
return StatusCodes.BadNodeIdUnknown;
|
|
}
|
|
|
|
if (m_buffer == null)
|
|
{
|
|
return StatusCodes.BadOutOfService;
|
|
}
|
|
|
|
byte[] bytes;
|
|
|
|
switch (ElementType)
|
|
{
|
|
case BuiltInType.UInt32:
|
|
{
|
|
if (!value.TryGetValue(out uint valueToWrite))
|
|
{
|
|
return StatusCodes.BadTypeMismatch;
|
|
}
|
|
|
|
bytes = BitConverter.GetBytes(valueToWrite);
|
|
break;
|
|
}
|
|
case BuiltInType.Double:
|
|
{
|
|
if (!value.TryGetValue(out double valueToWrite))
|
|
{
|
|
return StatusCodes.BadTypeMismatch;
|
|
}
|
|
|
|
bytes = BitConverter.GetBytes(valueToWrite);
|
|
break;
|
|
}
|
|
case >= BuiltInType.Null and <= BuiltInType.Enumeration:
|
|
return StatusCodes.BadNodeIdUnknown;
|
|
default:
|
|
throw ServiceResultException.Unexpected($"Unexpected BuiltInType {ElementType}");
|
|
}
|
|
|
|
for (int ii = 0; ii < bytes.Length; ii++)
|
|
{
|
|
if (!changed && m_buffer[offset + ii] != bytes[ii])
|
|
{
|
|
changed = true;
|
|
}
|
|
|
|
m_buffer[offset + ii] = bytes[ii];
|
|
}
|
|
}
|
|
|
|
if (changed)
|
|
{
|
|
OnBufferChanged(offset);
|
|
}
|
|
|
|
return ServiceResult.Good;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the value at the specified offset.
|
|
/// </summary>
|
|
/// <exception cref="ServiceResultException"></exception>
|
|
public Variant GetValueAtOffset(int offset)
|
|
{
|
|
lock (m_dataLock)
|
|
{
|
|
if (offset < 0 || offset >= m_buffer.Length)
|
|
{
|
|
return Variant.Null;
|
|
}
|
|
|
|
if (m_buffer == null)
|
|
{
|
|
return Variant.Null;
|
|
}
|
|
|
|
switch (ElementType)
|
|
{
|
|
case BuiltInType.UInt32:
|
|
return new Variant(BitConverter.ToUInt32(m_buffer, offset));
|
|
case BuiltInType.Double:
|
|
return new Variant(BitConverter.ToDouble(m_buffer, offset));
|
|
case >= BuiltInType.Null and <= BuiltInType.Enumeration:
|
|
return Variant.Null;
|
|
default:
|
|
throw ServiceResultException.Unexpected($"Bad element type {ElementType}");
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes the instance with the context for the node being monitored.
|
|
/// </summary>
|
|
public void InitializeMonitoring(IServerInternal server, INodeManager nodeManager)
|
|
{
|
|
lock (m_dataLock)
|
|
{
|
|
Server = server;
|
|
NodeManager = nodeManager;
|
|
m_nonValueMonitoredItems = [];
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a new data change monitored item.
|
|
/// </summary>
|
|
public MemoryBufferMonitoredItem CreateDataChangeItem(
|
|
ServerSystemContext context,
|
|
MemoryTagState tag,
|
|
uint subscriptionId,
|
|
uint monitoredItemId,
|
|
ReadValueId itemToMonitor,
|
|
DiagnosticsMasks diagnosticsMasks,
|
|
TimestampsToReturn timestampsToReturn,
|
|
MonitoringMode monitoringMode,
|
|
uint clientHandle,
|
|
double samplingInterval,
|
|
bool createDurable)
|
|
/*
|
|
ISystemContext context,
|
|
MemoryTagState tag,
|
|
uint monitoredItemId,
|
|
uint attributeId,
|
|
DiagnosticsMasks diagnosticsMasks,
|
|
TimestampsToReturn timestampsToReturn,
|
|
MonitoringMode monitoringMode,
|
|
uint clientHandle,
|
|
double samplingInterval)*/
|
|
{
|
|
lock (m_dataLock)
|
|
{
|
|
var monitoredItem = new MemoryBufferMonitoredItem(
|
|
Server,
|
|
NodeManager.ToAsyncNodeManager(),
|
|
this,
|
|
tag.Offset,
|
|
0,
|
|
monitoredItemId,
|
|
itemToMonitor,
|
|
diagnosticsMasks,
|
|
timestampsToReturn,
|
|
monitoringMode,
|
|
clientHandle,
|
|
null,
|
|
null,
|
|
null,
|
|
samplingInterval,
|
|
0,
|
|
false,
|
|
0,
|
|
createDurable);
|
|
|
|
/*
|
|
MemoryBufferMonitoredItem monitoredItem = new MemoryBufferMonitoredItem(
|
|
this,
|
|
monitoredItemId,
|
|
tag.Offset,
|
|
attributeId,
|
|
diagnosticsMasks,
|
|
timestampsToReturn,
|
|
monitoringMode,
|
|
clientHandle,
|
|
samplingInterval);
|
|
*/
|
|
|
|
AddMonitoredItemInternal(monitoredItem, tag);
|
|
|
|
return monitoredItem;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Restores data change monitored item after a server restart.
|
|
/// </summary>
|
|
public MemoryBufferMonitoredItem RestoreDataChangeItem(
|
|
ServerSystemContext context,
|
|
MemoryTagState tag,
|
|
IStoredMonitoredItem storedMonitoredItem)
|
|
{
|
|
lock (m_dataLock)
|
|
{
|
|
var monitoredItem = new MemoryBufferMonitoredItem(
|
|
Server,
|
|
NodeManager.ToAsyncNodeManager(),
|
|
this,
|
|
tag.Offset,
|
|
storedMonitoredItem);
|
|
|
|
AddMonitoredItemInternal(monitoredItem, tag);
|
|
|
|
return monitoredItem;
|
|
}
|
|
}
|
|
|
|
private void AddMonitoredItemInternal(
|
|
MemoryBufferMonitoredItem monitoredItem,
|
|
MemoryTagState tag)
|
|
{
|
|
if (monitoredItem.AttributeId != Attributes.Value)
|
|
{
|
|
m_nonValueMonitoredItems!.Add(monitoredItem.Id, monitoredItem);
|
|
return;
|
|
}
|
|
|
|
int elementCount = (int)(SizeInBytes!.Value / ElementSize);
|
|
|
|
if (m_monitoringTable == null)
|
|
{
|
|
m_monitoringTable = new MemoryBufferMonitoredItem[elementCount][];
|
|
TimeProvider timeProvider = (Server as ITimeProviderProvider)?.TimeProvider
|
|
?? TimeProvider.System;
|
|
m_scanTimer?.Dispose();
|
|
m_scanTimer = timeProvider.CreateTimer(
|
|
DoScan,
|
|
null,
|
|
TimeSpan.FromMilliseconds(100),
|
|
TimeSpan.FromMilliseconds(100));
|
|
}
|
|
|
|
int elementOffet = (int)(tag.Offset / ElementSize);
|
|
|
|
MemoryBufferMonitoredItem[]? monitoredItems = m_monitoringTable![elementOffet];
|
|
|
|
if (monitoredItems == null)
|
|
{
|
|
monitoredItems = new MemoryBufferMonitoredItem[1];
|
|
}
|
|
else
|
|
{
|
|
monitoredItems = new MemoryBufferMonitoredItem[monitoredItems.Length + 1];
|
|
m_monitoringTable[elementOffet]!.CopyTo(monitoredItems, 0);
|
|
}
|
|
|
|
monitoredItems[^1] = monitoredItem;
|
|
m_monitoringTable[elementOffet] = monitoredItems;
|
|
m_itemCount++;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Scans the buffer and updates every other element.
|
|
/// </summary>
|
|
private void DoScan(object? state)
|
|
{
|
|
DateTimeUtc start1 = DateTimeUtc.Now;
|
|
|
|
lock (m_dataLock)
|
|
{
|
|
for (int ii = 0; ii < m_buffer.Length; ii += m_elementSize)
|
|
{
|
|
m_buffer[ii]++;
|
|
|
|
// notify any monitored items that the value has changed.
|
|
OnBufferChanged(ii);
|
|
}
|
|
|
|
m_lastScanTime = DateTimeUtc.Now;
|
|
}
|
|
|
|
DateTimeUtc end1 = DateTimeUtc.Now;
|
|
|
|
double delta1 = (end1 - start1).TotalMilliseconds;
|
|
|
|
if (delta1 > 100)
|
|
{
|
|
m_logger.LogWarning("{StateName} SAMPLING DELAY ({Delta}ms)", nameof(MemoryBufferState), delta1);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Deletes the monitored item.
|
|
/// </summary>
|
|
public void DeleteItem(MemoryBufferMonitoredItem monitoredItem)
|
|
{
|
|
lock (m_dataLock)
|
|
{
|
|
if (monitoredItem.AttributeId != Attributes.Value)
|
|
{
|
|
m_nonValueMonitoredItems!.Remove(monitoredItem.Id);
|
|
return;
|
|
}
|
|
|
|
if (m_monitoringTable != null)
|
|
{
|
|
int elementOffet = (int)(monitoredItem.Offset / ElementSize);
|
|
|
|
MemoryBufferMonitoredItem[]? monitoredItems = m_monitoringTable[elementOffet];
|
|
|
|
if (monitoredItems != null)
|
|
{
|
|
int index = -1;
|
|
|
|
for (int ii = 0; ii < monitoredItems.Length; ii++)
|
|
{
|
|
if (ReferenceEquals(monitoredItems[ii], monitoredItem))
|
|
{
|
|
index = ii;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (index >= 0)
|
|
{
|
|
m_itemCount--;
|
|
|
|
if (monitoredItems.Length == 1)
|
|
{
|
|
monitoredItems = null;
|
|
}
|
|
else
|
|
{
|
|
monitoredItems = new MemoryBufferMonitoredItem[monitoredItems
|
|
.Length -
|
|
1];
|
|
|
|
Array.Copy(
|
|
m_monitoringTable[elementOffet]!,
|
|
0,
|
|
monitoredItems,
|
|
0,
|
|
index);
|
|
Array.Copy(
|
|
m_monitoringTable[elementOffet]!,
|
|
index + 1,
|
|
monitoredItems,
|
|
index,
|
|
monitoredItems.Length - index);
|
|
}
|
|
|
|
m_monitoringTable[elementOffet] = monitoredItems;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles change events raised by the node.
|
|
/// </summary>
|
|
public void OnBufferChanged(int offset)
|
|
{
|
|
lock (m_dataLock)
|
|
{
|
|
if (m_monitoringTable != null)
|
|
{
|
|
int elementOffet = (int)(offset / ElementSize);
|
|
|
|
MemoryBufferMonitoredItem[]? monitoredItems = m_monitoringTable[elementOffet];
|
|
|
|
if (monitoredItems != null)
|
|
{
|
|
var value = new DataValue(
|
|
GetValueAtOffset(offset),
|
|
StatusCodes.Good,
|
|
m_lastScanTime,
|
|
DateTimeUtc.Now);
|
|
|
|
for (int ii = 0; ii < monitoredItems.Length; ii++)
|
|
{
|
|
monitoredItems[ii].QueueValue(value, null);
|
|
m_updateCount++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void ScanTimer_Tick(object sender, EventArgs e)
|
|
{
|
|
DoScan(null);
|
|
}
|
|
|
|
private void PublishTimer_Tick(object sender, EventArgs e)
|
|
{
|
|
DateTimeUtc start1 = DateTimeUtc.Now;
|
|
|
|
lock (m_dataLock)
|
|
{
|
|
if (m_itemCount > 0 && m_updateCount < m_itemCount)
|
|
{
|
|
m_logger.LogInformation(
|
|
"{Now:HH:mm:ss.fff} MEMORYBUFFER Reported {UpdateCount}/{ItemCount} items ***.",
|
|
DateTimeUtc.Now.ToLocalTime(),
|
|
m_updateCount,
|
|
m_itemCount);
|
|
}
|
|
|
|
m_updateCount = 0;
|
|
}
|
|
|
|
DateTimeUtc end1 = DateTimeUtc.Now;
|
|
|
|
double delta1 = (end1 - start1).TotalMilliseconds;
|
|
|
|
if (delta1 > 100)
|
|
{
|
|
m_logger.LogInformation(
|
|
"{StateName} ****** PUBLISH DELAY ({Delta}ms) ******",
|
|
nameof(MemoryBufferState),
|
|
delta1);
|
|
}
|
|
}
|
|
|
|
private readonly ILogger m_logger;
|
|
private readonly Lock m_dataLock = new();
|
|
private MemoryBufferMonitoredItem[]?[]? m_monitoringTable;
|
|
private Dictionary<uint, MemoryBufferMonitoredItem>? m_nonValueMonitoredItems;
|
|
private int m_elementSize;
|
|
private DateTimeUtc m_lastScanTime;
|
|
private byte[] m_buffer = null!;
|
|
private ITimer? m_scanTimer;
|
|
private int m_updateCount;
|
|
private int m_itemCount;
|
|
}
|
|
}
|