简体   繁体   English

快速更改的集合MVVM WPF-高CPU使用率和UI几乎冻结

[英]Fast changing collection MVVM WPF - high CPU usage & UI almost freezes

I am developing an app with a datagrid of that displays certain running Windows processes (in my example Chrome processes). 我正在开发一个带有数据网格的应用程序,该应用程序显示某些正在运行的Windows进程(在我的示例Chrome进程中)。 The datagrid is loaded with processes when a checkbox is checked. 选中复选框后,将在数据网格中加载进程。

Requirements: 要求:

  • Display 'live' info for the name, memory usage (private working set) of each process, just like in the Windows Task Manager - Processes tab. 显示每个进程的名称,内存使用情况(专用工作集)的“实时”信息,就像在Windows任务管理器的“进程”选项卡中一样。
  • Monitor for processes that exit and remove them from the datagrid. 监视退出的进程并将其从数据网格中删除。
  • Monitor for certain processes that start. 监视启动的某些进程。

Used techniques: 二手技术:

Issue(s): 发行人:

  • When the processes are loaded, the CPU usage gets very high and the UI almost freezes. 加载进程后,CPU使用率非常高,UI几乎冻结。
  • CPU usage remains high even when the ManagerService.Stop() is called. 即使调用ManagerService.Stop() CPU使用率仍然很高。
  • Sometimes a System.InvalidOperationException - Cannot change ObservableCollection during a CollectionChanged event exception is thrown when a process is removed from the collection. 有时,当从集合中删除某个进程时,将引发System.InvalidOperationException - Cannot change ObservableCollection during a CollectionChanged event异常System.InvalidOperationException - Cannot change ObservableCollection during a CollectionChanged event

How can I fix this issues? 我该如何解决此问题? Also is my approach a 'good practice' one? 我的方法也是一种“良好实践”吗?

Any help would be greatly appreciated! 任何帮助将不胜感激! I've already spent a lot of time on this issue. 我已经在这个问题上花了很多时间。

Update 1 更新1

Didn't help, removing OnRendering() and implementing INotifyPropertyChanged 并没有帮助,删除OnRendering()并实现INotifyPropertyChanged

public class CustomProcess : INotifyPropertyChanged
{
    private double _memory;

    public double Memory
    {
        get { return _memory; }
        set
        {
            if (_memory != value)
            {
                _memory = value;
                OnPropertyChanged(nameof(Memory));
            }
        }
    }


    private bool _isChecked;

