PLC写入延迟排查报告.md 8.6 KB

PLC 状态写入延迟排查报告

1. 问题现象

1.1 现象描述

在固定相机检测拍照流程中,代码执行到 Management.cs 第 2553 行:

await plc.WriteNodeAsync(addressConfig.Out_FixedCameraPickStatus.Address, (Int16)1);

调用后,通过外部工具检测对应 OPC UA 节点值未发生变化,使用抓包工具也未观察到对应的 WriteRequest 数据包及时发出。

1.2 影响范围

  • 受影响节点:ns=4;s=DB_Communication|FromPC.FixedCameraPickStatus
  • 受影响流程:PlcCommandTriggerIn_FixedCameracheckExe → 固定相机检测拍照 → 状态回写
  • 每个产品周期涉及 6 个相机的采图与状态回写

2. 问题排查过程

2.1 第一轮:架构层面分析(async void + OPC UA Session)

排查方向PlcCommandTrigger 声明为 async void,被 OPC UA 订阅回调直接调用。怀疑回调线程未释放导致 Session 内部锁阻塞 WriteNodeAsync

措施:将 PlcCommandTriggerasync void 改为同步包装 + async Task

private void PlcCommandTrigger(...)
{
    _ = PlcCommandTriggerAsync(tuple);
}

结论WriteNodeAsync 本身极快(1-12ms),返回 result=True。不是写入操作慢,而是写入调用被前面的代码延迟了。


2.2 第二轮:添加分层诊断日志

排查方向:在各层添加 [PLC-TRACE] 前缀的耗时日志,覆盖:

文件 日志内容
业务层 Management.cs PLC 触发入队、相机触发开始、光源耗时、GrabImageEx 开始/结束、图像处理耗时、status write 前后
OPC 封装层 OPCuaClientPLC.cs ReadNodesAsync 慢日志、WriteNodeAsync 开始/结束、轮询周期日志、回调处理慢日志
采图核心 RemoteCommandService.cs SetExposureTime/SetGain 累计耗时、Grab 前状态
OPT 相机 SDK OptCamera.cs SetExposureTime/SetGain SDK 耗时、Grab 各子步骤(clearMs/triggerMs/grabMs/convertMs/freeMs)

关键日志数据(第一轮测试):

相机 enqueue→写入延迟 status write 耗时
FC-1 491ms 1ms
FC-2 1598ms 5ms
FC-3 1062ms 2ms
FC-4 1578ms 5ms
FC-5 747ms 3ms

结论WriteNodeAsync 本身极快(1-12ms),全部返回 result=True。阻塞发生在写入之前的代码段。


2.3 第三轮:OPT 相机层精确定位

排查方向:锁定 GrabImageExExecuteGrabImageAsync 内部耗时分布。

关键日志

[PLC-TRACE] GrabImageEx ExecuteGrabImageAsync start
[PLC-TRACE] OptCamera SetExposureTime done, elapsedMs=3        ← SDK 本身 3ms
[PLC-TRACE] OptCamera SetGain done, elapsedMs=10               ← SDK 本身 10ms
[PLC-TRACE] before Grab: setExpMs=3, setGainMs=1175            ← !! 1175ms !!
[PLC-TRACE] OptCamera Grab done, totalMs=296                   ← Grab 本身 296ms
指标 FC-2 FC-3 FC-4
SetExposureTime SDK 3ms 5ms 5ms
SetGain SDK 10ms ≤5ms
SetExp→SetGain 间隙 1162ms 694ms 1247ms
Grab 耗时 296ms 222ms 227ms
Grab 占比 20% 24% 15%

结论70-85% 的时间消耗在 SetExposureTimeSetGain 之间,而不是在 Grab 本身。两者之间唯一的非平凡代码是 SendTaskMessage 调用。


2.4 第四轮:SendTaskMessage 验证

排查方向:注释掉 SetExposureTimeSetGain 成功后的 SendTaskMessage 调用。

对比结果

指标 FC-2 注释前 FC-2 注释后 改善
setGainMs 1175ms 5ms -99.6%
总耗时 1473ms 239ms -84%

确认SendTaskMessage 就是瓶颈根因。


2.5 第五轮:根因深层分析

排查方向:检查 SendTaskMessage 的实现。

发现项目中存在 4 份独立的 SendTaskMessage 副本

位置 方式 影响
Management.cs BeginInvoke(异步) 之前已修复
RemoteCommandService.cs Invoke(同步阻塞) 主犯!
MainWindowViewModel.cs 直接 Publish UI 线程调用,无影响
AdvancedProcedureViewModel.cs Invoke UI 线程调用,无影响

