Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

v3.5.1+ ContentManager.Unload() doesn't seem to free up memory #5341

Closed
gap9269 opened this issue Nov 29, 2016 · 29 comments · Fixed by #5921
Closed

v3.5.1+ ContentManager.Unload() doesn't seem to free up memory #5341

gap9269 opened this issue Nov 29, 2016 · 29 comments · Fixed by #5921

Comments

@gap9269
Copy link

gap9269 commented Nov 29, 2016

After upgrading from 3.4 to 3.5.1 (and then 3.6) I noticed that calling ContentManager.Unload() doesn't lessen the amount of memory being used by the game, as seen in the Diagnostic Tools of VS2015 and the Resource Monitor on Windows. Loading, unloading, then reloading the same content would originally keep the memory usage about the same, but now it continues to increase during every Load() while not decreasing during Unload().

I tested this further by installing v3.4 again and using the same project and code. Loading and unloading the same content as before now has the desired effect where the memory usage increases during Load() and decreases after Unload(), as expected.

@Inverness
Copy link
Contributor

ContentManager.Unload() should dispose all loaded assets and free up any native memory they are using. This will not free up managed memory as that is only freed when a garbage collection is done.

What memory metric are you looking at here?

@gap9269
Copy link
Author

gap9269 commented Jan 7, 2017

Here's a thread I posted about this in the community: http://community.monogame.net/t/3-5-memory-issues-contentmanager-unload-not-working-properly/8463/5

I can confirm now that this is definitely an issue with Monogame 3.5+. I've tried 3.6 as well on the development branch and I had the same issue. Changing maps (unloading content) and exiting to the main menu (unloading all game content) didn't effect Memory Usage in the Resource Monitor on Windows, nor did it effect the Process Memory in the Diagnostic Tools of VS2015. (Other than an increase in memory as more content is loaded)

I switched back to Monogame 3.4 and the game began behaving as it normally would, where changing from a large map to a smaller one reduces memory usage, and exiting to the main menu reduces it significantly, because only the menu textures are loaded.

I made zero code changes during these tests. I only installed different versions of Monogame.

So, to answer your question, I was looking at the Process Memory in VS2015 and Memory Usage in the Windows Resource Monitor. I tested it as thoroughly as I could with 3.5 and 3.6 installed, noticing the same problem in both: memory kept increasing, but never decreased when I unloaded content. 3.4 works fine.

@Jjagg
Copy link
Contributor

Jjagg commented Jan 20, 2017

@gap9269 Is there anything you can share that shows this issue? Maybe even a minimal project that reproduces it?
This really shouldn't be happening considering these lines in ContentManager:

disposableAssets.Clear();
loadedAssets.Clear();

@gap9269
Copy link
Author

gap9269 commented Jan 28, 2017

I recorded 3.4 performance to show what it should actually look like. It's awful quality because I recorded it with VLC at like 12fps, but if you watch the list of processes above the game window you can see the memory jump up and down.

3.4: https://www.youtube.com/watch?v=J6lDHuRAYWY

In this you can see the memory increase and decrease as I load new areas and quit to the main menu. It always bounces between 90k-250k or so, depending on how much content is loaded.

3.6: https://www.youtube.com/watch?v=muAe7LKd28M

I recorded 3.6 with my phone because VLC kept giving me errors. You can see the memory usage only go up, despite doing the same thing as the last video. It doesn't seem to be releasing memory when I unload content (like moving between areas, or quitting to the main menu).

I don't know if this helps or not, but it's all I have. Here are the memory profiler reports for 3.4 and 3.6, respectively. It seems like 3.6 is about the same when you look at these. I don't really know what the deal is.

3.4:
monogame 3 4

3.6:
monogame 3 6

@Jjagg
Copy link
Contributor

Jjagg commented Jan 28, 2017

I've been trying to reproduce this for over an hour now with no success :/ I tried loading and unloading some assets a bunch of times and switched between 3.4 and latest develop, but they pretty much behaved the same. Maybe this is not happening for all types of assets?

@gap9269 How are you handling content exactly? Some insight might help in figuring out the issue. Are you using multiple content managers or just one? Are you disposing them or just call unload and reuse? Do you ever manually dispose content?

@gap9269
Copy link
Author

gap9269 commented Jan 29, 2017

Multiple ContentManagers, one for each map and menu. When I leave a map the current map's ContentManager calls UnloadContent(). The next map then loads all of its content.

