|
|
@@ -1,8 +1,67 @@
|
|
|
# VisionPro ToolBlock 加载/运行导致 UI 阻塞排查报告
|
|
|
|
|
|
-> 日期:2026-07-12
|
|
|
-> 涉及文件:`TeamAAS-VM/Services/ProductService.cs`、`TeamAAS-VM/Core/Management.cs`(~1640 行)、`TeamAAS-VM/Services/RemoteCommandService.cs`
|
|
|
-> 问题:加载 VPP 和运行 ToolBlock 时 UI 假死,尤其多 ToolBlock 串行场景下更加明显
|
|
|
+> 初版日期:2026-07-12(首次调查)
|
|
|
+> 修订日期:2026-07-12(日志复查 + 第二轮根因定位 + 修复验证)
|
|
|
+> 涉及文件:`TeamAAS-VM/Views/Home/ShowVisionRender.xaml.cs`、`TeamAAS-VM/Services/RemoteCommandService.cs`、`TeamAAS-VM/Helpers/VisionProHelper.cs`、`TeamAAS-VM/Core/Management.cs`、`TeamAAS-VM/Services/ProductService.cs`
|
|
|
+> 问题:多任务执行时只有相机图像在更新,软件按钮等 UI 操作假死无响应
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 修订记录
|
|
|
+
|
|
|
+| 日期 | 版本 | 内容 |
|
|
|
+|------|------|------|
|
|
|
+| 2026-07-12 | v1 | 初版:定位 ToolBlock.Run() COM 封送、VPP 同步加载、串行执行、countdown.Wait 等 4 个根因 |
|
|
|
+| 2026-07-12 | v2 | 日志复查:确认 P0(STA 线程隔离)已落地并验证通过;新增根因 5(RenderUpdate Dispatcher 洪泛)及对应修复 |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 0. 日志复查结果(2026-07-12 第二轮)
|
|
|
+
|
|
|
+### 0.1 数据来源
|
|
|
+- `C:\Users\teamk\Desktop\新建文件夹\Log\LogInfo\2026-07\2026-07-12.txt`
|
|
|
+- `C:\Users\teamk\Desktop\新建文件夹\Log\LogError\2026-07\2026-07-12.htm`
|
|
|
+- `C:\Users\teamk\Desktop\新建文件夹\Log\LogWarn\2026-07\2026-07-12.txt`
|
|
|
+
|
|
|
+### 0.2 关键发现
|
|
|
+
|
|
|
+**✅ 根因 1(ToolBlock.Run COM 封送)已在当前代码中修复。**
|
|
|
+
|
|
|
+日志中可见所有 ToolBlock 执行均通过 `VisionProHelper.RunToolBlockAsync` 投递到独立 STA 后台线程:
|
|
|
+
|
|
|
+```
|
|
|
+[VP-STA] RunToolBlockAsync ENTER, callingThread=10, callingApartment=MTA, isUIThread=False, newThread=VP-Run-xxx
|
|
|
+[VP-STA] STA-Run START, threadId=22, apartment=STA
|
|
|
+[VP-STA] STA-Run DONE, threadId=22, elapsedMs=512
|
|
|
+[VP-TRACE] ExecutePhotoAsync AFTER RunToolBlockAsync, proc=C1, thread=3, result=Accept
|
|
|
+```
|
|
|
+
|
|
|
+关键证据:`callingApartment=MTA`、`isUIThread=False`,新线程 `apartment=STA` 且 ID 不是 1(UI 线程),确认 ToolBlock.Run() 已不再占用主 UI 线程。
|
|
|
+
|
|
|
+**❌ UI heartbeat 在假死期间仍然持续,但 UI 不可交互。**
|
|
|
+
|
|
|
+```
|
|
|
+[UI-HEARTBEAT] UI thread alive, queueDepth=0, time=17:23:10.xxx
|
|
|
+[UI-HEARTBEAT] UI thread alive, queueDepth=0, time=17:23:11.xxx
|
|
|
+...(从未中断)
|
|
|
+```
|
|
|
+
|
|
|
+这说明**不是线程死锁或同步阻塞**,而是 UI 线程的消息泵虽然运行,但 Dispatcher 工作队列被渲染任务持续占满,用户输入(鼠标点击、按键)长时间得不到调度机会。
|
|
|
+
|
|
|
+**❌ 线程 [1] 仍在处理视觉结果和保存。**
|
|
|
+
|
|
|
+同一时段日志中 [1] 线程持续输出:
|
|
|
+```
|
|
|
+视觉流程[C1]执行结束!耗时:477.6ms
|
|
|
+视觉流程[C2]执行结束!耗时:247.8ms
|
|
|
+视觉流程[C3]执行结束!耗时:394.6ms
|
|
|
+[SAVE-IMAGE] 开始保存流程...
|
|
|
+当前产品SN不存在,无法保存图片!
|
|
|
+```
|
|
|
+
|
|
|
+这些操作通过 `RenderUpdateNotification` 事件 → 订阅端 → `Dispatcher.BeginInvoke` → Cognex 显示控件更新 + 截图保存,全部回到了 UI 线程。
|
|
|
+
|
|
|
+**结论**:第一轮修复(STA 线程隔离)生效后,新的瓶颈变为 **RenderUpdateNotification 订阅端在 UI 线程上的渲染/保存链路被高频触发导致饥饿**。
|
|
|
|
|
|
---
|
|
|
|
|
|
@@ -22,9 +81,11 @@
|
|
|
|
|
|
## 2. 根因分析
|
|
|
|
|
|
-### 🔴 根因 1(最核心):`ToolBlock.Run()` 是同步阻塞调用,且 Cognex VisionPro 是 COM/STA 组件
|
|
|
+### 🔴 根因 1:`ToolBlock.Run()` 是同步阻塞调用,且 Cognex VisionPro 是 COM/STA 组件
|
|
|
+
|
|
|
+> **状态:✅ 已修复(`VisionProHelper.RunToolBlockAsync`),日志验证通过。**
|
|
|
|
|
|
-**调用链**(`RemoteCommandService.cs` + `Management.cs`):
|
|
|
+**调用链**(初版,修复前):
|
|
|
|
|
|
```
|
|
|
PlcCommandTriggerAsync (async Task, 运行于线程池线程)
|
|
|
@@ -53,10 +114,16 @@ PlcCommandTriggerAsync (async Task, 运行于线程池线程)
|
|
|
|
|
|
**所有这 6 处 `ToolBlock.Run()` 均是同步阻塞调用,无一使用后台线程或异步封装。**
|
|
|
|
|
|
+#### 修复状态
|
|
|
+
|
|
|
+当前代码中所有 6 处调用已全部替换为 `await VisionProHelper.RunToolBlockAsync(procedure.ToolBlock)`,实现在 `TeamAAS-VM/Helpers/VisionProHelper.cs`。该方法为每次调用创建独立 STA 后台线程执行 `ToolBlock.Run()`,日志(见 §0.2)证实 `callingApartment=MTA`、STA 线程 ID 非 1、UI heartbeat 不受 ToolBlock 执行时段影响。
|
|
|
+
|
|
|
---
|
|
|
|
|
|
### 🔴 根因 2:VPP 文件加载也同步阻塞
|
|
|
|
|
|
+> **状态:⚠️ 部分修复。`VisionProHelper.LoadVisionProObjectAsync` 已提供异步加载能力,但 `ProductService.Initialize()` 等同步调用路径尚未全部迁移。**
|
|
|
+
|
|
|
**在 `ProductService.cs` 中**:
|
|
|
|
|
|
| 行号 | 方法 | 阻塞操作 |
|
|
|
@@ -72,6 +139,8 @@ PlcCommandTriggerAsync (async Task, 运行于线程池线程)
|
|
|
|
|
|
### 🟡 根因 3:多 ToolBlock 串行执行,非真正并发
|
|
|
|
|
|
+> **状态:⚠️ 待评估。当前仍为串行 `foreach` + `await`,但 STA 线程隔离后阻塞影响已大幅降低。**
|
|
|
+
|
|
|
**在 `Management.cs` 第 2520~2640 行区域**:
|
|
|
|
|
|
**分支 A**(`sysConfig.PhotosUse == true`,即"逐点采图+逐点处理"模式,第 2547 行):
|
|
|
@@ -98,6 +167,8 @@ foreach (var item in ImageTaskResults)
|
|
|
|
|
|
### 🟡 根因 4:`countdown.Wait()` 同步阻塞线程池线程
|
|
|
|
|
|
+> **状态:⚠️ 待修复。日志中可见 `countdown wait start, expected=6, current=1` 记录。**
|
|
|
+
|
|
|
**`Management.cs` 第 ~2650 行**:
|
|
|
```csharp
|
|
|
bool countdownCompleted = countdown.Wait(TimeSpan.FromSeconds(5));
|
|
|
@@ -106,8 +177,88 @@ bool countdownCompleted = countdown.Wait(TimeSpan.FromSeconds(5));
|
|
|
|
|
|
---
|
|
|
|
|
|
+### 🔴 根因 5(第二轮新增):`RenderUpdateNotification` 渲染/保存链路在 UI 线程上形成 Dispatcher 洪泛
|
|
|
+
|
|
|
+> **状态:✅ 已修复。将每帧独立 Dispatcher 投递改为后台入队 + Background 优先级批处理,修复在 `ShowVisionRender.xaml.cs`。**
|
|
|
+
|
|
|
+#### 5.1 问题机理
|
|
|
+
|
|
|
+当 ToolBlock 多任务并发运行且结果密集返回时,每个视觉流程完成都会触发:
|
|
|
+
|
|
|
+```
|
|
|
+RemoteCommandService.ExecutePhotoAsync()
|
|
|
+ → _eventAggregator.GetEvent<RenderUpdateNotification>().Publish(showRender) // 每条结果 1~2 次 Publish
|
|
|
+ → ShowVisionRender.UpdateRenderModuleSource(render) // 订阅端(后台线程)
|
|
|
+ → imageCopy = render.Image.CopyBase(CopyPixels) // 深拷贝图像(后台线程,开销小)
|
|
|
+ → gridRender.Dispatcher.BeginInvoke(() => { ... }) // ⚠️ 每帧一次独立 UI 投递
|
|
|
+ → recordDisplay.Image = imageCopy; // Cognex 显示控件设图(UI 线程,重)
|
|
|
+ → recordDisplay.StaticGraphics.AddList(graphic, ""); // 叠加图形(UI 线程)
|
|
|
+ → recordDisplay.Record = record; // 叠加 Record(UI 线程)
|
|
|
+ → RequestSaveImage() → disp.CreateContentBitmap() // WinForms 截图(UI 线程,极重)
|
|
|
+ → SaveImageAsync() // 磁盘 I/O(已切到后台线程,✅ 不占 UI)
|
|
|
+```
|
|
|
+
|
|
|
+**核心矛盾**:每个视觉结果都独立 `Dispatcher.BeginInvoke`(默认优先级 = Normal),当 6 个相机依次在 ~2 秒内完成并投递 6~12 个 UI 更新时,UI 线程的 Dispatcher 队列被这些渲染任务塞满。用户鼠标点击的 Input 优先级消息排在这些 Normal 优先级工作项后面,长时间得不到处理 → 表现为"画面在刷新但点不动按钮"。
|
|
|
+
|
|
|
+#### 5.2 日志证据
|
|
|
+
|
|
|
+`[1]` 线程(UI 线程)在 17:23:10 ~ 17:23:16 期间密集执行视觉结果处理和保存,而 `[UI-HEARTBEAT]` 在此期间仍在正常跳动(每 ~500ms),`queueDepth=0`(消息队列没有积压)——说明死锁不存在,确实是 UI Dispatcher 被占满。
|
|
|
+
|
|
|
+此外,无产品 SN 时仍在做深拷贝、截图等无效 UI 工作后才报"当前产品SN不存在,无法保存图片!",进一步浪费 UI 线程时间。
|
|
|
+
|
|
|
+#### 5.3 修复方案
|
|
|
+
|
|
|
+**修改文件**:`TeamAAS-VM/Views/Home/ShowVisionRender.xaml.cs`
|
|
|
+
|
|
|
+**策略**:将"每帧一次独立 UI 投递"改为"后台入队 + UI 线程 Background 优先级批量排空"。
|
|
|
+
|
|
|
+```csharp
|
|
|
+// 新增字段
|
|
|
+private readonly ConcurrentQueue<ShowRender> _pendingRenderUpdates = new ConcurrentQueue<ShowRender>();
|
|
|
+private int _renderDrainScheduled;
|
|
|
+
|
|
|
+// EnqueueRenderUpdate:后台线程入队(替代旧 UpdateRenderModuleSource)
|
|
|
+private void EnqueueRenderUpdate(ShowRender render)
|
|
|
+{
|
|
|
+ // 后台线程深拷贝图像
|
|
|
+ ICogImage imageCopy = render.Image?.CopyBase(CogImageCopyModeConstants.CopyPixels);
|
|
|
+ // 入队
|
|
|
+ _pendingRenderUpdates.Enqueue(new ShowRender { ..., Image = imageCopy, ... });
|
|
|
+ ScheduleRenderDrain();
|
|
|
+}
|
|
|
+
|
|
|
+// ScheduleRenderDrain:UI 线程按 Background 优先级批量排空
|
|
|
+private void ScheduleRenderDrain()
|
|
|
+{
|
|
|
+ if (Interlocked.CompareExchange(ref _renderDrainScheduled, 1, 0) != 0)
|
|
|
+ return; // 已有排空任务在运行,避免重复投递
|
|
|
+
|
|
|
+ _ = gridRender.Dispatcher.InvokeAsync(async () =>
|
|
|
+ {
|
|
|
+ while (_pendingRenderUpdates.TryDequeue(out var render))
|
|
|
+ {
|
|
|
+ ApplyRenderUpdate(render);
|
|
|
+ // 每处理 2 帧出让 UI 线程,让 Input 优先级消息有机会执行
|
|
|
+ if (processed % 2 == 0 && !_pendingRenderUpdates.IsEmpty)
|
|
|
+ await Dispatcher.Yield(DispatcherPriority.Input);
|
|
|
+ }
|
|
|
+ }, DispatcherPriority.Background); // ← 关键:Background 优先级,不抢用户输入
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**附加优化**:在 `RequestSaveImage` 入口增加无产品 SN 的提前返回(原本要走到截图阶段才检测),减少无效 UI 工作。
|
|
|
+
|
|
|
+#### 5.4 验证结果
|
|
|
+
|
|
|
+- 用户现场验证:多任务执行时 UI 按钮可正常点击交互,假死现象消失。✅
|
|
|
+- 新增诊断日志 `[UI-RENDER] Pending render queue depth=N` 可在积压时预警。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
## 3. 完整调用路径图
|
|
|
|
|
|
+### 3.1 初版路径(修复前,ToolBlock.Run 直接通过 COM 封送阻塞 UI 线程)
|
|
|
+
|
|
|
```mermaid
|
|
|
sequenceDiagram
|
|
|
participant PLC
|
|
|
@@ -133,11 +284,43 @@ sequenceDiagram
|
|
|
|
|
|
**关键点**:即使调用从线程池发起,`ToolBlock.Run()` 通过 COM 封送回到了 UI 线程执行,且是同步阻塞的。
|
|
|
|
|
|
+### 3.2 当前路径(P0 修复后:STA 隔离 + 渲染批处理)
|
|
|
+
|
|
|
+```mermaid
|
|
|
+sequenceDiagram
|
|
|
+ participant PLC
|
|
|
+ participant OPC_UA as OPC UA 回调
|
|
|
+ participant ThreadPool as 线程池
|
|
|
+ participant VP_STA as STA 后台线程 (VP-Run-xxx)
|
|
|
+ participant Remote as RemoteCommandService
|
|
|
+ participant UI_Thread as UI 线程
|
|
|
+ participant CogDisplay as CogRecordDisplay
|
|
|
+ participant Disk as 磁盘 I/O
|
|
|
+
|
|
|
+ PLC->>OPC_UA: 触发信号
|
|
|
+ OPC_UA->>ThreadPool: Task.Run → PlcCommandTriggerAsync
|
|
|
+ ThreadPool->>Remote: ExecutePhotoGetSinglePoint1()
|
|
|
+ Remote->>VP_STA: await RunToolBlockAsync(toolBlock)
|
|
|
+ Note over VP_STA: new Thread(ApartmentState.STA)<br/>toolBlock.Run() 在独立线程
|
|
|
+ VP_STA-->>Remote: 视觉结果
|
|
|
+ Remote->>Remote: Publish(RenderUpdateNotification)
|
|
|
+ Note over Remote: 后台线程订阅 EnqueueRenderUpdate<br/>深拷贝图像后入队
|
|
|
+ Remote-->>UI_Thread: Dispatcher.InvokeAsync(Background)<br/>批量排空 _pendingRenderUpdates
|
|
|
+ UI_Thread->>CogDisplay: 设图 + Graphic + Record
|
|
|
+ Note over UI_Thread: 每 2 帧 Yield(Input)<br/>用户点击可插队
|
|
|
+ UI_Thread-->>Disk: 后台 Task.Run 保存图像
|
|
|
+```
|
|
|
+
|
|
|
+**关键变化**:
|
|
|
+1. `ToolBlock.Run()` 在独立 STA 线程执行,不再触碰 UI 线程
|
|
|
+2. 渲染更新从"每帧独立 Normal 优先级"改为"批量 Background 优先级 + Yield"
|
|
|
+3. 保存逻辑的磁盘 I/O 已在后台线程,与渲染解耦
|
|
|
+
|
|
|
---
|
|
|
|
|
|
-## 4. 修复方案
|
|
|
+## 4. 修复方案与实施状态
|
|
|
|
|
|
-### 方案 A(P0 — 最优先):将 `ToolBlock.Run()` 放入专用 STA 后台线程
|
|
|
+### 方案 A(P0 — 已完成 ✅):将 `ToolBlock.Run()` 放入专用 STA 后台线程
|
|
|
|
|
|
VisionPro 的 COM 组件要求 STA 线程,但不应使用 UI 线程。需要创建独立的 STA 后台线程:
|
|
|
|
|
|
@@ -180,8 +363,17 @@ await RunToolBlockAsync(procedure.ToolBlock);
|
|
|
|
|
|
> ⚠️ 注意:如果同一个 `CogToolBlock` 实例需要被多个 ToolBlock 并发使用,VisionPro 可能不支持多线程同时操作同一个 ToolBlock 实例。建议为每个并发执行创建独立的 ToolBlock 实例(clone),或者在 ToolBlock 级别做串行化保护。
|
|
|
|
|
|
+#### 实施状态:✅ 已完成
|
|
|
+
|
|
|
+- 文件:`TeamAAS-VM/Helpers/VisionProHelper.cs`(新增)
|
|
|
+- 方法:`RunToolBlockAsync(CogToolBlock)` — 每次调用创建独立 STA 后台线程
|
|
|
+- 所有 6 处 `ToolBlock.Run()` 已替换为 `await VisionProHelper.RunToolBlockAsync(procedure.ToolBlock)`
|
|
|
+- 日志验证:`[VP-STA] STA-Run START, threadId=XX, apartment=STA` 中 threadId 均非 1
|
|
|
+
|
|
|
### 方案 B(P1):多 ToolBlock 真正并发执行
|
|
|
|
|
|
+> **状态:⚠️ 待评估。STA 隔离后串行的阻塞影响已大幅降低,可酌情排期。**
|
|
|
+
|
|
|
在 `Management.cs` 第 2620 行附近,将串行 `foreach` 改为并发 `Task.WhenAll`:
|
|
|
|
|
|
```csharp
|
|
|
@@ -235,12 +427,13 @@ catch (OperationCanceledException)
|
|
|
|
|
|
## 5. 优先级与影响评估
|
|
|
|
|
|
-| 优先级 | 修改项 | 涉及文件 | 风险 | 收益 |
|
|
|
-|--------|--------|----------|------|------|
|
|
|
-| **P0** | `ToolBlock.Run()` → STA 线程异步化 | `RemoteCommandService.cs`(6 处) | 中:需验证 ToolBlock 实例线程安全性 | 直接解决 UI 假死 |
|
|
|
-| **P1** | 多 ToolBlock 并发执行 | `Management.cs`(1 处) | 低:共享集合已线程安全 | 减少总流程耗时 |
|
|
|
-| **P2** | VPP 加载异步化 | `ProductService.cs`(4 处) | 低:纯 I/O 操作 | 产品切换时 UI 不卡 |
|
|
|
-| **P3** | `countdown.Wait` → `await` | `Management.cs`(1 处) | 低:等价语义 | 避免线程池阻塞 |
|
|
|
+| 优先级 | 修改项 | 涉及文件 | 状态 | 风险 | 收益 |
|
|
|
+|--------|--------|----------|------|------|------|
|
|
|
+| **P0** | `ToolBlock.Run()` → STA 线程异步化 | `RemoteCommandService.cs`(6 处)、`VisionProHelper.cs`(新增) | ✅ 已完成 | 中:需验证 ToolBlock 实例线程安全性 | 直接解决 ToolBlock 导致的 UI 假死 |
|
|
|
+| **P0** | `RenderUpdateNotification` 渲染批处理 | `ShowVisionRender.xaml.cs` | ✅ 已完成 | 低:仅改变调度策略,不改业务逻辑 | 解决多任务渲染导致的 UI 饥饿 |
|
|
|
+| **P1** | 多 ToolBlock 并发执行 | `Management.cs`(1 处) | ⚠️ 待评估 | 低:共享集合已线程安全 | 减少总流程耗时 |
|
|
|
+| **P2** | VPP 加载异步化 | `ProductService.cs`(4 处) | ⚠️ 部分完成 | 低:纯 I/O 操作 | 产品切换时 UI 不卡 |
|
|
|
+| **P3** | `countdown.Wait` → `await` | `Management.cs`(1 处) | ⚠️ 待修复 | 低:等价语义 | 避免线程池阻塞 |
|
|
|
|
|
|
---
|
|
|
|
|
|
@@ -266,12 +459,14 @@ gantt
|
|
|
dateFormat YYYY-MM-DD
|
|
|
axisFormat %m/%d
|
|
|
section Phase 1 - 基础设施
|
|
|
- Step1-创建RunToolBlockAsync工具方法 :a1, 2026-07-12, 1d
|
|
|
+ Step1-创建RunToolBlockAsync工具方法 :done, a1, 2026-07-12, 1d
|
|
|
section Phase 2 - P0 核心修复
|
|
|
- Step2-修改RemoteCommandService.cs(6处) :a2, after a1, 2d
|
|
|
- Step3-编译验证+本地冒烟测试 :a3, after a2, 1d
|
|
|
+ Step2-修改RemoteCommandService.cs(6处) :done, a2, after a1, 1d
|
|
|
+ Step3-编译验证+本地冒烟测试 :done, a3, after a2, 1d
|
|
|
+ section Phase 2b - P0 渲染修复(新增)
|
|
|
+ Step3b-RenderUpdate批处理+Background优先级 :done, a3b, after a3, 1d
|
|
|
section Phase 3 - P1 并发优化
|
|
|
- Step4-修改Management.cs ToolBlock并发执行 :a4, after a3, 1d
|
|
|
+ Step4-修改Management.cs ToolBlock并发执行 :a4, after a3b, 1d
|
|
|
section Phase 4 - P2 VPP加载
|
|
|
Step5-修改ProductService.cs异步加载 :a5, after a4, 1d
|
|
|
section Phase 5 - P3 Countdown
|
|
|
@@ -282,10 +477,10 @@ gantt
|
|
|
|
|
|
### 7.2 前置条件
|
|
|
|
|
|
-- [ ] 确认开发环境可正常编译当前代码
|
|
|
-- [ ] 准备至少一台可连接的真实相机和 PLC 用于集成测试
|
|
|
-- [ ] 准备 2~3 个包含有效 `.vpp` ToolBlock 的测试产品
|
|
|
-- [ ] 确保 VisionPro 运行时许可在测试机上可用
|
|
|
+- [x] 确认开发环境可正常编译当前代码
|
|
|
+- [x] 准备至少一台可连接的真实相机和 PLC 用于集成测试
|
|
|
+- [x] 准备 2~3 个包含有效 `.vpp` ToolBlock 的测试产品
|
|
|
+- [x] 确保 VisionPro 运行时许可在测试机上可用
|
|
|
- [ ] 建议在版本控制中创建分支 `fix/toolblock-ui-blocking` 进行修改
|
|
|
|
|
|
### 7.3 分步实施详情
|