|
-
Oct 18th, 2022, 05:05 PM
#1
Analyzing Memory Usage
I ran into something surprising today when a program I wrote to test a second program ended up with an Out Of Memory exception. That really shouldn't have happened. Sure, it was creating a complicated model, and was doing what would normally happen over the course of an entire year, or more, but I still felt that it shouldn't have run out of memory. Therefore, I delved into the profiling tools in VS, which have changed quite a bit since I last looked at them. What I'm asking about is specifically the .NET Object Allocation Tool and what to make of some results I'm seeing. I'm not sure if anybody has used this tool all that much, but here's the question:
There is an object that is created and passed as an argument to a method. Inside that method, the object gets passed around to a series of other methods. The object is just a vessel for a series of simple things which are usually just strings, Booleans, and so forth. It's an ugly object, so I won't show it here. The question applies as to this object, but there's a simpler object that is one of the members of the ugly object, and it makes a much nicer example for a question. This inner object has just two members, a GUID and a Date. This is added to the ugly object such that it can be used by the methods that get that ugly object. This inner object can be called EW, for convenience.
When looking at the profile, I was a bit surprised to see that there were 53,694 EW objects in the allocations. I think this makes sense. As it turns out, EW objects are a member of the ugly object, but are also members of some other objects that are used in events. The model can spawn a LOT of events, especially when I'm simulating a years worth of use. However, I would expect that virtually all of these EW objects would simply be unreachable. Once the event that held them is over, nobody is holding references to them, and the event argument is now gone.
So, the first question is: Is there any way to determine, in the output of the allocation tool, whether an object is still reachable, or not? I don't see a means.
I have an outrageous number of lists and dictionaries, but most of them are ephemeral, like that EW object that is just used as a member of some event argument. It's just a vessel to pass a GUID and a Date around for the use of whoever wants it. Once the event is over, the vessel is no longer of any use, and should be unreachable. I expect it to remain in memory, because I understand that it is easier to allocate new than to recover old, so perhaps what I'm seeing is 85,500 Dictionaries, of which a dozen are used and the rest are unreachable, and so forth.
If that's the case, then why am I getting an Out Of Memory exception? Sure, there are hundreds of thousands of items allocated. Heck, there are over 68,000 GUIDs allocated, but virtually all of them are unreachable. Shouldn't the GC automatically recover memory rather than throwing an exception?
Of course, if those objects are still held by something, then they can't be recovered, but that really shouldn't be the case.
My usual boring signature: Nothing
 
-
Oct 21st, 2022, 05:31 PM
#2
Re: Analyzing Memory Usage
I did a whole lot more studying of this and learning the tools. I created a simpler scenario and used the .NET Object Allocation tool that is available in 2022. It's pretty nice, though somewhat a work in progress, too. Specifically, I created a simpler scenario that created a bunch of objects, and should have discarded them. I then added a GC.Collect line which would do two things. The first thing it would do would be to give me an easily recognizable point in the allocation tracking. A hard Collect call like that stands out like a flag (a red flag, to be precise) in the Object delta graph. The second thing it would do for me would be to collect everything that could be collected.
The tool allows you to see the results of any collection. It shows a pie chart of the top collected items and a pie chart of the top survived types. Half of the survived types were strings, which didn't surprise me any. In fact, one thing I found was that the top two or three memory hogs were Fake classes created for testing that did nothing other than logging what was called. That was nice, because it means that those can largely be ignored. They aren't representative of the model in actual use.
The model that was being tested is essentially a series of one or more nodes connected by spokes, which is a common pattern that shows up in a lot of programming problems. Whenever a new spoke is going to be added, the model clones itself, the new spoke is added to the clone, the model is validated, and if it passes, the original is replaced by the clone. This design has an obvious issue. Each new spoke requires that a clone be made of every existing spoke. I added 100 spokes, so the first time...there was nothing to clone, so that was one spoke. The second time a spoke was added, the existing spoke was cloned and a new spoke was added. The third time, then the two existing spokes were cloned and a new spoke was added, and so forth for 100 spokes.
Therefore, a LOT of spokes got created in this test, but at the end, only 100 spokes should have been reachable. I was surprised to see that spokes made it into the Top Survived Types pie chart. In fact, spokes make up over 25% of the survived types. There are a couple slight deficiencies in this chart, though, because it's showing the top 5 object types, but it doesn't tell me what percentage of the total types those five make up. There are 44,333 objects that survived the GC.Collect, so if it was JUST those 5, that would mean somewhere over 11,000 spokes survived, but only about 5000 should have ever been created. However, since I don't know what those top 5 represent, that number could be anything.
It would be nice if I could see how many objects those are, and perhaps I can, but I'm not understanding what I'm seeing. If I click on one of the slices in the pie chart, I see allocations, and there are 100 spokes, the exact number that I feel should have survived, but if I sort the allocations by number or size, those spokes aren't in the top five. In fact, they are 13th on the list, so I'm not quite sure what that list holds. It isn't objects that survived the collection, because there are over 44K of those, and the list holds maybe 30K. Not sure what to make of that.
Another deficiency of the pie chart is that, since it only shows the top 5 for recovered and survived, I don't see a means to see how many spokes got recovered. I can select a window to look at, and see that almost the exact number of spokes are created that I would expect (I expected 5,050, but see 5,150), and there's that list that suggests that only 100 remain, but the list that suggests that only 100 remain shows up only if I click the pie slice for spokes remaining. I can't get to that list by any other means that I have found, so I'm not quite sure what that list means. In fact, I can't always get to the list by clicking on the pie chart. It seems to depend on what window of time I have selected on the Object Delta graph.
There are also items on that list that I get to by clicking the pie chart that don't look like objects at all. The largest single allocation item is an ArrayListEnumeratorSimple, which is fine, as I suppose that would be an object, but there is also GetOneAU, which is a method in the model, not what I would expect to be an object.
There's a lot of information, but I can't say I fully understand it, yet.
My usual boring signature: Nothing
 
Posting Permissions
- You may not post new threads
- You may not post replies
- You may not post attachments
- You may not edit your posts
-
Forum Rules
|
Click Here to Expand Forum to Full Width
|