Skip to content

Commit

Permalink
[windows] improve memory usage of CollectionView
Browse files Browse the repository at this point in the history
Context: #16436
Context: https://github.com/symbiogenesis/Maui.DataGrid/tree/net8-memory-leak

A customer sample has a `CollectionView` with 150,000 data-bound rows.

Debugging what happens at runtime, I found we effectively were doing:

    _itemTemplateContexts = new List<ItemTemplateContext>(capacity: 150_000);
    for (int n = 0; n < 150_000; n++)
    {
        _itemTemplateContexts.Add(null);
    }

Then the items were created as you scroll each row into view:

    if (_itemTemplateContexts[index] == null)
    {
        _itemTemplateContexts[index] = context = new ItemTemplateContext(_itemTemplate, _itemsSource[index],
            _container, _itemHeight, _itemWidth, _itemSpacing, _mauiContext);
    }
    return _itemTemplateContexts[index];

This code accesses the indexer multiple times, into a `List<T>` with
150,000 `null` items.

To improve this:

* use a `Dictionary<int, T>` instead, just let it size dynamically.

* use `TryGetValue(..., out var context)`, so each call accesses the
  indexer one less time than before.

* use either the bound collection's size or 64 (whichever is smaller) as
  a rough estimate of how many might fit on screen at a time.

Taking a memory snapshot of the app after startup:

    Before:
    Heap Size: 82,899.54 KB
    After:
    Heap Size: 81,768.76 KB

Which is saving about 1MB of memory on launch.

In this case, it feels better to just let the `Dictionary` size itself
with an estimate of what `capacity` will be.
  • Loading branch information
jonathanpeppers committed Aug 17, 2023
1 parent 040f2f1 commit ac10c42
Showing 1 changed file with 6 additions and 10 deletions.
@@ -1,4 +1,5 @@
#nullable disable
using System;
using System.Collections;
using System.Collections.Generic;

Expand All @@ -14,21 +15,21 @@ internal class ItemTemplateContextList : IReadOnlyList<ItemTemplateContext>
readonly double _itemWidth;
readonly Thickness _itemSpacing;

readonly List<ItemTemplateContext> _itemTemplateContexts;
readonly Dictionary<int, ItemTemplateContext> _itemTemplateContexts;

public int Count => _itemsSource.Count;

public ItemTemplateContext this[int index]
{
get
{
if (_itemTemplateContexts[index] == null)
if (!_itemTemplateContexts.TryGetValue(index, out var context))
{
_itemTemplateContexts[index] = new ItemTemplateContext(_itemTemplate, _itemsSource[index],
_itemTemplateContexts[index] = context = new ItemTemplateContext(_itemTemplate, _itemsSource[index],
_container, _itemHeight, _itemWidth, _itemSpacing, _mauiContext);
}

return _itemTemplateContexts[index];
return context;
}
}

Expand All @@ -48,12 +49,7 @@ internal class ItemTemplateContextList : IReadOnlyList<ItemTemplateContext>
if (itemSpacing.HasValue)
_itemSpacing = itemSpacing.Value;

_itemTemplateContexts = new List<ItemTemplateContext>(_itemsSource.Count);

for (int n = 0; n < _itemsSource.Count; n++)
{
_itemTemplateContexts.Add(null);
}
_itemTemplateContexts = new(capacity: Math.Max(64, _itemsSource.Count));
}

public IEnumerator<ItemTemplateContext> GetEnumerator()
Expand Down

0 comments on commit ac10c42

Please sign in to comment.