-1

I found a VirtualizingWrapPanel(VWP) project here which, I thought, could help me with optimizing my ListView scrolling performance. The listView have to has four columns and multiple rows to display the source items. So I tried to use this VWP, but scrolling(I made it as DoubleAnimation) smoothness still sucks, the framerate drops down to about 30 fps and it's very noticeable. I copied the source code from the link above to try to debug it and understand what is the reason of such a bad performance.

After several hours a found out that ArrangeOverride method of VWP and MeasureOverride method of its "inheritance parent" can execute about 20-30ms (1000ms / 30ms = ~30fps) and that's it, I found the reason why it's so slow... but why?

MeasureOverride method calls two potentially heavy methods: RealizeItems and VirtualizeItems

protected virtual void RealizeItems()
    {
        var startPosition = ItemContainerGenerator.GeneratorPositionFromIndex(ItemRange.StartIndex);

        int childIndex = startPosition.Offset == 0 ? startPosition.Index : startPosition.Index + 1;

        using (ItemContainerGenerator.StartAt(startPosition, GeneratorDirection.Forward, true))
        {
            for (int i = ItemRange.StartIndex; i <= ItemRange.EndIndex; i++, childIndex++)
            {
                UIElement child = (UIElement)ItemContainerGenerator.GenerateNext(out bool isNewlyRealized);
                if (isNewlyRealized || /*recycled*/!InternalChildren.Contains(child))
                {
                    if (childIndex >= InternalChildren.Count)
                    {
                        AddInternalChild(child);
                    }
                    else
                    {
                        InsertInternalChild(childIndex, child);
                    }
                    ItemContainerGenerator.PrepareItemContainer(child);

                    child.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
                }

                if (child is IHierarchicalVirtualizationAndScrollInfo groupItem)
                {
                    groupItem.Constraints = new HierarchicalVirtualizationConstraints(
                        new VirtualizationCacheLength(0),
                        VirtualizationCacheLengthUnit.Item,
                        new Rect(0, 0, ViewportWidth, ViewportHeight));
                    child.Measure(new Size(ViewportWidth, ViewportHeight));
                }
            }
        }
    }

    protected virtual void VirtualizeItems()
    {
        for (int childIndex = InternalChildren.Count - 1; childIndex >= 0; childIndex--)
        {
            var generatorPosition = GetGeneratorPositionFromChildIndex(childIndex);

            int itemIndex = ItemContainerGenerator.IndexFromGeneratorPosition(generatorPosition);

            if (!ItemRange.Contains(itemIndex))
            {

                if (VirtualizationMode == VirtualizationMode.Recycling)
                {
                    ItemContainerGenerator.Recycle(generatorPosition, 1);
                }
                else
                {
                    ItemContainerGenerator.Remove(generatorPosition, 1);
                }
                RemoveInternalChildRange(childIndex, 1);
            }
        }
    }

So I understand that developers could make a mistake in code, I also understand that there may not be a single thing that can be optimized, but I really cannot understand why it is so slow.. The loops iterates throught about 40-50 UIElement objects, every of which has two child objects(TextBlock and ImageSource) and that's not such a huge number of objects..

I want to use parallel tasks in these loops, but UIElement and others is not thread-safe and can only be accessed in UI Thread... or can?

How can I increase performance of these methods? Can there be implemented efficient multithreading? Thanks!