I never manually dispose content, nor do I call Dipose() on anything. It's just Load and Unload. Same for menus.

Assets are mostly Texture2Ds

@gap9269
Copy link
Author

gap9269 commented Apr 10, 2017

I bit of an update on this:

I'm using 3.6 now and the issue is still constantly rearing its ugly head. I can mostly ignore it until I get an OutOfMemory exception from playing too long. (Note: It's not a time related thing, it's just from loading new areas. Even though I'm calling Unload() on everything it eventually builds up and crashes)

I thought that it might be an issue with the higher GC generations not clearing so I'm manually calling GC.Collect() every time I switch areas, but that doesn't fix the problem.

Did something with garbage collection change between 3.4 and 3.5? Perhaps it isn't clearing everything that it used to.

For now I'll continue to make demo builds of the game using 3.4, but it's a pain to have to switch back to get rid of this issue.

@Jjagg
Copy link
Contributor

Jjagg commented Apr 10, 2017

I'll give this another spin with textures. Maybe it's an issue with GraphicsResources. They're​ stored in other places than the content manager, though i think only weak references are used and they are even explicitly cleaned up. Did you run into this on multiple platforms? Just to be sure we're not looking in the wrong place.

It's weird that not more people are experiencing this. Maybe your code uses MG in some uncommon way that causes the garbage? Are you loading custom types with the content manager?

@gap9269
Copy link
Author

gap9269 commented Apr 10, 2017

I've only tested on PC, but our UWP version is almost running to test on Xbox.

I don't think I'm doing anything strange. No custom types are being loaded, the majority is Texture2D. Some videos (.wmv), a couple shaders, some spritefonts.

I have a different ContentManager for each area and menu, so I load all of the necessary content on load for those areas, then when I leave them I simply call content.Unload();

I can paste larger pieces if there's anything specific that might help. Or we could Skype and screen share to walk through what I'm doing.

@mrhelmut
Copy link
Contributor

What types of assets are you loading? Would you mind listing all of them?

A full profiling log up to the OutOfMemoryException would be useful, videos don't show what's really going on, and it could very well be a memory management issue outside of the ContentManager (like keeping references to assets which would prevent them from being garbage collected, or doing memory heavy operations like plain text parsing). Your profiling screenshot shows irregular memory usage, I would look into allocations and references in your code as well.

@gap9269
Copy link
Author

gap9269 commented Apr 10, 2017

Types I'm loading:

  • Texture2D
  • Effect
  • Spritefont
  • Video (I don't load these often, and I haven't loaded any during my recent out-of-memory playthroughs)

To the best of my knowledge I'm not loading anything else. We have about 3GB of assets total. \


Here's a link to a profiling session. I'm not sure if it's exactly what you wanted, I only very recently started dabbling with memory profilers.

https://drive.google.com/file/d/0B9OprX_2DQFEMkZEMTRUNDZpQ0U/view?usp=sharing

As for keeping references to things, it's totally possible that I'm doing it without realizing it. A lot of this code was written when I was still in school so a lot of it is a mess. What baffles me is that switching back to 3.4 fixes this problem for me. I can swap back to 3.4 and record a profiling session there as well, but I'll wait to see if this is useful in the first place.

@nkast
Copy link
Contributor

nkast commented Apr 11, 2017

Here are the memory profiler reports for 3.4 and 3.6, respectively. It seems like 3.6 is about the same when you look at these. I don't really know what the deal is.

It appears so. Actually 3.5 & 3.6 allocates less memory because of the Scratch Buffer.
It has to be some native resource. You have to test a project that loads an asset and then Unloads it once per frame. Do that first for textures, then for audio, effects, etc. This way you will know if there is a resource that leaks memory.

@nkast
Copy link
Contributor

nkast commented Apr 11, 2017

I think it has to be SoundEffect instance. With this code the memory keeps adding up.

        protected override void Update(GameTime gameTime)
        {
                    Content.Unload();
                    Debug.WriteLine(GC.GetTotalMemory(true));

                    var sound = Content.Load<SoundEffect>("sound");            
                    var sfx = sound.CreateInstance();

                    base.Update(gameTime); 
        }

If I dispose the sfx then the memory is kept stable.

@gap9269 do you load sounds and how do you play them?
Do you create instances or you call the fire-and-forget Sound.Play()?

