1

I have an array of 3500 bitmaps. These bitmaps are displayed on the screen (a screen can display 12) and more are loaded while the vertical scroll bar is going down. At some point, i would like to release these images from memory and i'd like to know if Linq can help : What i tried :

Declaration :

    imageCache = new Bitmap[someCountWithaResultOf3500];

The imageCache gets loaded by images and then at some point i would like to dispose a few (from the beginning to 200 before the current one). Id tells me the first image displayed.

Function

imageCache.AsEnumerable()
                   .Select((s, j) => new { j, s })
                   .Where(x => (x.j > 0 && x.j < lowlimit && x.j != null))
                   .ToList().ForEach(x => Dispose());

the lowlimit is the Max of 0 and the id -200 of the image displayed.

When i reach the lowlimit, the screen becomes black, the images disappear (in debug mode, i had the lowlimit at 4)

2 questions :

  • in the where clause, i would like to filter on non disposed bitmaps, how to do it (my clause x.j !=null doesnt work )
  • Is the Dispose at the foreach supposed to work ?
  • 1
    1. `x.j != null` doesn’t mean the image is not disposed, it means it not null. If you dispose something it doesn’t become null. 2. It should be `ForEach(x => x.s.Dispose());` That aside there are some other issues: a) You don’t need to call `AsEnumerable()`. b) Instead of `s` you should give the property a more verbose name like `bitmap`. c) Instead of `ToList().ForEach(…)` I would use a regular foreach statement like `foreach (var item in imageCache.Select(…).Where(…)) { item.s.Dispose(); }` in order to avoid the allocation of throwaway list just to call the ForEach method. – ckuri May 04 '20 at 21:47
  • What is your display technology? If you are working in WPF or UWP, there is pretty good support for this kind of work. | 3500 Bitmaps seems way to large for human processing. 100 might be a more likely limit. Anything more you have to cut down, and it is best to not even load those. – Christopher May 04 '20 at 21:52
  • As you specified WPF; here is something on the Thematic: https://www.codeproject.com/Articles/34405/WPF-Data-Virtualization – Christopher May 04 '20 at 21:58
  • @SebastienChemouny The GPU does not mater, as there is no complex rendering. It is mostly a mater of fitting all those images - uncompressed - into RAM. – Christopher May 04 '20 at 22:03
  • 1
    Probably worth mentioning this is a follow-up to https://stackoverflow.com/q/61574511/156755 to give others some context. (This is a custom Winforms Control, you have a large number of images in view fora short period of time, you're using an [x] as a cache, loading on demand, etc...) – Basic May 04 '20 at 23:28
  • In a WPF application you shouldn't be using the Bitmap class at all, because it belogs to WinForms. Use BitmapImage or BitmapFrame instead. – Clemens May 05 '20 at 05:30
  • You should only load and display images that are currently in view. You should then dispose of them when they are no longer displayed. – Enigmativity May 05 '20 at 06:37

1 Answers1

1

The key question is how to determine when images can be unloaded.

Let's take a naive example... If an image is visible, it'll be kept in memory, otherwise unload it.

(This won't be ideal as any time you scroll, images need to be re-fetched from disk but it demonstrates the approach)

Let's say we have a function that determines which images are in view and returns a list of paths [could equally be IDs]....

public List<string> GetVisibleImagePaths() {
    //Do something here to return a dozen or so paths that are visible.

    //Later refinement: Also include images a few either side of the current position for 
    //smoother scrolling
}

So....

When someone changes the scroll position of your control

protected override void OnScroll(ScrollEventArgs se) {
    base.OnScroll(se);
    EvictFromCache();
}

private Dictionary<string, Bitmap> imageCache;

private void EvictFromCache() {
    var paths = GetVisibleImagePaths();
    // Now loop through all the keys in the cache and evict those not specifically requested
    // This is a naive approach to cache eviction. Perhaps you want to keep the last few dozen
    // in case the user reverses direction and scrolls back or .... Lots of options, but for now
    // we'll evict anything not in the list of paths we were given
    foreach (var expired in imageCache.Keys.Where(x => !paths.Contains(x)).ToList()) {
        //Dispose of the object, freeing up resources
        imageCache[expired].Dispose();

        //And now remove the reference to the disposed object
        imageCache.Remove(expired);
    }
}

You're going to have to do some tuning to work out the sweet spot for how much to keep in the cache/how far in advance to load images to give an optimal balance of performance/responsiveness on your target hardware.

Also, doing this on scroll isn't really necessary.... Just semi-regularly, but it was a convenient point to hook into for demo purposes. Depending on the strategy you pick, a timer or some other approach might be more appropriate.

One point on determining if something is disposed... Don't. As soon as you dispose of an object, release all references to it. If you need a new one, you'll have to create it fresh anyway, so keeping a reference to an unusable object is pointless.

Instead of checking if the object is disposed, check if you have an object referenced at all (is it null / does the key exist / is the list empty / etc)

This is handled above by disposing, then removing from the dictionary.

Basic
  • 26,321
  • 24
  • 115
  • 201
  • Thanks Basic (once again), it's incredible how simple your approach is, at the same time effective. i just had to modify one thing, the foreach didnt work because the collection was modifed doing the remove and was throwing an exception (adding `ToList()`at the end made it. I also added a new filter for null value to exclude disposed or non created values : `foreach (var expired in imageCache.Where(x => !paths.Contains(x.Key)).Where(x=> x.Value !=null).Select(x=> x.Key).ToList())` – Sebastien Chemouny May 05 '20 at 21:53
  • 1
    Good catch on the modifying a collection... Teach me to use an IDE. As to the rest, no worries... It's been 5 years since I've had a chance to do any C# (and about as long since I've spent time answering), so I was hunting for something a little challenging but quick enough to answer. On the last point... If your bitmap is disposed, the key should no longer be in the dictionary. My comment was erroneous in that removing from the dict is as good as setting to null [fixed]. The key point is that there's no longer a reference to the object in managed code. This makes your logic cleaner – Basic May 06 '20 at 01:44
  • 1
    Oh and... A clean way to tidy up that "Modifying a collection" issue... In the for loop, add a `.ToList()` at the end of the linq... `foreach (var expired in imageCache.Keys.Where(x => !paths.Contains(x)).ToList()) {` That way, instead of working through a filtered version of the original collection (cache) it creates a new list of items and iterates through that instead. – Basic May 06 '20 at 08:53
  • Thanks ! Yes fully agree on the facct the key should not be part of the dictionary. I modified it accordingly, it's cleaner and probably a tiny bit faster :) – Sebastien Chemouny May 06 '20 at 08:58
  • 1
    Very welcome. One last thing... If this were my project, I'd move the cache funcitonality to a new class. Either just code an `ImageCache` class with the GetImage/EvictCache methods and any others you need. That keeps all the caching logic separate from the form and allows you to reuse it elsewhere very easily. [All part of the "clean code" mantra]. In any case, all the best – Basic May 06 '20 at 21:42
  • 1
    yeah i really need to get better at cleaning code ... but good point, it's actually nice to have a "light" form (everything is in the class). Again thanks, i think i found the sweet spot, it's smooth, doesnt take memory, worked on the scroll bar and mouse wheel, i can now focus on the action when i mousemove over a picture or i click on it :) Again, thanks a bunch, never thought i was gonna get there :) – Sebastien Chemouny May 06 '20 at 23:39