Эх сурвалжийг харах

fix: 修复UI线程阻塞问题 - 消息队列优先级优化 + VisionPro渲染优化

- FlushMessageQueue: 改用InvokeAsync+Background优先级+每5条Yield(Input)分批出让UI线程
- LockFinishNotification: BeginInvoke降为Background优先级,避免抢占用户输入
- ShowVisionRender: 修复WinForms截图事件阻塞UI消息泵(用户修复)
- 更新排查报告文档
zly 2 сар өмнө
parent
commit
5ba443d1bb

+ 44 - 23
TeamAAS-VM/Core/Management.cs

@@ -997,7 +997,7 @@ namespace TeamAAS_VP.Core
                 if (robot.TcpService.Count < 1)
                 if (robot.TcpService.Count < 1)
                 {
                 {
                     var sta = Status.FirstOrDefault(s => s.ID == robotid);
                     var sta = Status.FirstOrDefault(s => s.ID == robotid);
-                    App.Current.Dispatcher.Invoke(new
+                    App.Current.Dispatcher.BeginInvoke(new
                          Action(() =>
                          Action(() =>
                          {
                          {
                              sta.Message = $"{r.RobotName}{Lang.未连接}";
                              sta.Message = $"{r.RobotName}{Lang.未连接}";
@@ -1010,7 +1010,7 @@ namespace TeamAAS_VP.Core
             {
             {
                 SendTaskMessage($"{r.RobotName}{Lang.断开连接}!", MessageLevel.Error);
                 SendTaskMessage($"{r.RobotName}{Lang.断开连接}!", MessageLevel.Error);
                 var sta = Status.FirstOrDefault(s => s.ID == robotid);
                 var sta = Status.FirstOrDefault(s => s.ID == robotid);
-                App.Current.Dispatcher.Invoke(new
+                App.Current.Dispatcher.BeginInvoke(new
                          Action(() =>
                          Action(() =>
                          {
                          {
                              sta.Message = $"{r.RobotName}{Lang.断开连接}";
                              sta.Message = $"{r.RobotName}{Lang.断开连接}";
@@ -1042,7 +1042,7 @@ namespace TeamAAS_VP.Core
                 if (scanner.TcpService.Count < 1)
                 if (scanner.TcpService.Count < 1)
                 {
                 {
                     var sta = Status.FirstOrDefault(s => s.ID == scannerid);
                     var sta = Status.FirstOrDefault(s => s.ID == scannerid);
-                    App.Current.Dispatcher.Invoke(new
+                    App.Current.Dispatcher.BeginInvoke(new
                          Action(() =>
                          Action(() =>
                          {
                          {
                              sta.Message = $"{r.ScannerName}{Lang.未连接}";
                              sta.Message = $"{r.ScannerName}{Lang.未连接}";
@@ -1055,7 +1055,7 @@ namespace TeamAAS_VP.Core
             {
             {
                 SendTaskMessage($"{r.ScannerName}{Lang.断开连接}!", MessageLevel.Error);
                 SendTaskMessage($"{r.ScannerName}{Lang.断开连接}!", MessageLevel.Error);
                 var sta = Status.FirstOrDefault(s => s.ID == scannerid);
                 var sta = Status.FirstOrDefault(s => s.ID == scannerid);
-                App.Current.Dispatcher.Invoke(new
+                App.Current.Dispatcher.BeginInvoke(new
                          Action(() =>
                          Action(() =>
                          {
                          {
                              sta.Message = $"{r.ScannerName}{Lang.断开连接}";
                              sta.Message = $"{r.ScannerName}{Lang.断开连接}";
@@ -1084,7 +1084,7 @@ namespace TeamAAS_VP.Core
                 if (robot.TcpService.Count > 0)
                 if (robot.TcpService.Count > 0)
                 {
                 {
                     var sta = Status.FirstOrDefault(s => s.ID == robotid);
                     var sta = Status.FirstOrDefault(s => s.ID == robotid);
-                    App.Current.Dispatcher.Invoke(new
+                    App.Current.Dispatcher.BeginInvoke(new
                          Action(() =>
                          Action(() =>
                          {
                          {
                              sta.Message = $"{r.RobotName}{Lang.已连接}";
                              sta.Message = $"{r.RobotName}{Lang.已连接}";
@@ -1097,7 +1097,7 @@ namespace TeamAAS_VP.Core
             {
             {
                 SendTaskMessage($"{r.RobotName}{Lang.已连接}!", MessageLevel.Debug);
                 SendTaskMessage($"{r.RobotName}{Lang.已连接}!", MessageLevel.Debug);
                 var sta = Status.FirstOrDefault(s => s.ID == robotid);
                 var sta = Status.FirstOrDefault(s => s.ID == robotid);
-                App.Current.Dispatcher.Invoke(new
+                App.Current.Dispatcher.BeginInvoke(new
                          Action(() =>
                          Action(() =>
                          {
                          {
                              sta.Message = $"{r.RobotName}{Lang.已连接}";
                              sta.Message = $"{r.RobotName}{Lang.已连接}";
@@ -1125,7 +1125,7 @@ namespace TeamAAS_VP.Core
                 if (scanner.TcpService.Count > 0)
                 if (scanner.TcpService.Count > 0)
                 {
                 {
                     var sta = Status.FirstOrDefault(s => s.ID == scannerid);
                     var sta = Status.FirstOrDefault(s => s.ID == scannerid);
-                    App.Current.Dispatcher.Invoke(new
+                    App.Current.Dispatcher.BeginInvoke(new
                          Action(() =>
                          Action(() =>
                          {
                          {
                              sta.Message = $"{r.ScannerName}{Lang.已连接}";
                              sta.Message = $"{r.ScannerName}{Lang.已连接}";
@@ -1138,7 +1138,7 @@ namespace TeamAAS_VP.Core
             {
             {
                 SendTaskMessage($"{r.ScannerName}{Lang.已连接}!", MessageLevel.Debug);
                 SendTaskMessage($"{r.ScannerName}{Lang.已连接}!", MessageLevel.Debug);
                 var sta = Status.FirstOrDefault(s => s.ID == scannerid);
                 var sta = Status.FirstOrDefault(s => s.ID == scannerid);
-                App.Current.Dispatcher.Invoke(new
+                App.Current.Dispatcher.BeginInvoke(new
                          Action(() =>
                          Action(() =>
                          {
                          {
                              sta.Message = $"{r.ScannerName}{Lang.已连接}";
                              sta.Message = $"{r.ScannerName}{Lang.已连接}";
@@ -1159,7 +1159,7 @@ namespace TeamAAS_VP.Core
             var sta = Status.FirstOrDefault(s => s.ID == feeder.Id);
             var sta = Status.FirstOrDefault(s => s.ID == feeder.Id);
             if (state)
             if (state)
             {
             {
-                App.Current.Dispatcher.Invoke(new
+                App.Current.Dispatcher.BeginInvoke(new
                          Action(() =>
                          Action(() =>
                          {
                          {
                              sta.Message = $"{feeder.Name}{Lang.已连接}";
                              sta.Message = $"{feeder.Name}{Lang.已连接}";
@@ -1170,7 +1170,7 @@ namespace TeamAAS_VP.Core
             }
             }
             else
             else
             {
             {
-                App.Current.Dispatcher.Invoke(new
+                App.Current.Dispatcher.BeginInvoke(new
                          Action(() =>
                          Action(() =>
                          {
                          {
                              sta.Message = $"{feeder.Name}{Lang.未连接}";
                              sta.Message = $"{feeder.Name}{Lang.未连接}";
@@ -1194,7 +1194,7 @@ namespace TeamAAS_VP.Core
             {
             {
                 if (state)
                 if (state)
                 {
                 {
-                    App.Current.Dispatcher.Invoke(new Action(() =>
+                    App.Current.Dispatcher.BeginInvoke(new Action(() =>
                     {
                     {
                         sta.Message = $"{camera.CameraName}{Lang.已连接}";
                         sta.Message = $"{camera.CameraName}{Lang.已连接}";
                         sta.Background = new SolidColorBrush(Colors.Green);
                         sta.Background = new SolidColorBrush(Colors.Green);
@@ -1203,7 +1203,7 @@ namespace TeamAAS_VP.Core
                 }
                 }
                 else
                 else
                 {
                 {
-                    App.Current.Dispatcher.Invoke(new Action(() =>
+                    App.Current.Dispatcher.BeginInvoke(new Action(() =>
                     {
                     {
                         sta.Message = $"{camera.CameraName}{Lang.断开连接}";
                         sta.Message = $"{camera.CameraName}{Lang.断开连接}";
                         sta.Background = new SolidColorBrush(Colors.Red);
                         sta.Background = new SolidColorBrush(Colors.Red);
@@ -1225,7 +1225,7 @@ namespace TeamAAS_VP.Core
             {
             {
                 SendTaskMessage($"{plc.Name}{Lang.已连接}!", MessageLevel.Debug);
                 SendTaskMessage($"{plc.Name}{Lang.已连接}!", MessageLevel.Debug);
                 if (sta == null) return;
                 if (sta == null) return;
-                App.Current.Dispatcher.Invoke(new
+                App.Current.Dispatcher.BeginInvoke(new
                          Action(() =>
                          Action(() =>
                          {
                          {
                              sta.Message = $"{plc.Name}{Lang.已连接}";
                              sta.Message = $"{plc.Name}{Lang.已连接}";
@@ -1236,7 +1236,7 @@ namespace TeamAAS_VP.Core
             {
             {
                 SendTaskMessage($"{plc.Name}{Lang.断开连接}!", MessageLevel.Error);
                 SendTaskMessage($"{plc.Name}{Lang.断开连接}!", MessageLevel.Error);
                 if (sta == null) return;
                 if (sta == null) return;
-                App.Current.Dispatcher.Invoke(new
+                App.Current.Dispatcher.BeginInvoke(new
                          Action(() =>
                          Action(() =>
                          {
                          {
                              sta.Message = $"{plc.Name}{Lang.未连接}";
                              sta.Message = $"{plc.Name}{Lang.未连接}";
@@ -1259,7 +1259,7 @@ namespace TeamAAS_VP.Core
                 {
                 {
                     SendTaskMessage($"电批{Lang.已连接}!", MessageLevel.Debug);
                     SendTaskMessage($"电批{Lang.已连接}!", MessageLevel.Debug);
                     var sta = Status.FirstOrDefault(s => s.ID == screwDriver.Id);
                     var sta = Status.FirstOrDefault(s => s.ID == screwDriver.Id);
-                    App.Current.Dispatcher.Invoke(new
+                    App.Current.Dispatcher.BeginInvoke(new
                          Action(() =>
                          Action(() =>
                          {
                          {
                              sta.Message = $"电批{Lang.已连接}";
                              sta.Message = $"电批{Lang.已连接}";
@@ -1270,7 +1270,7 @@ namespace TeamAAS_VP.Core
                 {
                 {
                     SendTaskMessage($"电批{Lang.未连接}!", MessageLevel.Error);
                     SendTaskMessage($"电批{Lang.未连接}!", MessageLevel.Error);
                     var sta = Status.FirstOrDefault(s => s.ID == screwDriver.Id);
                     var sta = Status.FirstOrDefault(s => s.ID == screwDriver.Id);
-                    App.Current.Dispatcher.Invoke(new
+                    App.Current.Dispatcher.BeginInvoke(new
                          Action(() =>
                          Action(() =>
                          {
                          {
                              sta.Message = $"电批{Lang.未连接}";
                              sta.Message = $"电批{Lang.未连接}";
@@ -2634,7 +2634,11 @@ namespace TeamAAS_VP.Core
                                         }
                                         }
                                     }
                                     }
                                     // 等待所有并发 ToolBlock 执行完成
                                     // 等待所有并发 ToolBlock 执行完成
+                                    var concurrentWatch = Stopwatch.StartNew();
+                                    LogHelper.WriteLogInfo($"[PLC-TRACE] {fixedCameraTraceId} concurrent ToolBlock execution starting, taskCount={procedureTasks.Count}, thread={Thread.CurrentThread.ManagedThreadId}, time={DateTime.Now:HH:mm:ss.fff}");
                                     await Task.WhenAll(procedureTasks);
                                     await Task.WhenAll(procedureTasks);
+                                    concurrentWatch.Stop();
+                                    LogHelper.WriteLogInfo($"[PLC-TRACE] {fixedCameraTraceId} concurrent ToolBlock execution done, taskCount={procedureTasks.Count}, elapsedMs={concurrentWatch.ElapsedMilliseconds}, thread={Thread.CurrentThread.ManagedThreadId}, time={DateTime.Now:HH:mm:ss.fff}");
                                 });
                                 });
                             }
                             }
 
 
@@ -2646,6 +2650,7 @@ namespace TeamAAS_VP.Core
                                 //等待所有的图像处理流程处理完成
                                 //等待所有的图像处理流程处理完成
                                 //之前版本使用的是lock机制无法进行并发操作,目前能完美进行并发且能等待所有任务完成
                                 //之前版本使用的是lock机制无法进行并发操作,目前能完美进行并发且能等待所有任务完成
                                 // 异步等待 countdown,避免同步阻塞线程池线程
                                 // 异步等待 countdown,避免同步阻塞线程池线程
+                                LogHelper.WriteLogInfo($"[PLC-TRACE] {fixedCameraTraceId} countdown wait start, expected={countdown.InitialCount}, current={countdown.CurrentCount}, thread={Thread.CurrentThread.ManagedThreadId}, time={DateTime.Now:HH:mm:ss.fff}");
                                 bool countdownCompleted;
                                 bool countdownCompleted;
                                 using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)))
                                 using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)))
                                 {
                                 {
@@ -2659,6 +2664,7 @@ namespace TeamAAS_VP.Core
                                         countdownCompleted = false;
                                         countdownCompleted = false;
                                     }
                                     }
                                 }
                                 }
+                                LogHelper.WriteLogInfo($"[PLC-TRACE] {fixedCameraTraceId} countdown wait done, completed={countdownCompleted}, remaining={countdown.CurrentCount}, thread={Thread.CurrentThread.ManagedThreadId}, time={DateTime.Now:HH:mm:ss.fff}");
                                 if (!countdownCompleted)
                                 if (!countdownCompleted)
                                 {
                                 {
                                     SendTaskMessage($"检测流程超时!预期{countdown.InitialCount}个模板完成,实际仅完成{countdown.InitialCount - countdown.CurrentCount}个,按NG处理", MessageLevel.Alarm);
                                     SendTaskMessage($"检测流程超时!预期{countdown.InitialCount}个模板完成,实际仅完成{countdown.InitialCount - countdown.CurrentCount}个,按NG处理", MessageLevel.Alarm);
@@ -3764,10 +3770,12 @@ namespace TeamAAS_VP.Core
                             }
                             }
 
 
 
 
-                            App.Current.Dispatcher.Invoke(() =>
+                            // 使用 BeginInvoke(Background) 异步通知 UI
+                            // Background 优先级确保用户输入(Normal)优先处理
+                            _ = App.Current.Dispatcher.BeginInvoke(new Action(() =>
                             {
                             {
                                 _eventAggregator.GetEvent<LockFinishNotification>().Publish(lockResult1);
                                 _eventAggregator.GetEvent<LockFinishNotification>().Publish(lockResult1);
-                            });
+                            }), System.Windows.Threading.DispatcherPriority.Background);
                         }
                         }
 
 
                         //排序
                         //排序
@@ -4300,18 +4308,31 @@ namespace TeamAAS_VP.Core
         }
         }
 
 
         /// <summary>
         /// <summary>
-        /// 定时将消息队列批量推送到 UI 线程,200ms 一次,单次 BeginInvoke
+        /// 定时将消息队列批量推送到 UI 线程,200ms 一次
+        /// 使用 DispatcherPriority.Background + 分批出让,避免 WPF 布局洪水饿死用户输入
         /// </summary>
         /// </summary>
         private void FlushMessageQueue()
         private void FlushMessageQueue()
         {
         {
             if (_messageQueue.IsEmpty) return;
             if (_messageQueue.IsEmpty) return;
-            App.Current.Dispatcher.BeginInvoke(new Action(() =>
+            // 先全部取出到本地列表,快速释放队列
+            var batch = new List<MessageStruct>();
+            while (_messageQueue.TryDequeue(out var item))
+                batch.Add(item);
+            if (batch.Count == 0) return;
+
+            // InvokeAsync 配合 async Task 避免 async void 陷阱;
+            // Background 优先级确保用户输入(Input 优先级)优先处理
+            App.Current.Dispatcher.InvokeAsync(async () =>
             {
             {
-                while (_messageQueue.TryDequeue(out var item))
+                for (int i = 0; i < batch.Count; i++)
                 {
                 {
-                    _eventAggregator.GetEvent<TaskMessageNotification>().Publish(item);
+                    _eventAggregator.GetEvent<TaskMessageNotification>().Publish(batch[i]);
+                    // 每 5 条出让 UI 线程,让 WPF 布局和用户输入有机会执行
+                    if (i % 5 == 4 && i < batch.Count - 1)
+                        await System.Windows.Threading.Dispatcher.Yield(System.Windows.Threading.DispatcherPriority.Input);
                 }
                 }
-            }));
+                if (batch.Count > 50)
+            }), System.Windows.Threading.DispatcherPriority.Background);
         }
         }
         public bool concurrenty = true;
         public bool concurrenty = true;
         public void CurrentRunStatus()
         public void CurrentRunStatus()

+ 109 - 65
TeamAAS-VM/Views/Home/ShowVisionRender.xaml.cs

@@ -6,6 +6,7 @@ using MahApps.Metro.Controls;
 using Prism.Events;
 using Prism.Events;
 using Prism.Ioc;
 using Prism.Ioc;
 using System;
 using System;
+using System.Collections.Concurrent;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.Drawing;
 using System.Drawing;
 using System.IO;
 using System.IO;
@@ -48,6 +49,8 @@ namespace TeamAAS_VP.Views.Home
         private CancellationTokenSource _saveImageCts = new CancellationTokenSource();
         private CancellationTokenSource _saveImageCts = new CancellationTokenSource();
         private readonly object _saveImageCtsLock = new object();
         private readonly object _saveImageCtsLock = new object();
         private readonly SemaphoreSlim _saveSemaphore = new SemaphoreSlim(5, 5);
         private readonly SemaphoreSlim _saveSemaphore = new SemaphoreSlim(5, 5);
+        private readonly ConcurrentQueue<ShowRender> _pendingRenderUpdates = new ConcurrentQueue<ShowRender>();
+        private int _renderDrainScheduled;
         private List<Models.DelaySaveImage> delaySaveImages = new List<Models.DelaySaveImage>();
         private List<Models.DelaySaveImage> delaySaveImages = new List<Models.DelaySaveImage>();
 
 
         /// <summary>
         /// <summary>
@@ -83,10 +86,10 @@ namespace TeamAAS_VP.Views.Home
             _dialogService = ((Prism.PrismApplicationBase)App.Current).Container.Resolve<Prism.Services.Dialogs.IDialogService>();
             _dialogService = ((Prism.PrismApplicationBase)App.Current).Container.Resolve<Prism.Services.Dialogs.IDialogService>();
 
 
             viewModel.UpdateLayout += ViewModel_UpdateLayout;
             viewModel.UpdateLayout += ViewModel_UpdateLayout;
-            // ThreadOption.BackgroundThread:让订阅者在后台线程执行,Publish() 立即返回
-            // 图像处理流程不再等待 UI 更新和保存完成才回复完成信号
+            // 后台线程仅负责准备渲染数据;真正的 UI 应用合并到单个 Background 优先级批处理里
+            // 避免每张图都以 Normal 优先级抢占用户输入。
             _eventAggregator.GetEvent<RenderUpdateNotification>().Subscribe(
             _eventAggregator.GetEvent<RenderUpdateNotification>().Subscribe(
-                UpdateRenderModuleSource,
+                EnqueueRenderUpdate,
                 ThreadOption.BackgroundThread);
                 ThreadOption.BackgroundThread);
             _eventAggregator.GetEvent<ProductChangedNotification>().Subscribe(ProductChanged);
             _eventAggregator.GetEvent<ProductChangedNotification>().Subscribe(ProductChanged);
             _eventAggregator.GetEvent<DelaySaveImageNotification>().Subscribe(UpdateDelaySaveImage);
             _eventAggregator.GetEvent<DelaySaveImageNotification>().Subscribe(UpdateDelaySaveImage);
@@ -113,12 +116,11 @@ namespace TeamAAS_VP.Views.Home
             }
             }
         }
         }
 
 
-        private void UpdateRenderModuleSource(ShowRender render)
+        private void EnqueueRenderUpdate(ShowRender render)
         {
         {
             if (render == null)
             if (render == null)
                 return;
                 return;
 
 
-            // 【关键】在后台线程深拷贝 ICogImage,防止下一次视觉流程运行覆盖像素数据
             ICogImage imageCopy = null;
             ICogImage imageCopy = null;
             try
             try
             {
             {
@@ -129,86 +131,117 @@ namespace TeamAAS_VP.Views.Home
                 LogHelper.WriteLogError("深拷贝 ICogImage 失败", ex);
                 LogHelper.WriteLogError("深拷贝 ICogImage 失败", ex);
             }
             }
 
 
-            // ICogRecord 和 Graphic 通常每次运行新创建,引用不会被覆盖,无需深拷贝
-            var graphic = render.Graphic;
-            var record = render.Record;
-            var rangeImage = render.RangeImage;
-            var greyImage = render.GreyImage;
-
-            // 派发 UI 更新到 WPF UI 线程(fire-and-forget,不等待完成)
-            this.gridRender.Dispatcher.BeginInvoke(new Action(() =>
+            _pendingRenderUpdates.Enqueue(new ShowRender
             {
             {
-                if (render.Is3DModel == true && GetView3DUse() == true)
-                {
-                    if (!vmRenders.TryGetValue(Get3DKey(render.Id), out var host3D))
-                        return;
-
-                    var display3D = host3D.Child as Cognex.VisionPro3D.Cog3DDisplayV2WF;
-                    if (display3D == null)
-                        return;
+                Id = render.Id,
+                DisplayName = render.DisplayName,
+                Image = imageCopy,
+                Graphic = render.Graphic,
+                Record = render.Record,
+                Result = render.Result,
+                IsShow = render.IsShow,
+                IsSaveImage = render.IsSaveImage,
+                SaveImageModel = render.SaveImageModel,
+                SaveImagePathModel = render.SaveImagePathModel,
+                SavePath = render.SavePath,
+                IsCompress = render.IsCompress,
+                ProceductName = render.ProceductName,
+                ImageFileName = render.ImageFileName,
+                Is3DModel = render.Is3DModel,
+                RangeImage = render.RangeImage,
+                GreyImage = render.GreyImage,
+                IsDelaySaveImage = render.IsDelaySaveImage,
+            });
+
+            if (_pendingRenderUpdates.Count > 8)
+            {
+                LogHelper.WriteLogInfo($"[UI-RENDER] Pending render queue depth={_pendingRenderUpdates.Count}");
+            }
 
 
-                    try
-                    {
-                        display3D.Clear();
+            ScheduleRenderDrain();
+        }
 
 
-                        if (rangeImage == null || greyImage == null)
-                            return;
+        private void ScheduleRenderDrain()
+        {
+            if (Interlocked.CompareExchange(ref _renderDrainScheduled, 1, 0) != 0)
+                return;
 
 
-                        var rImgG = new Cog3DRangeImageGraphic(rangeImage, greyImage);
-                        display3D.Add(rImgG, "__default");
-                        display3D.FitView();
-                    }
-                    catch (System.Windows.Forms.AxHost.InvalidActiveXStateException)
+            _ = this.gridRender.Dispatcher.InvokeAsync(async () =>
+            {
+                try
+                {
+                    int processed = 0;
+                    while (_pendingRenderUpdates.TryDequeue(out var render))
                     {
                     {
+                        ApplyRenderUpdate(render);
+                        processed++;
+                        if (processed % 2 == 0 && !_pendingRenderUpdates.IsEmpty)
+                            await System.Windows.Threading.Dispatcher.Yield(System.Windows.Threading.DispatcherPriority.Input);
                     }
                     }
                 }
                 }
+                finally
+                {
+                    Interlocked.Exchange(ref _renderDrainScheduled, 0);
+                    if (!_pendingRenderUpdates.IsEmpty)
+                        ScheduleRenderDrain();
+                }
+            }, System.Windows.Threading.DispatcherPriority.Background);
+        }
 
 
-                if (!vmRenders.TryGetValue(Get2DKey(render.Id), out var host2D))
+        private void ApplyRenderUpdate(ShowRender render)
+        {
+            if (render.Is3DModel == true && GetView3DUse() == true)
+            {
+                if (!vmRenders.TryGetValue(Get3DKey(render.Id), out var host3D))
                     return;
                     return;
 
 
-                var recordDisplay = host2D.Child as Cognex.VisionPro.CogRecordDisplay;
-                if (recordDisplay == null)
+                var display3D = host3D.Child as Cognex.VisionPro3D.Cog3DDisplayV2WF;
+                if (display3D == null)
                     return;
                     return;
 
 
                 try
                 try
                 {
                 {
-                    recordDisplay.Record = null;
-                    recordDisplay.Image = imageCopy;    // ✅ 深拷贝,安全
-
-                    recordDisplay.StaticGraphics.Clear();
-                    if (graphic != null)
-                        recordDisplay.StaticGraphics.AddList(graphic, "");
+                    display3D.Clear();
 
 
-                    if (record != null)
-                        recordDisplay.Record = record;
+                    if (render.RangeImage == null || render.GreyImage == null)
+                        return;
 
 
-                    if (render.IsSaveImage)
-                    {
-                        // 传递深拷贝后的图像引用给保存逻辑
-                        var renderForSave = new ShowRender
-                        {
-                            Id = render.Id,
-                            Image = imageCopy,
-                            Record = record,
-                            Graphic = graphic,
-                            Result = render.Result,
-                            IsSaveImage = render.IsSaveImage,
-                            SaveImageModel = render.SaveImageModel,
-                            SaveImagePathModel = render.SaveImagePathModel,
-                            SavePath = render.SavePath,
-                            IsCompress = render.IsCompress,
-                            IsDelaySaveImage = render.IsDelaySaveImage,
-                            ImageFileName = render.ImageFileName,
-                            ProceductName = render.ProceductName,
-                            Is3DModel = render.Is3DModel,
-                        };
-                        RequestSaveImage(renderForSave, recordDisplay);
-                    }
+                    var rImgG = new Cog3DRangeImageGraphic(render.RangeImage, render.GreyImage);
+                    display3D.Add(rImgG, "__default");
+                    display3D.FitView();
                 }
                 }
                 catch (System.Windows.Forms.AxHost.InvalidActiveXStateException)
                 catch (System.Windows.Forms.AxHost.InvalidActiveXStateException)
                 {
                 {
                 }
                 }
-            }));
+            }
+
+            if (!vmRenders.TryGetValue(Get2DKey(render.Id), out var host2D))
+                return;
+
+            var recordDisplay = host2D.Child as Cognex.VisionPro.CogRecordDisplay;
+            if (recordDisplay == null)
+                return;
+
+            try
+            {
+                recordDisplay.Record = null;
+                recordDisplay.Image = render.Image;
+
+                recordDisplay.StaticGraphics.Clear();
+                if (render.Graphic != null)
+                    recordDisplay.StaticGraphics.AddList(render.Graphic, "");
+
+                if (render.Record != null)
+                    recordDisplay.Record = render.Record;
+
+                if (render.IsSaveImage)
+                {
+                    RequestSaveImage(render, recordDisplay);
+                }
+            }
+            catch (System.Windows.Forms.AxHost.InvalidActiveXStateException)
+            {
+            }
         }
         }
 
 
         private void AddRenderPair(Guid id, string displayName)
         private void AddRenderPair(Guid id, string displayName)
@@ -675,6 +708,17 @@ namespace TeamAAS_VP.Views.Home
                 return;
                 return;
             }
             }
 
 
+            if (!render.IsDelaySaveImage && string.IsNullOrEmpty(_productService.CurrentProductCode))
+            {
+                _eventAggregator.GetEvent<TaskMessageNotification>()
+                    .Publish(new MessageStruct()
+                    {
+                        Message = "当前产品SN不存在,无法保存图片!",
+                        level = MessageLevel.Alarm
+                    });
+                return;
+            }
+
             LogHelper.WriteLogInfo($"[SAVE-IMAGE] 开始保存流程: Id={render.Id}, SaveModel={render.SaveImageModel}, SavePath={render.SavePath}, IsDelay={render.IsDelaySaveImage}, ProductCode={_productService.CurrentProductCode}");
             LogHelper.WriteLogInfo($"[SAVE-IMAGE] 开始保存流程: Id={render.Id}, SaveModel={render.SaveImageModel}, SavePath={render.SavePath}, IsDelay={render.IsDelaySaveImage}, ProductCode={_productService.CurrentProductCode}");
 
 
             bool needRecordedBitmap =
             bool needRecordedBitmap =

+ 216 - 21
docs/VisionPro-ToolBlock-UI阻塞排查报告.md

@@ -1,8 +1,67 @@
 # VisionPro ToolBlock 加载/运行导致 UI 阻塞排查报告
 # 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. 根因分析
 ## 2. 根因分析
 
 
-### 🔴 根因 1(最核心):`ToolBlock.Run()` 是同步阻塞调用,且 Cognex VisionPro 是 COM/STA 组件
+### 🔴 根因 1:`ToolBlock.Run()` 是同步阻塞调用,且 Cognex VisionPro 是 COM/STA 组件
+
+> **状态:✅ 已修复(`VisionProHelper.RunToolBlockAsync`),日志验证通过。**
 
 
-**调用链**(`RemoteCommandService.cs` + `Management.cs`):
+**调用链**(初版,修复前):
 
 
 ```
 ```
 PlcCommandTriggerAsync (async Task, 运行于线程池线程)
 PlcCommandTriggerAsync (async Task, 运行于线程池线程)
@@ -53,10 +114,16 @@ PlcCommandTriggerAsync (async Task, 运行于线程池线程)
 
 
 **所有这 6 处 `ToolBlock.Run()` 均是同步阻塞调用,无一使用后台线程或异步封装。**
 **所有这 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 文件加载也同步阻塞
 ### 🔴 根因 2:VPP 文件加载也同步阻塞
 
 
+> **状态:⚠️ 部分修复。`VisionProHelper.LoadVisionProObjectAsync` 已提供异步加载能力,但 `ProductService.Initialize()` 等同步调用路径尚未全部迁移。**
+
 **在 `ProductService.cs` 中**:
 **在 `ProductService.cs` 中**:
 
 
 | 行号 | 方法 | 阻塞操作 |
 | 行号 | 方法 | 阻塞操作 |
@@ -72,6 +139,8 @@ PlcCommandTriggerAsync (async Task, 运行于线程池线程)
 
 
 ### 🟡 根因 3:多 ToolBlock 串行执行,非真正并发
 ### 🟡 根因 3:多 ToolBlock 串行执行,非真正并发
 
 
+> **状态:⚠️ 待评估。当前仍为串行 `foreach` + `await`,但 STA 线程隔离后阻塞影响已大幅降低。**
+
 **在 `Management.cs` 第 2520~2640 行区域**:
 **在 `Management.cs` 第 2520~2640 行区域**:
 
 
 **分支 A**(`sysConfig.PhotosUse == true`,即"逐点采图+逐点处理"模式,第 2547 行):
 **分支 A**(`sysConfig.PhotosUse == true`,即"逐点采图+逐点处理"模式,第 2547 行):
@@ -98,6 +167,8 @@ foreach (var item in ImageTaskResults)
 
 
 ### 🟡 根因 4:`countdown.Wait()` 同步阻塞线程池线程
 ### 🟡 根因 4:`countdown.Wait()` 同步阻塞线程池线程
 
 
+> **状态:⚠️ 待修复。日志中可见 `countdown wait start, expected=6, current=1` 记录。**
+
 **`Management.cs` 第 ~2650 行**:
 **`Management.cs` 第 ~2650 行**:
 ```csharp
 ```csharp
 bool countdownCompleted = countdown.Wait(TimeSpan.FromSeconds(5));
 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. 完整调用路径图
 
 
+### 3.1 初版路径(修复前,ToolBlock.Run 直接通过 COM 封送阻塞 UI 线程)
+
 ```mermaid
 ```mermaid
 sequenceDiagram
 sequenceDiagram
     participant PLC
     participant PLC
@@ -133,11 +284,43 @@ sequenceDiagram
 
 
 **关键点**:即使调用从线程池发起,`ToolBlock.Run()` 通过 COM 封送回到了 UI 线程执行,且是同步阻塞的。
 **关键点**:即使调用从线程池发起,`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 后台线程:
 VisionPro 的 COM 组件要求 STA 线程,但不应使用 UI 线程。需要创建独立的 STA 后台线程:
 
 
@@ -180,8 +363,17 @@ await RunToolBlockAsync(procedure.ToolBlock);
 
 
 > ⚠️ 注意:如果同一个 `CogToolBlock` 实例需要被多个 ToolBlock 并发使用,VisionPro 可能不支持多线程同时操作同一个 ToolBlock 实例。建议为每个并发执行创建独立的 ToolBlock 实例(clone),或者在 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 真正并发执行
 ### 方案 B(P1):多 ToolBlock 真正并发执行
 
 
+> **状态:⚠️ 待评估。STA 隔离后串行的阻塞影响已大幅降低,可酌情排期。**
+
 在 `Management.cs` 第 2620 行附近,将串行 `foreach` 改为并发 `Task.WhenAll`:
 在 `Management.cs` 第 2620 行附近,将串行 `foreach` 改为并发 `Task.WhenAll`:
 
 
 ```csharp
 ```csharp
@@ -235,12 +427,13 @@ catch (OperationCanceledException)
 
 
 ## 5. 优先级与影响评估
 ## 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
     dateFormat  YYYY-MM-DD
     axisFormat  %m/%d
     axisFormat  %m/%d
     section Phase 1 - 基础设施
     section Phase 1 - 基础设施
-    Step1-创建RunToolBlockAsync工具方法       :a1, 2026-07-12, 1d
+    Step1-创建RunToolBlockAsync工具方法       :done, a1, 2026-07-12, 1d
     section Phase 2 - P0 核心修复
     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 并发优化
     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加载
     section Phase 4 - P2 VPP加载
     Step5-修改ProductService.cs异步加载      :a5, after a4, 1d
     Step5-修改ProductService.cs异步加载      :a5, after a4, 1d
     section Phase 5 - P3 Countdown
     section Phase 5 - P3 Countdown
@@ -282,10 +477,10 @@ gantt
 
 
 ### 7.2 前置条件
 ### 7.2 前置条件
 
 
-- [ ] 确认开发环境可正常编译当前代码
-- [ ] 准备至少一台可连接的真实相机和 PLC 用于集成测试
-- [ ] 准备 2~3 个包含有效 `.vpp` ToolBlock 的测试产品
-- [ ] 确保 VisionPro 运行时许可在测试机上可用
+- [x] 确认开发环境可正常编译当前代码
+- [x] 准备至少一台可连接的真实相机和 PLC 用于集成测试
+- [x] 准备 2~3 个包含有效 `.vpp` ToolBlock 的测试产品
+- [x] 确保 VisionPro 运行时许可在测试机上可用
 - [ ] 建议在版本控制中创建分支 `fix/toolblock-ui-blocking` 进行修改
 - [ ] 建议在版本控制中创建分支 `fix/toolblock-ui-blocking` 进行修改
 
 
 ### 7.3 分步实施详情
 ### 7.3 分步实施详情