    public bool IsChecked
    {
        get { return _isChecked; }
        set
        {
            if (_isChecked != value)
            {
                _isChecked = value;
                OnPropertyChanged(nameof(IsChecked));
    }
}

Update 2 更新2

Following Evk advice I've updated 按照Evk的建议,我已更新

  • Used regular ObservableCollection 使用常规的ObservableCollection
  • moved timer to viewmodel 将计时器移至视图模型

CPU usage is much lower now. 现在,CPU使用率要低得多。 However I sometimes get an Process with an ID of ... is not running exception in the OnProcessStarted() 但是我有时会得到一个Process with an ID of ... is not runningProcess with an ID of ... is not runningOnProcessStarted() Process with an ID of ... is not running异常 在此处输入图片说明

Viewmodel 视图模型

public class MainViewModel 
    {
        System.Threading.Timer timer;
        private ObservableCollection<CustomProcess> _processes;
        public ObservableCollection<CustomProcess> Processes
        {
            get
            {
                if (_processes == null)
                    _processes = new ObservableCollection<CustomProcess>();

                return _processes;
            }
        }
        private void OnBooleanChanged(PropertyChangedMessage<bool> propChangedMessage)
        {
            if (propChangedMessage.NewValue == true)
            {
                _managerService.Start(_processes);
                timer = new System.Threading.Timer(OnTimerTick, null, 0, 200); //every 200ms
                ProcessesIsVisible = true;
            }
            else
            {
                timer.Dispose();
                _managerService.Stop();
                ProcessesIsVisible = false;
            }
        }
        private void OnTimerTick(object state)
        {
            try
            {
                for (int i = 0; i < Processes.Count; i++)
                    Processes[i].UpdateMemory();
            }
            catch (Exception)
            {

            }
        }

Model 模型

public class CustomProcess : INotifyPropertyChanged
    {    
        public void UpdateMemory()
        {
            if (!ProcessObject.HasExited)
                Memory = Process.GetProcessById(ProcessObject.Id).PagedMemorySize64;
        }
        private double _memory;

        public double Memory
        {
            get { return _memory; }
            set
            {
                if (_memory != value)
                {
                    _memory = value;
                    OnPropertyChanged(nameof(Memory));
                }
            }
        }

Service 服务

        private void OnProcessNotification(NotificationMessage<Process> notMessage)
        {
            if (notMessage.Notification == "exited")
            {
                _processes.Remove(p => p.ProcessObject.Id == notMessage.Content.Id, DispatcherHelper.UIDispatcher);
            }

        }

Original code 原始码

XAML XAML

<DataGrid ItemsSource="{Binding Processes}">
   <DataGridTextColumn Header="Process name"
                            Binding="{Binding ProcessObject.ProcessName}"
                            IsReadOnly='True'
                            Width='Auto' />
        <DataGridTextColumn Header="PID"
                            Binding="{Binding ProcessObject.Id}"
                            IsReadOnly='True'
                            Width='Auto' />
        <DataGridTextColumn Header="Memory"
                            Binding='{Binding Memory}'
                            IsReadOnly='True'
                            Width='Auto' />
</DataGrid>

XAML Code behind 后面的XAML代码

public MainWindow()
{
        InitializeComponent();
        DataContext = SimpleIoc.Default.GetInstance<MainViewModel>();
        CompositionTarget.Rendering += OnRendering;
    }

    private void OnRendering(object sender, EventArgs e)
    {
        if (DataContext is IRefresh)
            ((IRefresh)DataContext).Refresh();
    }
}

ViewModel 视图模型

public class MainViewModel : Shared.ViewModelBase, IRefresh
{
    private AsyncObservableCollection<CustomProcess> _processes;
    public AsyncObservableCollection<CustomProcess> Processes
    {
        get
        {
            if (_processes == null)
                _processes = new AsyncObservableCollection<CustomProcess>();

            return _processes;
        }
    }
    private readonly IManagerService _managerService;

    public MainViewModel(IManagerService managerService)
    {
        _managerService = managerService;
        Messenger.Default.Register<PropertyChangedMessage<bool>>(this, OnBooleanChanged);
    }      

    #region PropertyChangedMessage
    private void OnBooleanChanged(PropertyChangedMessage<bool> propChangedMessage)
    {
        if (propChangedMessage.NewValue == true)
        {
            _managerService.Start(_processes);
        }
        else
        {
            _managerService.Stop();
        }
    }

    public void Refresh()
    {
        foreach (var process in Processes)
            RaisePropertyChanged(nameof(process.Memory)); //notify UI that the property has changed
    }

Service 服务

public class ManagerService : IManagerService
{
    AsyncObservableCollection<CustomProcess> _processes;
    ManagementEventWatcher managementEventWatcher;

    public ManagerService()
    {
        Messenger.Default.Register<NotificationMessage<Process>>(this, OnProcessNotification);
    }

    private void OnProcessNotification(NotificationMessage<Process> notMessage)
    {
        if (notMessage.Notification == "exited")
        {
            //a process has exited. Remove it from the collection
            _processes.Remove(p => p.ProcessObject.Id == notMessage.Content.Id);
        }

    }

    /// <summary>
    /// Starts the manager. Add processes and monitor for starting processes
    /// </summary>
    /// <param name="processes"></param>
    public void Start(AsyncObservableCollection<CustomProcess> processes)
    {
        _processes = processes;
        _processes.CollectionChanged += OnCollectionChanged;

        foreach (var process in Process.GetProcesses().Where(p => p.ProcessName.Contains("chrome")))
            _processes.Add(new CustomProcess(process));

        MonitorStartedProcess();
        Task.Factory.StartNew(() => MonitorLogFile());
    }

    /// <summary>
    /// Stops the manager.
    /// </summary>
    public void Stop()
    {       
        _processes.CollectionChanged -= OnCollectionChanged;
        managementEventWatcher = null;
        _processes = null;
    }

    private void MonitorLogFile()
    {
        //this code monitors a log file for changes. It is possible that the IsChecked property of a CustomProcess object is set in the Processes collection
    }

    /// <summary>
    /// Monitor for started Chrome
    /// </summary>
    private void MonitorStartedProcess()
    {
        var qStart = "SELECT * FROM Win32_ProcessStartTrace WHERE ProcessName like '%chrome%'";
        ManagementEventWatcher managementEventWatcher = new ManagementEventWatcher(new WqlEventQuery(qStart));
        managementEventWatcher.EventArrived += new EventArrivedEventHandler(OnProcessStarted);
        try
        {
            managementEventWatcher.Start();
        }
        catch (Exception)
        {

        }
    }



    private void OnProcessStarted(object sender, EventArrivedEventArgs e)
    {

        try
        {
            int pid = Convert.ToInt32(e.NewEvent.Properties["ProcessID"].Value);
            _processes.Add(new CustomProcess(Process.GetProcessById(pid)));  //add to collection
        }
        catch (Exception)
        {

        }

    }

Model 模型

public class CustomProcess
{        
    public Process ProcessObject { get; }

    public CustomProcess(Process process)
    {
        ProcessObject = process;
        try
        {
            ProcessObject.EnableRaisingEvents = true;
            ProcessObject.Exited += ProcessObject_Exited;
            Task.Factory.StartNew(() => UpdateMemory());
        }
        catch (Exception)
        {

        }

    }

    private void ProcessObject_Exited(object sender, EventArgs e)
    {
        Process process = sender as Process;
        NotificationMessage<Process> notMessage = new NotificationMessage<Process>(process, "exited");
        Messenger.Default.Send(notMessage); //send a notification that the process has exited
    }

    private void UpdateMemory()
    {
        while (!ProcessObject.HasExited)
        {
            try
            {
                Memory = Process.GetProcessById(ProcessObject.Id).PagedMemorySize64;
            }
            catch (Exception)
            {

            }
        }
    }

    private double _memory;

    public double Memory
    {
        get { return _memory; }
        set
        {
            if (_memory != value)
            {
                _memory = value;
            }
        }
    }


    private bool _isChecked;

    public bool IsChecked
    {
        get { return _isChecked; }
        set
        {
            if (_isChecked != value)
            {
                _isChecked = value;
            }
        }
    }

Writing to a GUI is expensive. 写入GUI非常昂贵。 If you only do it once per user triggered event you will not notice it. 如果每个用户触发的事件仅执行一次,您将不会注意到它。 But once you write from any kind of loop - including a loop running on another thread - you will notice it. 但是,一旦您从任何类型的循环(包括在另一个线程上运行的循环)进行写入,您都会注意到它。 I even wrote some example code for Windows Forms to showcase this: 我什至为Windows Forms编写了一些示例代码来说明这一点:

using System;
using System.Windows.Forms;

namespace UIWriteOverhead
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        int[] getNumbers(int upperLimit)
        {
            int[] ReturnValue = new int[upperLimit];

            for (int i = 0; i < ReturnValue.Length; i++)
                ReturnValue[i] = i;

            return ReturnValue;
        }

        void printWithBuffer(int[] Values)
        {
            textBox1.Text = "";
            string buffer = "";

            foreach (int Number in Values)
                buffer += Number.ToString() + Environment.NewLine;
            textBox1.Text = buffer;
        }

        void printDirectly(int[] Values){
            textBox1.Text = "";

            foreach (int Number in Values)
                textBox1.Text += Number.ToString() + Environment.NewLine;
        }

        private void btnPrintBuffer_Click(object sender, EventArgs e)
        {
            MessageBox.Show("Generating Numbers");
            int[] temp = getNumbers(10000);
            MessageBox.Show("Printing with buffer");
            printWithBuffer(temp);
            MessageBox.Show("Printing done");
        }

        private void btnPrintDirect_Click(object sender, EventArgs e)
        {
            MessageBox.Show("Generating Numbers");
            int[] temp = getNumbers(1000);
            MessageBox.Show("Printing directly");
            printDirectly(temp);
            MessageBox.Show("Printing done");
        }
    }
}

Your code is even slightly worse, as you allow the Update and thus Layout code to run between each update. 您的代码甚至稍差一些,因为您允许在每次更新之间运行Update和Layout代码。 While it does keep the UI responsive, it is more code to run. 尽管它确实可以使UI保持响应,但要运行的代码更多。

You will not get around limiting the updates. 您将无法限制更新。 I would put these kinds of Limitations clearly on the View Side. 我将这些限制明确地放在“查看侧”上。 Personally I prefer this way: 我个人更喜欢这样:

  1. Do not register the Change Notificaiton events realted to the Observable collection 不要注册变为Observable集合的Change Notificaiton事件
  2. Make a timer that regularly updates the UI with the current value of the Collection. 设置一个计时器,以使用Collection的当前值定期更新UI。 Set the timer to something like 60 Updates per second. 将计时器设置为每秒60次更新。 That should be fast enough for humans. 对于人类来说,那应该足够快。
  3. You may want to add some form of Locking to the code writing the Collection and the accessor code to avoid race conditions. 您可能需要在编写Collection和访问者代码的代码中添加某种形式的Locking以避免竞争情况。

A few side notes: 一些注意事项:

A pet Peeve of mine is Exception Hanlding. 我的宠儿Peeve是Exception Hanlding。 And I see some swallowing of Fatal Exceptions there. 我在那里看到一些致命异常。 You really should fix that ASAP. 您确实应该尽快解决该问题。 It is bad enough that Threads can accidentally swallow exceptions, you should not write additional code for this. 线程可能会意外吞下异常,这非常糟糕,您不应该为此编写其他代码。 Here are two articles I link a lot: http://blogs.msdn.com/b/ericlippert/archive/2008/09/10/vexing-exceptions.aspx | 这是我经常链接的两篇文章: http : //blogs.msdn.com/b/ericlippert/archive/2008/09/10/vexing-exceptions.aspx | http://www.codeproject.com/Articles/9538/Exception-Handling-Best-Practices-in-NET http://www.codeproject.com/Articles/9538/Exception-Handling-Best-Practices-in-NET

Secondly, ObservableColelctions are notoriously bad with complete reworks. 其次,众所周知,ObservableColelctions具有完全的返工性。 It lacks a add-range Function. 它缺少添加范围功能。 So every single change will trigger an update. 因此,每次更改都会触发更新。 My usual workaround is: 1. Give the property exposing the Collection Change Notification 2. Do not work with the exposed collection on any update. 我通常的解决方法是:1.给属性公开“集合更改通知”。2.在任何更新上都不要使用公开的集合。 3. Instead work with a background collection. 3.而是使用后台集合。 Only when this new state is finished, do you expose it. 仅当此新状态完成时,才公开它。

Instead of you updating/refresing the UI yourself, make use of the WPF change notification system achieved using DataBinding & PropertyChanged event. 不用您自己更新/刷新UI,而是利用通过DataBindingPropertyChanged事件实现的WPF更改通知系统。

As MSDN quotes - 正如MSDN所言-

The INotifyPropertyChanged interface is used to notify clients, typically binding clients, that a property value has changed. INotifyPropertyChanged接口用于通知客户端(通常是绑定客户端)属性值已更改。

For example, consider a Person object with a property called FirstName . 例如,考虑一个具有名为FirstName的属性的Person对象。 To provide generic property-change notification, the Person type implements the INotifyPropertyChanged interface and raises a PropertyChanged event when FirstName is changed. 为了提供通用的属性更改通知, Person类型实现INotifyPropertyChanged接口,并在更改FirstName时引发PropertyChanged事件。

More details here . 更多细节在这里

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM