The reason behind slow performance in WPF
Asked Answered
C

3

17

I'm creating a large number of texts in WPF using DrawText and then adding them to a single Canvas.

I need to redraw the screen in each MouseWheel event and I realized that the performance is a bit slow, so I measured the time the objects are created and it was less than 1 milliseconds!

So what could be the problem? A long time ago I guess I read somewhere that it actually is the Rendering that takes the time, not creating and adding the visuals.

Here is the code I'm using to create the text objects, I've only included the essential parts:

public class ColumnIdsInPlan : UIElement
    {
    private readonly VisualCollection _visuals;
    public ColumnIdsInPlan(BaseWorkspace space)
    {
        _visuals = new VisualCollection(this);

        foreach (var column in Building.ModelColumnsInTheElevation)
        {
            var drawingVisual = new DrawingVisual();
            using (var dc = drawingVisual.RenderOpen())
            {
                var text = "C" + Convert.ToString(column.GroupId);
                var ft = new FormattedText(text, cultureinfo, flowdirection,
                                           typeface, columntextsize, columntextcolor,
                                           null, TextFormattingMode.Display)
                {
                    TextAlignment = TextAlignment.Left
                };

                // Apply Transforms
                var st = new ScaleTransform(1 / scale, 1 / scale, x, space.FlipYAxis(y));
                dc.PushTransform(st);

                // Draw Text
                dc.DrawText(ft, space.FlipYAxis(x, y));
            }
            _visuals.Add(drawingVisual);
        }
    }

    protected override Visual GetVisualChild(int index)
    {
        return _visuals[index];
    }

    protected override int VisualChildrenCount
    {
        get
        {
            return _visuals.Count;
        }
    }
}

And this code is run each time the MouseWheel event is fired:

var columnsGroupIds = new ColumnIdsInPlan(this);
MyCanvas.Children.Clear();
FixedLayer.Children.Add(columnsGroupIds);

What could be the culprit?

I'm also having trouble while panning:

    private void Workspace_MouseMove(object sender, MouseEventArgs e)
    {
        MousePos.Current = e.GetPosition(Window);
        if (!Window.IsMouseCaptured) return;
        var tt = GetTranslateTransform(Window);
        var v = Start - e.GetPosition(this);
        tt.X = Origin.X - v.X;
        tt.Y = Origin.Y - v.Y;
    }
Carilla answered 9/6, 2014 at 11:25 Comment(14)
you shouldn't use draw text. what you should do is apply a template to a Label or Textblock and put it into an ItemControl and just feed a string array to it so it get drawn automatically.Mchenry
@Mchenry Thanks Frank, what about the position and rotation of each text?Carilla
it is all in template you do that. WPF allow you to do ScaleTransform to flip X,Y, Z( in case of 3dviewport for that last one), Rotation i can't recall what is the name of the Tag to use but it's in the Transform Group that's for sure. Position wise simple offset on the margin/padding base on a property of the databinded object is enought.Mchenry
Why are you are clearing out and completely rebuilding your canvas on each wheel event? What is it you actually want to happen on the wheel event?Thi
@MikeStrobel I'm using it to Zoom and I need to keep the size of the text the same when I zoom, so each time I'm redrawing it with a new text size based on the scale level.Carilla
@Mchenry Thanks Franck, I'll give it a try, do you have any examples similar to this situation?Carilla
this is a simple tutorial example on the item control with simple templating of the items inside. Big picture the ItemControl hold the collection of items, and then repeat the format within to generate visually every item of that collection.Mchenry
Thanks Franck I hope this will solve the problem, but I figured that even when I move/pan I'm still having a bit slow performance, maybe it is because of the huge number of elements present. I was wondering if I could convert these to image after redrawing to have a better performance?Carilla
Have a look at VirtualCanvas. Other than that, delete all that horrible code and use proper WPF techniques such as DataBinding and DataTemplating.Ballet
@HighCore I've seen VirtualCanvas it removed/added objects while zooming/panning and the process was totally visible and it didn't convey a good feeling. I know I'm not using MVVM, but other than that can you tell me which part or parts are horrible code? Cause I've based mine on .Net documentation.Carilla
@vahid "not using MVVM" in WPF automatically yields horrible code.Ballet
@Carilla You do not need to rebuild the visual tree to change the scaling. You can just set a ScaleTransform on the element containing the text (e.g., ColumnIdsInPlan), and then update the Scale property as the wheel events come in. You can use RenderTransform or LayoutTransform depending on your needs.Thi
@MikeStrobel Thanks so much Mike, I knew I was doing something wrong, I'm totally new to WPF and C#, only 5-6 months of experience. But I know that I've come a long way. What bothers me is that why moving/panning I still feel a little slow performance, you know like when you scroll a huge text file in a editor, that kind of slow performance. Although, I'm not rebuilding the visual tree in pan/move. What factors can be the culprit? What methods I can use to find why this happens?Carilla
@HighCore Ok then if not using MVVM means horrible code I prefer that , no offense though :)Carilla
B
24