@gap9269
Copy link
Author

gap9269 commented Apr 11, 2017

We don't load SoundEffects in the game, we're using Wwise and I simply make API calls to play sounds. It loads everything on its own end.

I dug a bit deeper and I'm just getting more and more confused. Here's what I tried:

First, I disabled Wwise to see if something was going wrong on its end. There isn't. The only difference was less total memory at the start because it wasn't loading any sounds or making any instances of Sound Objects.

Second, I ran the profiler I installed and watched closely as I ran through areas (which Loads and then Unloads them). What I saw was a constant increase in Total Managed Bytes (TMB) and Live Managed Bytes (LMB) every time I loaded assets (these are all Texture2D assets, by the way). I also call a general GC.Collect() on every area Unload. So far this is what I've always seen. However, when I re-entered those areas that I had already loaded, the TMB and LMB stayed the same, despite the fact that I was calling Content.Load on all of their Textures again. Leaving the area still didn't drop the process memory.

It seems like maybe the content really isn't being released on Unload for some reason? I'm not sure if that would make it so subsequent Loads wouldn't increase memory usage, but that appears to be what's happening.

Here's the kicker: when I quit the game to main menu memory usage doesn't drop. However, when I start a new game I recreate every area class (each has their own ContentManager) to get a clean reset of the game. As soon as I change areas in this new game it calls GC.Collect() and the TMB/LMB/Process Memory plummets down to where I would expect it to be, roughly. It's still a bit high, but this consistently happens and it always drops me down to the same range. (First launch of game is 315k TMB, all reloads afterward are around 340k)

It doesn't matter if I get the memory up to over 1GB or if I reload at 300MB, it always resets on that first new GC.Collect(). Maybe the ContentManager is a red herring, but loading/unloading content is the only major thing that happens during area transitions.

Short summary: Constant Load/Unload with different ContentManagers only increases usage, doesn't drop it even with manual GC.Collect() calls. Further Load() of the same assets doesn't increase usage. Recreating those ContentManagers and calling GC.Collect() seemingly clears it all out.

@gap9269
Copy link
Author

gap9269 commented Apr 11, 2017

I think I've confirmed that it's a problem stemming from the ContentManagers.

I disabled the recreating of the areas (and their ContentManagers) when exiting to the main menu and starting a new game. Now the memory usage no longer drops back down to normal levels on GC.Collect(). Re-loading the same content from the previous playthrough has no effect on memory usage, but entering new areas effects it.

Here is how I am creating new ContentManagers in every class that needs one:

protected ContentManager content;

public MapClass(List<Texture2D> bg, Game1 g, ref Player play)
{
    //Create a bunch of Lists and Dictionaries and stuff that every map needs
    ...
    ...
    content = new ContentManager(g.Services);
    content.RootDirectory = "Content";
}

Then I simply load content when I enter a map, and here's an example...

    public override void LoadContent()
    {
    base.LoadContent();

        background.Add(content.Load<Texture2D>(@"Maps\History\NapoleonsCamp\background"));
        background.Add(content.Load<Texture2D>(@"Maps\History\NapoleonsCamp\background2"));
        foreground = content.Load<Texture2D>(@"Maps\History\NapoleonsCamp\foreground");

        smoke = ContentLoader.LoadContent(content, @"Maps\History\NapoleonsCamp\smoke");
     }

(Note: base.LoadContent() just does more of the same. Checks for generic objects and loads their content if present. Nothing special there)

Finally, unloading:

    public virtual void UnloadContent()
    {
        Sound.MuteReverb();
        UnloadNPCContent();
        content.Unload();
        if (mapName != "Bathroom")
            EnemyContentLoader.UnloadEnemySoundBanks();
        Portal.UnloadContent();
        background.Clear();
    }

The only other thing I can think of that might be wrong is how I handle references to more common textures. What I mean is, some things like NPCs might exist in more than one place and if I unload content their reference to the Texture2D will be disposed, so they won't draw in another area. To get around this I set their Texture2D to be a placeholder texture, then I unload all content. Then when I enter an area with the NPC again I simply set their texture again (npc.sprite = content.Load...). I'm not sure why that would be causing this issue, but I figured I'd mention everything.

@mrhelmut
Copy link
Contributor

mrhelmut commented Apr 11, 2017

Further Load() of the same assets doesn't increase usage.

