mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-29 15:11:53 +08:00
723 lines
24 KiB
C#
723 lines
24 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;
|
|
using Quickstarts.Servers;
|
|
|
|
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.SamplingDelay(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.IsEnabled(LogLevel.Information))
|
|
{
|
|
m_logger.MemoryBufferReported(
|
|
DateTimeUtc.Now.ToLocalTime(),
|
|
m_updateCount,
|
|
m_itemCount);
|
|
}
|
|
|
|
m_updateCount = 0;
|
|
}
|
|
|
|
DateTimeUtc end1 = DateTimeUtc.Now;
|
|
|
|
double delta1 = (end1 - start1).TotalMilliseconds;
|
|
|
|
if (delta1 > 100)
|
|
{
|
|
m_logger.PublishDelay(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;
|
|
}
|
|
|
|
internal static partial class MemoryBufferStateLog
|
|
{
|
|
[LoggerMessage(
|
|
EventId = QuickstartsServersEventIds.MemoryBufferState + 0, Level = LogLevel.Warning,
|
|
Message = "{StateName} SAMPLING DELAY ({Delta}ms)")]
|
|
public static partial void SamplingDelay(this ILogger logger, string stateName, double delta);
|
|
|
|
[LoggerMessage(
|
|
EventId = QuickstartsServersEventIds.MemoryBufferState + 1, Level = LogLevel.Information,
|
|
Message = "{Now:HH:mm:ss.fff} MEMORYBUFFER Reported {UpdateCount}/{ItemCount} items ***.")]
|
|
public static partial void MemoryBufferReported(
|
|
this ILogger logger,
|
|
DateTime now,
|
|
int updateCount,
|
|
int itemCount);
|
|
|
|
[LoggerMessage(
|
|
EventId = QuickstartsServersEventIds.MemoryBufferState + 2, Level = LogLevel.Information,
|
|
Message = "{StateName} ****** PUBLISH DELAY ({Delta}ms) ******")]
|
|
public static partial void PublishDelay(this ILogger logger, string stateName, double delta);
|
|
}
|
|
|
|
}
|