RemoteCommandService.csSendTaskMessage 使用了 Dispatcher.Invoke(同步等待 UI 线程)

private void SendTaskMessage(string msg, MessageLevel level)
{
    App.Current.Dispatcher.Invoke(() =>    // ← 同步阻塞!每次等待 700-1462ms
    {
        _eventAggregator.GetEvent<TaskMessageNotification>().Publish(...);
    });
}

阻塞成因

  1. Dispatcher.Invoke 需要 UI 线程空闲才能执行委托
  2. UI 线程忙于 Cognex VisionPro 图像渲染、ObservableCollection 数据绑定更新
  3. 每个产品周期有 50+ 次 SendTaskMessage 调用
  4. 累积阻塞 = 700-1462ms/相机

3. 问题排查结论

3.1 根本原因

RemoteCommandService.cs 中的 SendTaskMessage 方法使用了 Dispatcher.Invoke(同步阻塞),在采图流程的高频调用下,每次都需要等待 UI 线程空闲,导致累计阻塞 700-1462ms。

3.2 时间分布(修复后)

环节 耗时 占比
SetExposureTime (SDK) 2-5ms 1%
SetGain (SDK) 2-6ms 1%
Grab (硬件采集) 225-256ms 96%
status write 1-3ms 1%
总计 ~235ms

Grab 的 ~185ms 硬件采集时间是物理极限,无法进一步优化。


4. 问题解决方法

4.1 全局修复方案

SendTaskMessage 改为 无锁队列 + 定时器批量推送,所有调用方零改动。

Management.cs

// 新增字段
private readonly ConcurrentQueue<MessageStruct> _messageQueue = new ConcurrentQueue<MessageStruct>();
private Timer _messageFlushTimer;

// 构造函数中初始化(200ms 批量推送)
_messageFlushTimer = new Timer(_ => FlushMessageQueue(), null, 200, 200);

// 改造后的 SendTaskMessage(~0ms)
private void SendTaskMessage(string msg, MessageLevel level)
{
    _messageQueue.Enqueue(new Models.MessageStruct() { Message = msg, level = level });
}

private void FlushMessageQueue()
{
    if (_messageQueue.IsEmpty) return;
    App.Current.Dispatcher.BeginInvoke(new Action(() =>
    {
        while (_messageQueue.TryDequeue(out var item))
            _eventAggregator.GetEvent<TaskMessageNotification>().Publish(item);
    }));
}

RemoteCommandService.cs(同样方式):

private static readonly ConcurrentQueue<MessageStruct> _msgQueue = new ConcurrentQueue<MessageStruct>();
private static System.Threading.Timer _msgFlushTimer;
private static IEventAggregator _msgEventAggregator;
private static bool _msgInitialized;

private void SendTaskMessage(string msg, MessageLevel level)
{
    if (!_msgInitialized)
    {
        _msgEventAggregator = _eventAggregator;
        _msgFlushTimer = new System.Threading.Timer(_ => FlushMessageQueue(), null, 200, 200);
        _msgInitialized = true;
    }
    _msgQueue.Enqueue(new Models.MessageStruct() { Message = msg, level = level });
}

4.2 原理

改造前:每次 SendTaskMessage → Dispatcher.Invoke → 等待 UI 线程 → 700-1462ms ✗

改造后:每次 SendTaskMessage → ConcurrentQueue.Enqueue → 0ms ✓
        Timer 200ms → BeginInvoke 一次 → 批量消费队列 → UI 展示 ✓

4.3 涉及文件

文件 改动
Management.cs SendTaskMessage + 新增 FlushMessageQueue
RemoteCommandService.cs SendTaskMessage + 新增 FlushMessageQueue
OptCamera.cs 添加 Grab/SetExposure/SetGain 诊断日志(保留用于监控)
OPCuaClientPLC.cs 添加读写/轮询诊断日志(保留用于监控)

4.4 修复效果

指标 修复前 修复后 改善
SetExp→SetGain 间隙 700-1462ms 0-30ms -98%
单相机总耗时 927-1494ms 231-261ms -84%
status write 延迟 ~1500ms ~235ms -84%
调用方改动 0 处

5. 附加优化

在排查过程中同步修复了以下问题:

问题 修复
PlcCommandTriggerasync void 改为同步包装 + async Task,释放 OPC UA 回调线程
PollingSubscriptionLoopAsync 连续读抢占 Session 改为"读完再等待",给 WriteRequest 留出发送窗口
GrabImageExTask.Run 包装确保主流程不阻塞 保留 GrabImageEx 原始调用方式