Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I increase performance of FlowDocumentScrollViewer?

In a previous question, I asked how to get real-time logging output in a WPF textbox-like element (WPF append text blocks UI thread heavily but WinForms doesn't?). The answer there led me to using a FlowDocumentScrollViewer, which indeed was much faster than RichTextBox. However, I've found that running commands that have a massive amount of text output (like 'svn co') leads to a noticeable slowdown in my WPF application. Switching tabs after checking out 3 or 4 very large svn branches takes 3-4 seconds, and I'm sure the time would go up with the number of checkouts I do. Scrolling also has noticeable lag.

As stated in the question I linked above, I switched my application from Windows Forms to WPF recently. I like WPF a lot - it gives many advantages I didn't have in Forms. However, performance seems to be quite an issue in WPF, at least for me. In the Forms version of my application, I could print out massive amounts of text to the RichTextBox control and had no slowdown at all in my app. Switching tabs was instant, and scrolling was seamless. This is the experience I want in my WPF app.

So my question is this: How can I improve the performance of my FlowDocumentScrollViewer to match the performance of the Windows Forms RichTextBox, without losing formatting capabilities like bold and italic, and without losing copy/paste functionality? I'm willing to switch WPF controls as long as they offer the formatting capabilities I'm looking for.

Here's my printing code, for reference:

public void PrintOutput(String s)
{
    if (outputParagraph.FontSize != defaultFontSize)
    {
        outputParagraph = new Paragraph();
        outputParagraph.Margin = new Thickness(0);
        outputParagraph.FontFamily = font;
        outputParagraph.FontSize = defaultFontSize;
        outputParagraph.TextAlignment = TextAlignment.Left;
        OutputBox.Document.Blocks.Add(outputParagraph);
    }
    outputParagraph.Inlines.Add(s);
    if (!clearOutputButton.IsEnabled) clearOutputButton.IsEnabled = true;
}

public void PrintImportantOutput(String s)
{
    if (outputParagraph.FontSize != importantFontSize)
    {
        outputParagraph = new Paragraph();
        outputParagraph.Margin = new Thickness(0);
        outputParagraph.FontFamily = font;
        outputParagraph.FontSize = importantFontSize;
        outputParagraph.TextAlignment = TextAlignment.Left;
        OutputBox.Document.Blocks.Add(outputParagraph);
    }
    String timestamp = DateTime.Now.ToString("[hh:mm.ss] ");
    String toPrint = timestamp + s;
    outputParagraph.Inlines.Add(new Bold(new Run(toPrint)));
    if (!clearOutputButton.IsEnabled) clearOutputButton.IsEnabled = true;
}

I switch font sizes and make text bold when printing "important" text. The reason this code is so many lines is because I'm trying to re-use the same paragraph for all text until I hit "important" text; I add a new paragraph with all the "important" text, then add another paragraph once I switch back to the non-important text and append to that paragraph until I hit more "important" text. I was hoping that re-using the same paragraph would improve performance.

Also, it should be noted that I'm printing stdout to one FlowDocumentScrollViewer, stderr to another FlowDocumentScrollViewer, and both at once to a third FlowDocumentScrollViewer. So each line of stdout and stderr technically gets printed twice, doubling the load on my app. Again, this wasn't an issue in WinForms.


Below is a full code sample, as requested in the comments. It is extremely simple (3 FlowDocumentScrollViewer's and simple printing) but still slows down big time around 20000 lines of text, and much worse past that.

EDIT: The code sample has been removed. In its place is the working code to solve my performance issues. It works just as FlowDocumentScrollViewer would, with one exception: you can't select substrings of lines. I'm looking into fixing that, although it seems difficult.

Bridge.cs


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Documents;
using System.Threading;
using System.Collections.Concurrent;
using System.Threading.Tasks;

namespace PerformanceTest
{
    public class Bridge
    {
        int counterLimit;

        public BlockingCollection<PrintInfo> output;
        public BlockingCollection<PrintInfo> errors;
        public BlockingCollection<PrintInfo> logs;


        protected static Bridge controller = new Bridge();

        public static Bridge Controller
        {
            get
            {
                return controller;
            }
        }

        public MainWindow Window
        {
            set
            {
                if (value != null)
                {
                    output = value.outputProducer;
                    errors = value.errorProducer;
                    logs = value.logsProducer;
                }
            }
        }

        public bool Running
        {
            get;
            set;
        }

        private Bridge()
        {
            //20000 lines seems to slow down tabbing enough to prove my point.
            //increase this number to get even worse results.
            counterLimit = 40000;
        }

        public void PrintLotsOfText()
        {
            new Thread(new ThreadStart(GenerateOutput)).Start();
            new Thread(new ThreadStart(GenerateError)).Start();
        }

        private void GenerateOutput()
        {
            //There is tons of output text, so print super fast if possible.
            int counter = 1;
            while (Running && counter < counterLimit)
            {
                if (counter % 10 == 0)
                    PrintImportantOutput("I will never say this horrible word again as long I live. This is confession #" + counter++ + ".");
                else
                    PrintOutput("I will never say this horrible word again as long I live. This is confession #" + counter++ + ".");
                //Task.Delay(1).Wait();
            }
            Console.WriteLine("GenerateOutput thread should end now...");
        }

        private void GenerateError()
        {
            int counter = 1;
            while (Running && counter < counterLimit)
            {
                PrintError("I will never forgive your errors as long I live. This is confession #" + counter++ + ".");
                //Task.Delay(1).Wait();
            }
            Console.WriteLine("GenerateError thread should end now...");
        }

        #region Printing
        delegate void StringArgDelegate(String s);
        delegate void InlineArgDelegate(Inline inline);
        public void PrintOutput(String s)
        {
            output.TryAdd(new PrintInfo(s, false));
            PrintLog("d " + s);
        }

        public void PrintImportantOutput(String s)
        {
            output.TryAdd(new PrintInfo(s, true));
            PrintLog("D " + s);
        }

        public void PrintError(String s)
        {
            errors.TryAdd(new PrintInfo(s, false));
            PrintLog("e " + s);
        }

        public void PrintImportantError(String s)
        {
            errors.TryAdd(new PrintInfo(s, true));
            PrintLog("E " + s);
        }

        public void PrintLog(String s)
        {
            logs.TryAdd(new PrintInfo(s, false));
        }
        #endregion
    }

    public class PrintInfo
    {
        public String Text { get; set; }
        public bool IsImportant { get; set; }

        public PrintInfo() { }
        public PrintInfo(String text, bool important)
        {
            Text = text;
            IsImportant = important;
        }
    }
}

MainWindow.xaml


<Window x:Class="PerformanceTest.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow"
        xmlns:l="clr-namespace:PerformanceTest"
        WindowStartupLocation="CenterScreen">
  <Grid>
    <TabControl>
      <TabControl.Resources>
        <Style TargetType="ListBox">
          <Setter Property="TextElement.FontFamily" Value="Consolas" />
          <Setter Property="TextElement.FontSize" Value="12" />
          <Setter Property="VirtualizingStackPanel.IsVirtualizing" Value="True" />
          <Setter Property="VirtualizingStackPanel.VirtualizationMode" Value="Recycling" />
          <Setter Property="l:ListBoxSelector.Enabled" Value="True" />
          <Setter Property="ContextMenu">
            <Setter.Value>
              <ContextMenu>
                <MenuItem Command="Copy" />
              </ContextMenu>
            </Setter.Value>
          </Setter>
        </Style>
        <Style TargetType="ListBoxItem">
          <Setter Property="HorizontalAlignment" Value="Left" />
          <Setter Property="Margin" Value="0" />
          <Setter Property="Template">
            <Setter.Value>
              <ControlTemplate TargetType="ListBoxItem">
                <TextBlock Text="{Binding Text}" TextWrapping="Wrap"
                    Background="{TemplateBinding Background}"
                    Foreground="{TemplateBinding Foreground}"
                    FontWeight="{TemplateBinding FontWeight}" />
                <ControlTemplate.Triggers>
                  <DataTrigger Binding="{Binding IsImportant}" Value="true">
                    <Setter Property="TextElement.FontWeight" Value="SemiBold" />
                    <Setter Property="TextElement.FontSize" Value="14" />
                  </DataTrigger>
                  <Trigger Property="IsSelected" Value="true">
                    <Setter Property="Background" Value="{StaticResource {x:Static SystemColors.HighlightBrushKey}}" />
                    <Setter Property="Foreground" Value="{StaticResource {x:Static SystemColors.HighlightTextBrushKey}}" />
                  </Trigger>
                </ControlTemplate.Triggers>
              </ControlTemplate>
            </Setter.Value>
          </Setter>
        </Style>
      </TabControl.Resources>
      <TabItem Header="Bridge">
        <StackPanel Orientation="Vertical" HorizontalAlignment="Left">
          <Button Content="Start Test" Click="StartButton_Click" />
          <Button Content="End Test" Click="EndButton_Click" />
        </StackPanel>
      </TabItem>
      <TabItem Header="Output">
        <Grid>
          <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="Auto" />
            <ColumnDefinition Width="*" />
          </Grid.ColumnDefinitions>
          <!--<RichTextBox x:Name="OutputBox" ScrollViewer.VerticalScrollBarVisibility="Auto"/>-->
          <ListBox Grid.Column="0" ItemsSource="{Binding Output,RelativeSource={RelativeSource FindAncestor,AncestorType=Window}}"
                   ScrollViewer.HorizontalScrollBarVisibility="Disabled">
            <ListBox.CommandBindings>
              <CommandBinding Command="Copy" Executed="CopyExecuted" />
            </ListBox.CommandBindings>
          </ListBox>

          <GridSplitter Grid.Column="1" Width="5" ResizeBehavior="PreviousAndNext" />
          <ListBox Grid.Column="2" ItemsSource="{Binding Errors,RelativeSource={RelativeSource FindAncestor,AncestorType=Window}}" 
                   ScrollViewer.HorizontalScrollBarVisibility="Disabled">
            <ListBox.CommandBindings>
              <CommandBinding Command="Copy" Executed="CopyExecuted" />
            </ListBox.CommandBindings>
          </ListBox>
        </Grid>

      </TabItem>
      <TabItem Header="Log">
        <Grid>
          <ListBox Grid.Column="0" ItemsSource="{Binding Logs,RelativeSource={RelativeSource FindAncestor,AncestorType=Window}}" 
                   ScrollViewer.HorizontalScrollBarVisibility="Disabled">
            <ListBox.CommandBindings>
              <CommandBinding Command="Copy" Executed="CopyExecuted" />
            </ListBox.CommandBindings>
          </ListBox>
        </Grid>
      </TabItem>
    </TabControl>
  </Grid>
</Window>

MainWindow.xaml.cs


using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Windows.Threading;
using System.Threading;

namespace PerformanceTest
{
    /// 
    /// Interaction logic for MainWindow.xaml
    /// 
    public partial class MainWindow : Window
    {
        public BlockingCollection<PrintInfo> outputProducer = new BlockingCollection<PrintInfo>();
        public BlockingCollection<PrintInfo> errorProducer = new BlockingCollection<PrintInfo>();
        public BlockingCollection<PrintInfo> logsProducer = new BlockingCollection<PrintInfo>();

        public ObservableCollection<PrintInfo> Output { get; set; }
        public ObservableCollection<PrintInfo> Errors { get; set; }
        public ObservableCollection<PrintInfo> Logs { get; set; }

        protected FontFamily font = new FontFamily("Consolas");
        protected int defaultFontSize = 12;
        protected int importantFontSize = 14;

        Dispatcher dispatcher;

        public MainWindow()
        {
            Bridge.Controller.Window = this;
            try
            {
                InitializeComponent();
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.InnerException.ToString());
                Console.WriteLine(ex.StackTrace);
            }

            dispatcher = Dispatcher;
            Output = new ObservableCollection<PrintInfo>();
            Errors = new ObservableCollection<PrintInfo>();
            Logs = new ObservableCollection<PrintInfo>();


            new Thread(new ThreadStart(() => Print(outputProducer, Output))).Start();
            new Thread(new ThreadStart(() => Print(errorProducer, Errors))).Start();
            new Thread(new ThreadStart(() => Print(logsProducer, Logs))).Start();
        }

        public delegate void EmptyDelegate();

        public void Print(BlockingCollection<PrintInfo> producer, ObservableCollection<PrintInfo> target)
        {
            try
            {
                foreach (var info in producer.GetConsumingEnumerable())
                {
                    dispatcher.Invoke(new EmptyDelegate(() =>
                    {
                        if (info.IsImportant)
                        {
                            String timestamp = DateTime.Now.ToString("[hh:mm.ss] ");
                            String toPrint = timestamp + info.Text;
                            info.Text = toPrint;
                        }
                        target.Add(info);
                    }), DispatcherPriority.Background);
                }
            }
            catch (TaskCanceledException)
            {
                //window closing before print finish
            }
        }

        private void StartButton_Click(object sender, RoutedEventArgs e)
        {
            if (!Bridge.Controller.Running)
            {
                Bridge.Controller.Running = true;
                Bridge.Controller.PrintLotsOfText();
            }
        }

        private void EndButton_Click(object sender, RoutedEventArgs e)
        {
            Bridge.Controller.Running = false;
        }

        private void CopyExecuted(object sender, ExecutedRoutedEventArgs e)
        {
            ListBox box = sender as ListBox;

            HashSet<PrintInfo> allItems = new HashSet<PrintInfo>(box.Items.OfType<PrintInfo>());
            HashSet<PrintInfo> selectedItems = new HashSet<PrintInfo>(box.SelectedItems.OfType<PrintInfo>());

            IEnumerable<PrintInfo> sortedItems = allItems.Where(i => selectedItems.Contains(i));
            IEnumerable<String> copyItems = from i in sortedItems select i.Text;

            string log = string.Join("\r\n", copyItems);
            Clipboard.SetText(log);
        }
    }
}

ListBoxSelector.cs is in @pushpraj's answer.

like image 258
Darkhydro Avatar asked Jul 30 '14 18:07

Darkhydro


2 Answers

I execute the sample you've provided, apart from few threading issues the biggest issue is amount of data. which slows down the text rendering as the it grows.

I tried to rewrite your code in a different way. I used Tasks, BlockingCollection and Virtualization to improve the performance with assumptions that main interest of the application is logging speed.

Bridge.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Documents;
using System.Threading;
using System.Collections.Concurrent;
using System.Threading.Tasks;

namespace PerformanceTest
{
    public class Bridge
    {
        int counterLimit;

        public BlockingCollection<string> output;
        public BlockingCollection<string> impOutput;
        public BlockingCollection<string> errors;
        public BlockingCollection<string> impErrors;
        public BlockingCollection<string> logs;


        protected static Bridge controller = new Bridge();

        public static Bridge Controller
        {
            get
            {
                return controller;
            }
        }

        public MainWindow Window
        {
            set
            {
                if (value != null)
                {
                    output = value.outputProducer;
                    impOutput = value.impOutputProducer;
                    errors = value.errorProducer;
                    impErrors = value.impErrorProducer;
                    logs = value.logsProducer;
                }
            }
        }

        public bool Running
        {
            get;
            set;
        }

        private Bridge()
        {
            //20000 lines seems to slow down tabbing enough to prove my point.
            //increase this number to get even worse results.
            counterLimit = 40000;
        }

        public void PrintLotsOfText()
        {
            Task.Run(() => GenerateOutput());
            Task.Run(() => GenerateError());
        }

        private void GenerateOutput()
        {
            //There is tons of output text, so print super fast if possible.
            int counter = 1;
            while (Running && counter < counterLimit)
            {
                PrintOutput("I will never say this horrible word again as long I live. This is confession #" + counter++ + ".");
                //Task.Delay(1).Wait();
            }
        }

        private void GenerateError()
        {
            int counter = 1;
            while (Running && counter < counterLimit)
            {
                PrintError("I will never forgive your errors as long I live. This is confession #" + counter++ + ".");
                //Task.Delay(1).Wait();
            }
        }

        #region Printing
        delegate void StringArgDelegate(String s);
        delegate void InlineArgDelegate(Inline inline);
        public void PrintOutput(String s)
        {
            output.TryAdd(s);
            PrintLog("d " + s);
        }

        public void PrintImportantOutput(String s)
        {
            impOutput.TryAdd(s);
            PrintLog("D " + s);
        }

        public void PrintError(String s)
        {
            errors.TryAdd(s);
            PrintLog("e " + s);
        }

        public void PrintImportantError(String s)
        {
            impErrors.TryAdd(s);
            PrintLog("E " + s);
        }

        public void PrintLog(String s)
        {
            String text = s;
            logs.TryAdd(text);
        }
        #endregion
    }
}

MainWindow.xaml

<Window x:Class="PerformanceTest.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow"
        WindowStartupLocation="CenterScreen">
    <Grid>
        <TabControl>
            <TabControl.Resources>
                <Style TargetType="ListBox">
                    <Setter Property="TextElement.FontFamily"
                            Value="Consolas" />
                    <Setter Property="TextElement.FontSize"
                            Value="12" />
                    <Setter Property="VirtualizingPanel.IsVirtualizing"
                            Value="True" />
                    <Setter Property="VirtualizingPanel.VirtualizationMode"
                            Value="Recycling" />
                </Style>
            </TabControl.Resources>
            <TabItem Header="Bridge">
                <StackPanel Orientation="Vertical"
                            HorizontalAlignment="Left">
                    <Button Content="Start Test"
                            Click="StartButton_Click" />
                    <Button Content="End Test"
                            Click="EndButton_Click" />
                </StackPanel>
            </TabItem>
            <TabItem Header="Output">
                <Grid>
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="*" />
                        <ColumnDefinition Width="Auto" />
                        <ColumnDefinition Width="*" />
                    </Grid.ColumnDefinitions>
                    <ListBox Grid.Column="0"
                             ItemsSource="{Binding Output,RelativeSource={RelativeSource FindAncestor,AncestorType=Window}}" />

                    <GridSplitter Grid.Column="1"
                                  Width="5"
                                  ResizeBehavior="PreviousAndNext" />
                    <ListBox Grid.Column="2"
                             ItemsSource="{Binding Errors,RelativeSource={RelativeSource FindAncestor,AncestorType=Window}}" />
                </Grid>

            </TabItem>
            <TabItem Header="Log">
                <Grid>
                    <ListBox Grid.Column="0"
                             ItemsSource="{Binding Logs,RelativeSource={RelativeSource FindAncestor,AncestorType=Window}}" />
                </Grid>
            </TabItem>
        </TabControl>
    </Grid>
</Window>

MainWindow.xaml.cs

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Windows.Threading;

namespace PerformanceTest
{
    /// 
    /// Interaction logic for MainWindow.xaml
    /// 
    public partial class MainWindow : Window
    {
        public BlockingCollection<string> outputProducer = new BlockingCollection<string>();
        public BlockingCollection<string> impOutputProducer = new BlockingCollection<string>();
        public BlockingCollection<string> errorProducer = new BlockingCollection<string>();
        public BlockingCollection<string> impErrorProducer = new BlockingCollection<string>();
        public BlockingCollection<string> logsProducer = new BlockingCollection<string>();

        public ObservableCollection<object> Output { get; set; }
        public ObservableCollection<object> Errors { get; set; }
        public ObservableCollection<object> Logs { get; set; }

        Dispatcher dispatcher;

        public MainWindow()
        {
            Bridge.Controller.Window = this;
            try
            {
                InitializeComponent();
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.InnerException.ToString());
                Console.WriteLine(ex.StackTrace);
            }

            dispatcher = Dispatcher;
            Output = new ObservableCollection<object>();
            Errors = new ObservableCollection<object>();
            Logs = new ObservableCollection<object>();
            Task.Run(() => Print(outputProducer, Output));
            Task.Run(() => Print(errorProducer, Errors));
            Task.Run(() => Print(logsProducer, Logs));
        }

        public void Print(BlockingCollection<string> producer, ObservableCollection<object> target)
        {
            try
            {
                foreach (var str in producer.GetConsumingEnumerable())
                {
                    dispatcher.Invoke(() =>
                    {
                        target.Insert(0, str);
                    }, DispatcherPriority.Background);
                }
            }
            catch (TaskCanceledException)
            {
                //window closing before print finish
            }
        }

        private void StartButton_Click(object sender, RoutedEventArgs e)
        {
            if (!Bridge.Controller.Running)
            {
                Bridge.Controller.Running = true;
                Bridge.Controller.PrintLotsOfText();
            }
        }

        private void EndButton_Click(object sender, RoutedEventArgs e)
        {
            Bridge.Controller.Running = false;
        }
    }
}

For a full working sample download PerformanceTest.zip and see if this is close to what you need. I have only rewritten only some portion. If this example is heading to the desired direction, we can implement rest of the functionality. un-comment Task.Delay(1).Wait(); in Bridge.cs if you may want to slow down the production to see the mixed logs otherwise the logs generation is too fast so it appear one after other in logs tab.

like image 103
pushpraj Avatar answered Nov 02 '22 09:11

pushpraj


Using FlowDocumentPageViewer will help the performance since it loads document asynchronously.

like image 1
Ekk Avatar answered Nov 02 '22 08:11

Ekk