That's the normal behavior of ContentManager, it keeps track of what has been loaded and won't load twice the same asset, unless you load it from a different ContentManager.

Recreating those ContentManagers and calling GC.Collect() seemingly clears it all out.

There may be something going on here. What's puzzling me is that Texture2D are loaded on the GPU memory and shouldn't impact much the RAM usage. It looks like a content reader issue keeping references to decoded textures.

Do you think that you can put together a minimal project exposing the issue?

@gap9269
Copy link
Author

gap9269 commented Apr 11, 2017

Yep! I'm going to try that soon. I have a ton of stuff I need to do today first, but I'm going to get around to it ASAP. I'll post here with a link once I have it.

@gap9269
Copy link
Author

gap9269 commented Apr 12, 2017

Here's a link to the project: https://drive.google.com/file/d/0B9OprX_2DQFENGJwb1FaRHlmdGs/view?usp=sharing

It's about 1GB in size because I tossed in a bunch of textures to load. The instructions are pretty simple and the project is minimal, but here's an overview:

I created 4 separate ContentManagers and four separate folders of images. By pressing '1', '2', '3', or '4' on your keyboard you will load the corresponding folder with the corresponding ContentManager. Pressing again will Unload it. I use .Net Memory Profiler to test, and I saw the memory usage going up during loads, but never dropping during unloads.

I added a final command to fix that. Press 'X' to recreate the ContentManagers and unload everything. For some reason I had to press it twice, but it would clear everything out and return the memory usage to normal.


I also switched back to 3.4 to test and saw some crazy things. First, it had much lower TMB/LMB than 3.6 off the start. However, when I loaded content it would run a ton of garbage collections. It was crazy. Unloading -did- drop the memory usage back down though, which is what I expected since I had never had issues with 3.4.

Hopefully this helps shed some light on the issue here.

@nkast
Copy link
Contributor

nkast commented Apr 13, 2017

also switched back to 3.4 to test and saw some crazy things.
However, when I loaded content it would run a ton of garbage collections. It was crazy.

I bet it was much slower as well! There are two changes between 3.4 and 3.5 for this, First, we avoid full
in memory decompression
of compressed content. Second, we use a scratch buffer to avoid unnecessary -slow- memory allocation which also triggers the GC.

This still has a flaw, which is that the scratch buffer can get as big as the bigger asset you load.
I'm sure that's what you see in 3.5/3.6. For example the memory doesn't grow each time you unload/load assets from the same ContentManager. And the number of assets and their total size are irrelevant. It's just that the contentManager now reserves an amount of memory for itself for it's lifetime.

@gap9269
Copy link
Author

gap9269 commented Apr 13, 2017

Ah! That would explain the behavior entirely then.

I suppose a quick fix on my end would be to trash and recreate the ContentManager during loading screens, right before I do a manual GC.Collect(). I don't know what this will do to performance (if anything, really), but it should prevent future OutOfMemory exceptions. Unfortunately, that won't help anyone who runs into this the way I did. Is it possible to drop the size of the scratch buffer down after the ContentManager is unloaded?

Regardless, this issue might be worth closing now since it is a little misleading. What are your thoughts?

@nkast
Copy link
Contributor

nkast commented Apr 13, 2017

I suppose a quick fix on my end would be to trash and recreate the ContentManager during loading screens,
Is it possible to drop the size of the scratch buffer down after the ContentManager is unloaded?

I don't see a reason to do so since the scratch buffer will grow again with the next assets. This will defeat the purpose of the scratch buffer and just make your loading slower.

Unfortunately, that won't help anyone who runs into this the way I did.

True. Although it's a fraction of the total memory it's still something that MG can improve.
Generally a game should be able to adapt to the available memory. Feature #5111 move in this direction, until then you have to include multiple content at different resolutions. Back to ContentManager...

One issue is that each ContentManager has at least 1MB of scratch memory, this could be a problem when someone uses multiple ContentManagers, there are scenarios where you have one manager per asset, for example when you want to have independent instances and manipulate assets with SetData() or handle the life of those assets more precisely.
Possible Solutions:

  • Make the buffer static. This is a short term solution since it doesn't allow loading from multiple threads.
  • Share a buffer pool.

The other issue is that the buffer grows to the largest asset.
A possible solution would be to load assets in chunks. For Texture2D this could be easy to do for color but I'm not so sure for compressed formats (DXT, etc). It might be tricky.