PandrPi
  • 1
  • 2
  • You can't parallelize container generation. The containers must created on the UI thread. – BionicCode Jul 22 '20 at 14:36
  • Is there any alternatives, which would increase performance? – PandrPi Jul 22 '20 at 14:58
  • Generally, the code you have posted looks ok. You can check if it is really necessary to do all the container realization over again by checking e.g. if the constraint size passed to `MeasureOverride` has changed or exceeded a certain threshold. Basically try to optimize or add a constraint that helps to avoid redundant iterations or shorten them by using a break out criteria. On the user side, try to keep the visual tree of thew containers as small as possible. Remove every unused elements like borders and layout attributes. Check the caching policy, is the cash too big or maybe too small? – BionicCode Jul 23 '20 at 07:30
  • Maybe analyze the code part that is responsible for generating new items on offset changes. Start from `SetHorizontalOffset` and `SetVerticalOffset`. Maybe here the code generates too much items and you find a way to optimize the algorithm. – BionicCode Jul 23 '20 at 07:35
  • Very strange.. I reduced a number of loops iterations by manipulating only with the changed items, but almost nothing changed :( Now loops affects a several(1-6) objects, but child.Measure(), child.Arrange() and other methods take too much time in total.. – PandrPi Jul 23 '20 at 14:36
  • What if create independent STA thread, create all children elements inside this thread, write some simple logic to make RealizeItems and VirtualizeItems asynchronous and call them in MeasureOverride method with await operator that will not block the main thread? I didn't show any details, there can be a lot of small probrems with implementation, but it should work.. i think it should.. Child UIElement's are created due to ItemGenerator.GenerateNext method, theoretically if call this method parallel thread all the children must also belong to this thread. Any thoughts about such a solution? – PandrPi Jul 23 '20 at 15:33
  • This won't work. You can spawn a second UI thread, no problem. But then you would have an individual `Dispatcher` associated with this thread. Each UI thread has its own `Dispatcher`. `UIElements` are always associated to the `Dispatcher` of the thread they are created on. You can't pass those objects between threads (except they are frozen). You would get a cross thread exception when accessing those objects. Usually this shouldn't take too much time to do some little measure and arrange.Do you explicitly invalidate the layout somewhere which causes extra layout passes? – BionicCode Jul 23 '20 at 16:16
  • How often are measure and arrange called before the content is rendered? Can you post a small example code that reproduces this behavior? I have a custom chart panel to draw caretesian chart objects. I can realize thousands of containers without any issues. issues will always arise when the visual tree of the item containers get too big. You should know that every child element executes Measure() and Arrange() _recursively_ each time. Layout pass is executed recursively. – BionicCode Jul 23 '20 at 16:16
  • That's why you should keep the container tree small in order to have as few as possible Measure() and Arrange() invocations down the container's tree. – BionicCode Jul 23 '20 at 16:16
  • About `Dispatcher`, yes, I realize such a problem, there may be way to create a whole control in separate thread and communicate with Invokes or smth else. About Invalidate I will post and "answer" here with code Visual tree do not have a lot of child items, every item has `StackPanel`, there are single `Image` and `TextBlock` controls – PandrPi Jul 23 '20 at 18:12

1 Answers1

0

I found this method, which calls InvalidateMeasure() every time vertical scroll offset changes. Because of DoubleAnimation I use to animate vertical scroll offset this is code really often called, but if there is no one thing to change measure and arrange methods executes for about 1-2ms in total, else - a lot slowlier.

public void SetVerticalOffset(double offset)
{
    if (offset < 0 || Viewport.Height >= Extent.Height)
    {
        offset = 0;
    }
    else if (offset + Viewport.Height >= Extent.Height)
    {
        offset = Extent.Height - Viewport.Height;
    }
    Offset = new Point(Offset.X, offset);
    ScrollOwner?.InvalidateScrollInfo();
    InvalidateMeasure();
}

UPDATE: I had opened Profiler and found out, that a lot of time(7-10 and more ms) is taken by layout operations.

UPDATE 2: The code bellow offsets(arranges) all the visible items

protected override Size ArrangeOverride(Size finalSize)
    {
        double offsetX = GetX(Offset);
        double offsetY = GetY(Offset);

        /* When the items owner is a group item offset is handled by the parent panel. */
        if (ItemsOwner is IHierarchicalVirtualizationAndScrollInfo groupItem)
        {
            offsetY = 0;
        }

        Size childSize = CalculateChildArrangeSize(finalSize);

        CalculateSpacing(finalSize, out double innerSpacing, out double outerSpacing);

        for (int childIndex = 0; childIndex < InternalChildren.Count; childIndex++)
        {
            UIElement child = InternalChildren[childIndex];

            int itemIndex = GetItemIndexFromChildIndex(childIndex);

            int columnIndex = itemIndex % itemsPerRowCount;
            int rowIndex = itemIndex / itemsPerRowCount;

            double x = outerSpacing + columnIndex * (GetWidth(childSize) + innerSpacing);
            double y = rowIndex * GetHeight(childSize);

            if (GetHeight(finalSize) == 0.0)
            {
                /* When the parent panel is grouping and a cached group item is not 
                 * in the viewport it has no valid arrangement. That means that the 
                 * height/width is 0. Therefore the items should not be visible so 
                 * that they are not falsely displayed. */
                child.Arrange(new Rect(0, 0, 0, 0));
            }
            else
            {
                child.Arrange(CreateRect(x - offsetX, y - offsetY, childSize.Width, childSize.Height));
            }
        }
        return finalSize;
    }
PandrPi
  • 1
  • 2