I'm currently dealing with what is likely the same issue and I've discovered something quite unexpected. I'm rendering to a WriteableBitmap and allowing the user to scroll (zoom) and pan to change what is rendered. The movement seemed choppy for both the zooming and panning, so I naturally figured the rendering was taking too long. After some instrumentation, I verified that I'm rendering at 30-60 fps. There is no increase in render time regardless of how the user is zooming or panning, so the choppiness must be coming from somewhere else.

I looked instead at the OnMouseMove event handler. While the WriteableBitmap updates 30-60 times per second, the MouseMove event is only fired 1-2 times per second. If I decrease the size of the WriteableBitmap, the MouseMove event fires more often and the pan operation appears smoother. So the choppiness is actually a result of the MouseMove event being choppy, not the rendering (e.g. the WriteableBitmap is rendering 7-10 frames that look the same, a MouseMove event fires, then the WriteableBitmap renders 7-10 frames of the newly panned image, etc).

I tried keeping track of the pan operation by polling the mouse position every time the WriteableBitmap updates using Mouse.GetPosition(this). That had the same result, however, because the returned mouse position would be the same for 7-10 frames before changing to a new value.

I then tried polling the mouse position using the PInvoke service GetCursorPos like in this SO answer eg:

[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
static extern bool GetCursorPos(out POINT lpPoint);

[StructLayout(LayoutKind.Sequential)]
public struct POINT
{
    public int X;
    public int Y;

    public POINT(int x, int y)
    {
        this.X = x;
        this.Y = y;
    }
}

and this actually did the trick. GetCursorPos returns a new position each time it is called (when the mouse is moving), so each frame is rendered at a slightly different position while the user is panning. The same sort of choppiness seems to be affecting the MouseWheel event, and I have no idea how to work around that one.

So, while all of the above advice about efficiently maintaining your visual tree is good practice, I suspect that your performance issues may be a result of something interfering with the mouse event frequency. In my case, it appears that for some reason the rendering is causing the Mouse events to update and fire much slower than usual. I'll update this if I find a true solution rather than this partial work-around.


Edit: Ok, I dug into this a little more and I think I now understand what is going on. I'll explain with more detailed code samples:

I am rendering to my bitmap on a per-frame basis by registering to handle the CompositionTarget.Rendering event as described in this MSDN article. Basically, it means that every time the UI is rendered my code will be called so I can update my bitmap. This is essentially equivalent to the rendering that you are doing, it's just that your rendering code gets called behind the scenes depending on how you've set up your visual elements and my rendering code is where I can see it. I override the OnMouseMove event to update some variable depending on the position of the mouse.

public class MainWindow : Window
{
  private System.Windows.Point _mousePos;
  public Window()
  {
    InitializeComponent();
    CompositionTarget.Rendering += CompositionTarget_Rendering;
  }

  private void CompositionTarget_Rendering(object sender, EventArgs e)
  {
    // Update my WriteableBitmap here using the _mousePos variable
  }

  protected override void OnMouseMove(MouseEventArgs e)
  {
    _mousePos = e.GetPosition(this);
    base.OnMouseMove(e);
  }
}

The problem is that, as the rendering takes more time, the MouseMove event (and all mouse events, really) gets called much less frequently. When the rendering code takes 15ms, the MouseMove event gets called every few ms. When the rendering code takes 30ms, the MouseMove event gets called every few hundred milliseconds. My theory on why this happens is that the rendering is happening on the same thread where the WPF mouse system updates its values and fires mouse events. The WPF loop on this thread must have some conditional logic where if the rendering takes too long during one frame it skips doing the mouse updates. The problem arises when my rendering code takes "too long" on every single frame. Then, instead of the interface appearing to slow down a little bit because the rendering is taking 15 extra ms per frame, the interface stutters greatly because that extra 15ms of render time introduces hundreds of milliseconds of lag between mouse updates.

The PInvoke workaround I mentioned before essentially bypasses the WPF mouse input system. Every time the rendering happens it goes straight to the source, so starving the WPF mouse input system no longer prevents my bitmap from updating correctly.

public class MainWindow : Window
{
  private System.Windows.Point _mousePos;
  public Window()
  {
    InitializeComponent();
    CompositionTarget.Rendering += CompositionTarget_Rendering;
  }

  private void CompositionTarget_Rendering(object sender, EventArgs e)
  {
    POINT screenSpacePoint;
    GetCursorPos(out screenSpacePoint);

    // note that screenSpacePoint is in screen-space pixel coordinates, 
    // not the same WPF Units you get from the MouseMove event. 
    // You may want to convert to WPF units when using GetCursorPos.
    _mousePos = new System.Windows.Point(screenSpacePoint.X, 
                                         screenSpacePoint.Y);
    // Update my WriteableBitmap here using the _mousePos variable
  }

  [DllImport("user32.dll")]
  [return: MarshalAs(UnmanagedType.Bool)]
  static extern bool GetCursorPos(out POINT lpPoint);

  [StructLayout(LayoutKind.Sequential)]
  public struct POINT
  {
    public int X;
    public int Y;

    public POINT(int x, int y)
    {
      this.X = x;
      this.Y = y;
    }
  }
}

This approach didn't fix the rest of my mouse events (MouseDown, MouseWheel, etc), however, and I wasn't keen on taking this PInvoke approach for all of my mouse input, so I decided I better just stop starving the WPF mouse input system. What I ended up doing was only updating the WriteableBitmap when it really needed to be updated. It only needs to be updated when some mouse input has affected it. So the result is that I receive mouse input one frame, update the bitmap on the next frame but do not receive more mouse input on the same frame because the update takes a few milliseconds too long, and then the next frame I'll receive more mouse input because the bitmap didn't need to be updated again. This produces a much more linear (and reasonable) performance degradation as my rendering time increases because the variable length frame times just sort of average out.

public class MainWindow : Window
{
  private System.Windows.Point _mousePos;
  private bool _bitmapNeedsUpdate;
  public Window()
  {
    InitializeComponent();
    CompositionTarget.Rendering += CompositionTarget_Rendering;
  }

  private void CompositionTarget_Rendering(object sender, EventArgs e)
  {
    if (!_bitmapNeedsUpdate) return;
    _bitmapNeedsUpdate = false;
    // Update my WriteableBitmap here using the _mousePos variable
  }

  protected override void OnMouseMove(MouseEventArgs e)
  {
    _mousePos = e.GetPosition(this);
    _bitmapNeedsUpdate = true;
    base.OnMouseMove(e);
  }
}

Translating this same knowledge to your own particular situation: for your complex geometries that lead to performance issues I would try some type of caching. For example, if the geometries themselves never change or if they don't change often, try rendering them to a RenderTargetBitmap and then add the RenderTargetBitmap to your visual tree instead of adding the geometries themselves. That way, when WPF is performing it's rendering path, all it needs to do is blit those bitmaps rather than reconstruct the pixel data from the raw geometric data.

Bio answered 24/6, 2014 at 17:29 Comment(2)
Thanks Dan, the info you have provided here is of great value to me. Also I have alleviated the issue by drawing on the drawing visual and etc but I still have this choppiness for large projects when I have large number of geometries on the screen. I'm having trouble understanding the code you provided above though, can you provide me with a sample? This will truly make my day if not my year!Carilla
I've updated my answer with more details and a better idea of what's going on.Bio
I
7

@Vahid: the WPF system is using [retained graphics]. What you eventually should do, is devise a system where you only send "what has changed compared to previous frame" - nothing more, nothing less, you should not be creating new objects at all. It's not about "creating objects takes zero seconds", it's about how it affects rendering and the time. It's about letting the WPF do it's job using caching.

Sending new objects to the GPU for rendering=slow. Sending only updates to the GPU which tells what objects moved=fast.

Also, it's possible to create Visuals in an arbitrary thread to improve the performance (Multithreaded UI: HostVisual - Dwayne Need). That all said, if your project is pretty complex in 3D wise - there's good chance that WPF won't just cut it. Using DirectX.. directly, is much, much, more performant!

Some of the articles I suggest you to read & understand:

[Writing More Efficient ItemsControls - Charles Petzold] - understand the process how one achieves better drawing rate in WPF.

As for why your UI is lagging, Dan answer seems to be spot on. If you are trying to render more than WPF can handle, the input system will suffer.

Ingurgitate answered 28/12, 2014 at 13:9 Comment(4)
Thanks Chris for your answer. I understand that WPF is using retained graphics but why it is affecting Panning? I'm not creating any new graphics while panning, so I assume that nothing is being rendered. Or am I wrong?Carilla
I tried the solution Dan proposed, you can see my other question here: https://mcmap.net/q/743907/-slow-pan-and-zoom-in-wpf But it still didn't solve the problem :(Carilla
@Vahid: did you get any further? It's hard to see where you are exactly right now, and what you've done.Ingurgitate
did you look at the link in my above comment? I've implemented the CompositionTargetOnRendering as Dan suggested. But it still didn't improve anything. I'm yet to look at the Multithreaded UI: HostVisual method. I haven't got the time though to see if it actually works.Carilla
T
4

The likely culprit is the fact that you are clearing out and rebuilding your visual tree on each wheel event. According to your own post, that tree includes a "large number" of text elements. For each event that comes in, each of those text elements must be recreated, reformatted, measured, and eventually rendered. That is not the way to accomplish simple text scaling.

Rather than setting a ScaleTransform on each FormattedText element, set one on the element containing the text. Depending on your needs, you can set a RenderTransform or LayoutTransform. Then, when you receive wheel events, adjust the Scale property accordingly. Don't rebuild the text on each event.

I would also do what other have recommended and bind an ItemsControl to the list of columns and generate the text that way. There is no reason you should need to do this by hand.

Thi answered 9/6, 2014 at 14:36 Comment(18)
Did you see my last comment? I also have it while moving/panning which doesn't involve rebuilding the visual tree, I have to apply the ScaleTransform to each object individually cause they all have their specific position on the screen. And I need to keep the proportions in relation to others.Carilla
I would have to see the visual subtree that is actually getting panned, and how you're doing the panning.Thi
Drawing text is always expensive.Gatefold
@MikeStrobel I updated the question. tt is the TranslateTransform.Carilla
@Gatefold Any alternatives to that?Carilla
@Gatefold is correct that text rendering is where WPF performance really starts to break down. Are you using a LayoutTransform or RenderTransform to do the panning? Does it start to smooth out after you've panned through all the text, as if there is extra overhead when showing an item for the first time? Also, based on your code, it looks like you're using a single transform instead of one for each piece of text (which is good), but to confirm: you are applying the transform to only one element, right?Thi
@Carilla Draw less text or draw the same text less! These are the golden rules of graphics performance. Draw only what you need and draw what you need only when you need to. You are drawing what you need but you are drawing it too often. Trust WPF to work it out for you before you drop back to OnRender.Gatefold
@MikeStrobel I'm using RenderTransform, it doesn't get smooth. I have lots of lines and polygons on the Canvas too. They don't pose any problem but once I add the texts get the low performance.Carilla
Is there a reason you need a separate DrawingVisual for each column? If you are not going to do virtualization, then you may get better performance by drawing all of the text elements to a single visual. And approximately how many text elements are you drawing?Thi
@MikeStrobel I'm applying RotateTransform to each individual text, I thought that I would need separate DrawingVisual in this case. Am I wrong? if so, then this might be the reason. How can I convert the above code so I can use only one visual?Carilla
@Carilla I believe you can do PushTransform, DrawText, then Pop for each element. The call to Pop should remove the transform that you pushed.Thi
@Gatefold I'm drawing texts as ids for some structural columns, so I need to draw them all.Carilla
You mean I should bring the using outside of the foreach then do like this: dc.PushTransform(st); then dc.DrawText(ft, space.FlipYAxis(x, y)); and finally dc.Pop(); ?Carilla
Correct, do those three calls for each element in the loop.Thi
@MikeStrobel This was a great idea, although I can't tell if this will solve my problem cause I need to change may parts of drawing engine to work only with one DrawingVisual, but anyway it is a great leap forward for me, many thanks.Carilla
@Carilla is there a reason that DataGrid or ListView isn't good enough for you?Gatefold
@Gatefold What I'm doing can be accomplished using these? I'm creating like 1000+ individual text objects with Rotation/Scale with different positions on canvas.Carilla
You could always simply use a Grid, DataGrid or ListView(with grid template) and scale that object once and feed it a DataTable or whatever List / Array you haveMchenry

© 2022 - 2024 — McMap. All rights reserved.