.netwpfperformanceloggingresponsiveness

WPF - Displaying Large Number of Log Messages Continously


I am developing an application using WPF. I used Serilog for logging and I have a log sink that displays the log events in a list box. Incoming log events are queued in the Emit method of the log sink and queued log events are processed in the CompositionTarget.Rendering event.

Queuing Log events:

public void EventsLogged(IEnumerable<Event> Events)
        {
            System.Windows.Application.Current?.Dispatcher.Invoke((Action)delegate
                {
                    foreach (var item in Events)
                    {
                        _logMessageQueue.Enqueue(new LogEvent(item));
                    }
                });
        }

Rendering event:

private void CompositionTargetOnRendering(object sender, EventArgs e)
        {
            while (_logMessageQueue.TryDequeue(out var logEvent))
                {
                    Messages.Add(logEvent);
                }          
        }

Messages is an ObservableCollection that is binded to the ItemSource of the list box.

Here is my problem, when there are huge amounts of enqueued log events in a short period of time, UI responsiveness becomes horrible. Is there any suggestions to improve my application? Or any learning material that I can follow would be really nice.

Thank you


Solution

  • Here are a couple of suggestions that should improve the performance:

    public void EventsLogged(IEnumerable<Event> events)
    {
      // Don't block
      Application.Current.Dispatcher.InvokeAsync(
        () =>
        {
          foreach (var item in events)
          {
            _logMessageQueue.Enqueue(new LogEvent(item));
          }
        }, DispatcherPriority.Background);
    }
    
    // In case this member is an event handler, define the method of type 'void' (instead of 'Task')
    public async Task EventsLoggedAsync(IEnumerable<Event> events)
    {
      // Don't block
      await Task.Run(
        () =>
        {
          foreach (var item in events)
          {
            _logMessageQueue.Enqueue(new LogEvent(item));
          }
        });
    }
    
    private void CompositionTargetOnRendering(object sender, EventArgs e)
    {
      while (_logMessageQueue.TryDequeue(out var logEvent))
      {
        // Enqueue task to the Dispatcher queue 
        // and let the Dispatcher decide when to process the tasks by defining a priority
        Application.Current.Dispatcher.InvokeAsync(
          () =>
          {
            Messages.Add(logEvent);
          }, DispatcherPriority.Background);
      }
    }
    

    To further improve the solution I suggest to implement the Producer/Consumer pattern. If you use Channel you can make the complete implementation asynchronous (see Microsoft Docs: System.Threading.Channels library).

    For efficiency, this example also avoids reading from the queue by handling the CompositionTarget.Rendering event. Instead the example will start a loop on a background thread to consume the queue.
    Dispatcher.InvokeAsync is used to control the pressure on the Dispatcher: using DispatcherPriority.Background or DispatcherPriority.ContextIdle should relief the pressure so that the the main thread can continue to render the surface.

    The final solution could look as follows:

    class ChannelExample
    {
      private Channel<LogEvent> LogEventChannel { get; }
    
      public ChannelExample()
      {
        var channelOptions = new UnboundedChannelOptions
        {
          SingleReader = true,
          SingleWriter = false,
        };
    
        this.LogEventChannel = Channel.CreateUnbounded<LogEvent>(channelOptions);
    
        // Start the consumer loop in the background
        Task.Run(DisplayQueuedLogEventsAsync);
      }
    
      // Optional: allows to stop the producer/consumer process 
      // and closes the queue for additional writes.
      // Because this is an application logger, this method is very likely redundant for your scenario
      private void CloseLogEventQueueForWrites()
        => this.LogEventChannel.Writer.Complete();
    
      // Thread-safe implementation allows for concurrency
      public async Task EventsLoggedAsync(IEnumerable<Event> events)
      {
        await Task.Run(
          async () =>
          {
            ChannelWriter<int> logEventChannelWriter = this.LogEventChannel.Writer;
    
            // Consider to use Parallel.ForEach (must be tested because it is not guaranteed that it will improve the performance)
            foreach (Event event in events)
            {
              var logMessage = new LogEvent(event);
    
              while (await logEventChannelWriter.WaitToWriteAsync())
              {
                if (valueChannelWriter.TryWrite(logMessage))
                {
                  // TODO:: Optionally do something after an item 
                  // was successfully written to the channel (e.g. increment counter)
                }
              }
            }
          });
      }
    
      private async Task DisplayQueuedLogEventsAsync()
      {
        ChannelReader<LogEvent> logEventChannelReader = this.LogEventChannel.Reader;
    
        // Asynchronous iteration will continue until the Channel is closed (by calling ChannelWriter.Complete()).
        await foreach (LogEvent logEvent in logEventChannelReader.ReadAllAsync())
        {
          // Use priority Background or ContextIdle 
          // to control and relief the pressure on the Dispatcher
          Application.Current.Dispatcher.InvokeAsync(
            () =>
            {
              Messages.Add(logEvent);
            }, DispatcherPriority.Background);
        }
      }
    }