diff --git a/Logger/Nlog.config b/Logger/Nlog.config index 1b31316..6ab6379 100644 --- a/Logger/Nlog.config +++ b/Logger/Nlog.config @@ -3,7 +3,7 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> - + _logs = new(); @@ -23,11 +22,14 @@ namespace TestingModule.ViewModels get => _logs; set => SetProperty(ref _logs, value); } + public ICommand ClearLogCommand { get; set; } private readonly ScopedContext _scopedContext; private readonly GlobalInfo _globalInfo; - private IProgress<(string Message, Brush Color, int Depth)>? _logProgress; + + /// UI 线程定时器,每 100ms 批量从 LogBuffer 出队并刷新 + private readonly DispatcherTimer _flushTimer; public LogAreaViewModel(IContainerProvider containerProvider) : base(containerProvider) { @@ -35,17 +37,39 @@ namespace TestingModule.ViewModels _scopedContext = containerProvider.Resolve(); _globalInfo = containerProvider.Resolve(); - // 创建本作用域专属的日志接收器并注册到 ScopedContext - _logProgress = new Progress<(string Message, Brush Color, int Depth)>(log => + // UI 线程定时器:每 100ms 从 ScopedContext.LogBuffer 批量出队并 Add + _flushTimer = new DispatcherTimer(DispatcherPriority.Normal, System.Windows.Application.Current.Dispatcher) { - if (Logs == null) return; - Logs.Add(new LogItem(log.Message, log.Color, log.Depth)); - }); - _scopedContext.LogProgress = _logProgress; + Interval = TimeSpan.FromMilliseconds(100) + }; + _flushTimer.Tick += FlushLogBuffer; + _flushTimer.Start(); + } + + /// + /// 定时批量刷新日志:一次性出队并 Add,WPF 对连续 Add 做合并渲染 + /// + private void FlushLogBuffer(object? sender, EventArgs e) + { + if (_scopedContext.LogBuffer.IsEmpty || Logs == null) return; + + var batch = new List<(string Message, Brush Color, int Depth)>(); + while (_scopedContext.LogBuffer.TryDequeue(out var item)) + { + batch.Add(item); + } + + // 批量 Add,WPF 会对连续 Add 做合并渲染,只触发一次 layout/render pass + foreach (var item in batch) + { + Logs.Add(new LogItem(item.Message, item.Color, item.Depth)); + } } private void ClearLog() { + // 清空缓冲区,防止定时器下次还把旧数据刷出来 + while (_scopedContext.LogBuffer.TryDequeue(out _)) { } Logs?.Clear(); } @@ -56,12 +80,12 @@ namespace TestingModule.ViewModels { try { - // 注销本作用域的日志接收器,避免 Dispose 后仍收到旧消息 - if (_scopedContext != null && _scopedContext.LogProgress == _logProgress) - { - _scopedContext.LogProgress = null; - } - _logProgress = null; + // 停止定时器 + _flushTimer.Stop(); + _flushTimer.Tick -= FlushLogBuffer; + + // 最后一次刷新缓冲区中的剩余日志 + FlushLogBuffer(null, EventArgs.Empty); if (Logs != null) { @@ -71,7 +95,9 @@ namespace TestingModule.ViewModels } catch (Exception ex) { - Logger.LoggerHelper.ErrorWithNotify(_scopedContext != null ? _globalInfo.CurrentScope : "", $"释放日志组件(LogAreaViewModel)资源失败: {ex.Message}"); + Logger.LoggerHelper.ErrorWithNotify( + _scopedContext != null ? _globalInfo.CurrentScope : "", + $"释放日志组件(LogAreaViewModel)资源失败: {ex.Message}"); } } } @@ -81,7 +107,8 @@ namespace TestingModule.ViewModels { public string Message { get; set; } public Brush Color { get; set; } = Brushes.Black; - public int Depth { get; set; } + public int Depth { get; set; } + public LogItem(string message, Brush color, int depth = 0) { Message = new string(' ', depth * 20) + message; diff --git a/UIShare/GlobalVariable/ScopeLogDispatcher.cs b/UIShare/GlobalVariable/ScopeLogDispatcher.cs index 7d4d8de..309e1f4 100644 --- a/UIShare/GlobalVariable/ScopeLogDispatcher.cs +++ b/UIShare/GlobalVariable/ScopeLogDispatcher.cs @@ -1,17 +1,22 @@ using Logger; using System; +using System.Collections.Concurrent; using System.Windows.Media; namespace UIShare.GlobalVariable { /// /// 作用域日志分发器:根据显式传入的 scope 参数把日志路由到对应 - /// ,实现每个台架/作用域拥有独立 LogArea。 + /// ,实现每个台架/作用域拥有独立 LogArea。 + /// 直接写入 ConcurrentQueue,不走 Progress<T> / SynchronizationContext,避免 UI 线程压力。 /// public class ScopeLogDispatcher : IProgress<(string scope, string message, string color, int depth)> { private readonly GlobalInfo _globalInfo; + /// Brush 缓存,避免每次都 new BrushConverter + private static readonly ConcurrentDictionary _brushCache = new(); + public ScopeLogDispatcher(GlobalInfo globalInfo) { _globalInfo = globalInfo ?? throw new ArgumentNullException(nameof(globalInfo)); @@ -23,19 +28,21 @@ namespace UIShare.GlobalVariable if (string.IsNullOrEmpty(scope)) return; if (!_globalInfo.ContextDic.TryGetValue(scope, out var context)) return; - if (context?.LogProgress == null) return; - Brush? brush = null; - try + Brush brush = _brushCache.GetOrAdd(value.color, color => { - brush = (Brush)new BrushConverter().ConvertFromString(value.color); - } - catch - { - brush = Brushes.Black; - } + try + { + return (Brush)new BrushConverter().ConvertFromString(color); + } + catch + { + return Brushes.Black; + } + }); - context.LogProgress.Report((value.message, brush, value.depth)); + // 直接入队到后台线程安全的 ConcurrentQueue,不走 SynchronizationContext + context.LogBuffer.Enqueue((value.message, brush, value.depth)); } } } diff --git a/UIShare/GlobalVariable/ScopedContext.cs b/UIShare/GlobalVariable/ScopedContext.cs index f8701fa..7fcffdb 100644 --- a/UIShare/GlobalVariable/ScopedContext.cs +++ b/UIShare/GlobalVariable/ScopedContext.cs @@ -1,6 +1,7 @@ using DeviceCommand.Base; using MaterialDesignThemes.Wpf; using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics; @@ -32,8 +33,12 @@ namespace UIShare.GlobalVariable public List DeviceList { get; set; } = new(); - /// 当前作用域的日志接收器,LogAreaViewModel 订阅此处实现按作用域隔离日志。 - public IProgress<(string Message, Brush Color, int Depth)>? LogProgress { get; set; } + /// + /// 日志缓冲队列:后台线程(ScopeLogDispatcher)直接入队, + /// UI 线程(DispatcherTimer)定时出队并刷新到 ObservableCollection。 + /// 不走 Progress<T> / SynchronizationContext,避免 Post 淹没 UI 消息队列。 + /// + public ConcurrentQueue<(string Message, Brush Color, int Depth)> LogBuffer { get; } = new(); // 【新增测试属性】:每个实例被 new 出来时独一无二的随机身份 // 证 ID