Thoughts?

@Jjagg
Copy link
Contributor

Jjagg commented Apr 13, 2017

@gap9269 How does this cause the OutOfMemoryException? The number of ContentManagers you have doesn't keep growing does it? The scratchbuffer will be GC'ed when its ContentManager is, so I don't get why memory keeps going up in your game.

Share a buffer pool.

We already have a ByteBufferPool class, so this wouldn't be too hard. It might need some minor adaptations like a minimum array size. Maybe we could offer a function to clear the pool to reduce ram usage when not loading assets. That could be done for the scratchbuffer as it is right now too, offer a function to discard it to clear memory.

A possible solution would be to load assets in chunks. For Texture2D this could be easy to do for color but I'm not so sure for compressed formats (DXT, etc). It might be tricky.

I think there wouldn't be any issues with compressed formats.

@gap9269
Copy link
Author

gap9269 commented Apr 13, 2017

I don't see a reason to do so since the scratch buffer will grow again with the next assets. This will defeat the purpose of the scratch buffer and just make your loading slower.

Right, I think I'm seeing the issue a bit more clearly now. This particular issue that I'm having is the result of two things:

  • One, the buffer grows to the largest asset, as you mentioned
  • Two, I use a lot of ContentManagers

These things together obviously create an issue over time. It seems like limiting the amount of ContentManagers in a project will prevent any major problems, but it is a bit difficult considering you can't choose to unload single assets. I can't think of a way to get around not having a specific ContentManager for menus, gameplay, etc if you want to be as efficient as possible with what assets you have loaded.

Even if you have a small handful of ContentManagers, you could still run into issues if you load giant assets for each one. Loading assets in smaller chunks would certainly help this problem.


@gap9269 How does this cause the OutOfMemoryException? The number of ContentManagers you have doesn't keep growing does it? The scratchbuffer will be GC'ed when its ContentManager is, so I don't get why memory keeps going up in your game.

We have a static amount of ContentManagers, but they aren't created as they're needed. At game load I create each Map (~200 or so) and each one has their own ContentManager. When you enter that map it loads all of the map's content, which increases the scratch buffer size to the largest asset it loads. When you leave the map it Unloads(), but it doesn't get rid of the ContentManager. Unless I dispose of the ContentManager myself or recreate it, it won't be GC'd.

A better solution than the one I mentioned in my last post is to simply share a single ContentManager between all Maps since only one is ever loaded at once anyway.

@nkast
Copy link
Contributor

nkast commented Apr 13, 2017

In that case, disposing the ContentManager when you unload the map and then recreate it when you need it will work. What I said above was in case you were reusing the same ContentManager on each map.

EDIT
@gap9269 Please note that you also need to set your manager = null. Simply disposing will not free the managed memory of the ScratchBuffer. #5655

@Jjagg
Copy link
Contributor

Jjagg commented Apr 13, 2017

I think using a pool of scratchbuffers is a good idea here. Most people will only use 1 at a time (like @gap9269), in which case only 1 buffer will be allocated.

@nkast
Copy link
Contributor

nkast commented Apr 13, 2017

@Jjagg Can we use your ByteBufferPool for this purpose? Originally I had a memory pool too, specially for the ContentManager. 032bd7e#diff-2cc0151b6c7e917bf7f4accecbd54741

@Jjagg
Copy link
Contributor

Jjagg commented Apr 13, 2017

We can use ByteBufferPool. Just need to add a minimum array size. I think it's fine other than that.

@gap9269
Copy link
Author

gap9269 commented Apr 13, 2017

I'm curious about how many people will even run into this issue - as you mentioned above @Jjagg, no one else seems to have had a problem. At least not openly.

I just switched to using a single ContentManager for maps and the problem is gone. Man, this one had been eating at me for months.

processmemory

Those garbage collections are me leaving an area, which would have caused an increase every time before this fix. Now it just sits flat until I load a larger asset, which I'll have to keep in mind going forward.

@gap9269
Copy link
Author

gap9269 commented Apr 13, 2017

What do you guys want to do with this issue? It isn't a Content.Unload() issue as the title implies, but it seems like there might be something that can be done to prevent people like me misusing ContentManagers and causing problems that are difficult to trace.

On the other hand, it's entirely preventable on the user's side so I'm not sure it warrants a lot of effort being put into making a fix.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants