From d186b1d8e920d87e975f11beb0213b44dae087bd Mon Sep 17 00:00:00 2001 From: xpzhxhm <128050350+xpzhxhm@users.noreply.github.com> Date: Sat, 14 Mar 2026 22:37:20 +0000 Subject: [PATCH] Limit MsgView log queue growth to reduce memory usage --- v2rayN/ServiceLib/ViewModels/MsgViewModel.cs | 97 +++++++++++++++++--- v2rayN/v2rayN.Desktop/Views/MsgView.axaml.cs | 30 +++--- v2rayN/v2rayN/Views/MsgView.xaml.cs | 24 ++++- 3 files changed, 120 insertions(+), 31 deletions(-) diff --git a/v2rayN/ServiceLib/ViewModels/MsgViewModel.cs b/v2rayN/ServiceLib/ViewModels/MsgViewModel.cs index 06657a93..9b5764d5 100644 --- a/v2rayN/ServiceLib/ViewModels/MsgViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/MsgViewModel.cs @@ -2,9 +2,15 @@ namespace ServiceLib.ViewModels; public class MsgViewModel : MyReactiveObject { + private const int MaxQueuedMessagesVisible = 2_000; + private const int MaxQueuedMessagesHidden = 500; + private const int MaxFlushMessages = 200; + private const int MaxFlushChars = 64 * 1024; private readonly ConcurrentQueue _queueMsg = new(); private volatile bool _lastMsgFilterNotAvailable; + private int _queuedMessageCount = 0; private int _showLock = 0; // 0 = unlocked, 1 = locked + private long _droppedMessageCount = 0; public int NumMaxMsg { get; } = 500; [Reactive] @@ -31,19 +37,28 @@ public class MsgViewModel : MyReactiveObject AppEvents.SendMsgViewRequested .AsObservable() - //.ObserveOn(RxSchedulers.MainThreadScheduler) + //.ObserveOn(RxApp.MainThreadScheduler) .Subscribe(content => _ = AppendQueueMsg(content)); } - private async Task AppendQueueMsg(string msg) + private Task AppendQueueMsg(string msg) { if (AutoRefresh == false) { - return; + return Task.CompletedTask; } - EnqueueQueueMsg(msg); + if (!EnqueueQueueMsg(msg)) + { + return Task.CompletedTask; + } + TryScheduleDrain(); + return Task.CompletedTask; + } + + private void TryScheduleDrain() + { if (!AppManager.Instance.ShowInTaskbar) { return; @@ -54,25 +69,59 @@ public class MsgViewModel : MyReactiveObject return; } + _ = DrainQueueMsgAsync(); + } + + private async Task DrainQueueMsgAsync() + { try { await Task.Delay(500).ConfigureAwait(false); - var sb = new StringBuilder(); - while (_queueMsg.TryDequeue(out var line)) + var batch = DequeueBatch(); + if (_updateView != null && !string.IsNullOrEmpty(batch)) { - sb.Append(line); + await _updateView(EViewAction.DispatcherShowMsg, batch); } - - await _updateView?.Invoke(EViewAction.DispatcherShowMsg, sb.ToString()); } finally { Interlocked.Exchange(ref _showLock, 0); } + + if (AppManager.Instance.ShowInTaskbar && !_queueMsg.IsEmpty) + { + TryScheduleDrain(); + } } - private void EnqueueQueueMsg(string msg) + private string DequeueBatch() + { + var sb = new StringBuilder(); + var droppedCount = Interlocked.Exchange(ref _droppedMessageCount, 0); + if (droppedCount > 0) + { + sb.Append($"----- Message queue trimmed: dropped {droppedCount} messages -----{Environment.NewLine}"); + } + + var dequeuedCount = 0; + while (dequeuedCount < MaxFlushMessages + && _queueMsg.TryDequeue(out var line)) + { + Interlocked.Decrement(ref _queuedMessageCount); + sb.Append(line); + dequeuedCount++; + + if (sb.Length >= MaxFlushChars) + { + break; + } + } + + return sb.ToString(); + } + + private bool EnqueueQueueMsg(string msg) { //filter msg if (MsgFilter.IsNotEmpty() && !_lastMsgFilterNotAvailable) @@ -81,23 +130,41 @@ public class MsgViewModel : MyReactiveObject { if (!Regex.IsMatch(msg, MsgFilter)) { - return; + return false; } } catch (Exception ex) { - _queueMsg.Enqueue(ex.Message); + msg = ex.Message; _lastMsgFilterNotAvailable = true; } } - _queueMsg.Enqueue(msg); - if (!msg.EndsWith(Environment.NewLine)) + _queueMsg.Enqueue(NormalizeMessage(msg)); + Interlocked.Increment(ref _queuedMessageCount); + TrimQueuedMessages(); + return true; + } + + private void TrimQueuedMessages() + { + var maxQueuedMessages = AppManager.Instance.ShowInTaskbar + ? MaxQueuedMessagesVisible + : MaxQueuedMessagesHidden; + + while (Volatile.Read(ref _queuedMessageCount) > maxQueuedMessages + && _queueMsg.TryDequeue(out _)) { - _queueMsg.Enqueue(Environment.NewLine); + Interlocked.Decrement(ref _queuedMessageCount); + Interlocked.Increment(ref _droppedMessageCount); } } + private static string NormalizeMessage(string msg) + { + return msg.EndsWith(Environment.NewLine) ? msg : msg + Environment.NewLine; + } + //public void ClearMsg() //{ // _queueMsg.Clear(); diff --git a/v2rayN/v2rayN.Desktop/Views/MsgView.axaml.cs b/v2rayN/v2rayN.Desktop/Views/MsgView.axaml.cs index 78b0a4a1..7a6c86f9 100644 --- a/v2rayN/v2rayN.Desktop/Views/MsgView.axaml.cs +++ b/v2rayN/v2rayN.Desktop/Views/MsgView.axaml.cs @@ -43,24 +43,32 @@ public partial class MsgView : ReactiveUserControl private void ShowMsg(object msg) { - //var lineCount = txtMsg.LineCount; - //if (lineCount > ViewModel?.NumMaxMsg) - //{ - // var cutLine = txtMsg.Document.GetLineByNumber(lineCount - KeepLines); - // txtMsg.Document.Remove(0, cutLine.Offset); - //} - if (txtMsg.LineCount > ViewModel?.NumMaxMsg) - { - ClearMsg(); - } - txtMsg.AppendText(msg.ToString()); + TrimMsg(); if (togScrollToEnd.IsChecked ?? true) { txtMsg.ScrollToEnd(); } } + private void TrimMsg() + { + var maxLines = ViewModel?.NumMaxMsg ?? 500; + if (txtMsg.LineCount <= maxLines || txtMsg.Document == null) + { + return; + } + + var firstKeepLine = txtMsg.LineCount - maxLines + 1; + var cutLine = txtMsg.Document.GetLineByNumber(firstKeepLine); + if (cutLine.Offset <= 0) + { + return; + } + + txtMsg.Document.Remove(0, cutLine.Offset); + } + public void ClearMsg() { txtMsg.Clear(); diff --git a/v2rayN/v2rayN/Views/MsgView.xaml.cs b/v2rayN/v2rayN/Views/MsgView.xaml.cs index bccb4a5e..65c03d2f 100644 --- a/v2rayN/v2rayN/Views/MsgView.xaml.cs +++ b/v2rayN/v2rayN/Views/MsgView.xaml.cs @@ -45,18 +45,32 @@ public partial class MsgView private void ShowMsg(object msg) { - if (txtMsg.LineCount > ViewModel?.NumMaxMsg) - { - ClearMsg(); - } - txtMsg.AppendText(msg.ToString()); + TrimMsg(); if (togScrollToEnd.IsChecked ?? true) { txtMsg.ScrollToEnd(); } } + private void TrimMsg() + { + var maxLines = ViewModel?.NumMaxMsg ?? 500; + if (txtMsg.LineCount <= maxLines) + { + return; + } + + var keepFromCharIndex = txtMsg.GetCharacterIndexFromLineIndex(txtMsg.LineCount - maxLines); + if (keepFromCharIndex <= 0) + { + return; + } + + txtMsg.Select(0, keepFromCharIndex); + txtMsg.SelectedText = string.Empty; + } + public void ClearMsg() { txtMsg.Clear();