ASP.NET Quiz - Does Page.Cache leak memory?

Yesterday I received an email from a blog reader about caching and memory leaks…

Paraphrasing freely it went something like this:

We use Page.Cache to store temporary data, but we have recently discovered that it causes high memory consumption. The bad thing is that the memory never goes down even though the cache items have expired, and we suspect a possible memory leak in its implementation.

We have created this simple page:

protected void Page_Load(object sender, EventArgs e){
        this.Page.Cache.Add(Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), null, DateTime.MaxValue, TimeSpan.FromMinutes(1), CacheItemPriority.NotRemovable, new
CacheItemRemovedCallback(this.OnRemoved));
}

public void OnRemoved(string key, object value, CacheItemRemovedReason r)
{
value = null;
}

Which we stress with ACT (Application Center Test) for 5 minutes. Memory usage peaks at 450 MB, and after some time it decreases to 253 MB, but never goes down completely even though we waited for 10 minutes after the stress test. Our expectation is that the memory should go down to about 50-60 MB.

The question is does the above scenario fall into the category of memory leaks?

Since I get quite a bit of emails I don’t have time to answer them privately, so if you email me and I don’t answer, and you really need urgent help, please contact Microsoft Support or comment on the blog.

However, if I think the question is common enough that the answer would benefit more people (which may or may not be true, but I’ll reserve the right to make that call:)) I’ll answer with a blog post like in this case.

I will never give out any names of people who email me or include any sensitive data on the blog such as machine names, custom classes or anything else that can identify the sender, but if you email me and do not want me to paraphrase your question on the blog, please let me know.

Back to the question... The short answer to the question is No (or at least not that I currently know of), however with a few minor changes to the code and the stress test you will see memory go down significantly.

So then why does memory not go down using this sample? And also, the test (when I ran it on my machine) generated a total of 15 161 requests, but the memory usage seems to indicate that a lot more than just the actual cached items stick around, why?

I will answer the question more thoroughly but before I do (later this week) I wanted to give all of you a chance to dissect this and see what things you come up with because I think it can bring up some nice discussion items, so feel free to comment away. 

Hint: With a subtle (but not too obvious change) I can get the memory usage to peak at a maximum of 48 MB on my machine, now that is a huge difference:).

The questions we want answered are:

  1. Why does memory usage not go down to about 50-60 MB?
  2. Why does it seem like we are using more memory than the actual items we store in cache?
  3. What makes the stress test “invalid”?
  4. What is the difference between Page.Cache and Cache?

I will summarize the comments and add any additional things I can think of on Thursday or Friday.   

Laters,

Comments

  • Anonymous
    August 08, 2006
    The comment has been removed
  • Anonymous
    August 08, 2006
    The comment has been removed
  • Anonymous
    August 08, 2006
    OnRemoved is an instance member which will force the Page object to remain reachable and not eligible for Garbage Collection.
  • Anonymous
    August 08, 2006
    My first instinct is the fact that every new request generates a brand new Cache instance (via the guid)... versus a more real world scenario where between 99% - 20% of the requests wouldn't allocate a new slot.

    So in theory 256MB is the timeout of the set of data that could be created in that space of time...  Your machine going to 50MB would be influenced to be direct relationship to how many times you can run a request.

    Thats my first instinct...
  • Anonymous
    August 08, 2006
    Nice comments both of them, the OnRemoved that Matt talks about is the subtle change I made (which alone made it go down to about 50 MB) so spot on Matt.  Eric's comment is also very valid. I didn't change the data I cached in my test, but in a real-life scenario that would be appropriate.

    So awesome stuff already, keep them comming...
  • Anonymous
    August 08, 2006
    This one seems really too simple - Why are we adding to the cache on EVERY page load?  Why not on just the initial load?  

  • Anonymous
    August 08, 2006
    I should clarify, the sample is used to simulate caching many items and is supposed to be used to determine if the cache is leaking, i.e. why cached objects are not released from memory after the cache item is expired.  

    In the real-world scenario the caching would only occur under certain conditions.  
  • Anonymous
    August 08, 2006
    The comment has been removed
  • Anonymous
    August 08, 2006
    I'm with Matt on this one.
  • Anonymous
    August 08, 2006
    A little off topic but is this scenerio also valid for the regular Cache object? i.e.

    Cache["something"] = "some value"

    When does this get clear? I have seen the behavior that the aspnet_wp.exe gets bigger upto 150MB (on not very busy site) and stays at that size.

    Any clue?

    Thanks,
    Rachit
  • Anonymous
    August 08, 2006
    My guess is that the memory usage did not drop because the cache objects where still alive and could not be removed from the cache to free up memory since they where marked as NotRemovable the last time the garbage collector ran.

    Even if one would wait 5 min after the stress test to be sure that all the timeouts expires the memory would not drop because nothing is causing the GC to run again. I believe Tess has blogged about having some sort of a slow down period in the end of the stress test to avoid stuff like that.

    But hey, I could be totaly off
  • Anonymous
    August 08, 2006
    The comment has been removed
  • Anonymous
    August 09, 2006
    But if non-removable is used, IIRC asp.net cannot remove that item due to memory pressure at all.  If that's the case then by using this setting don't you turn cache into a session like system that concievably has more memory problems than session itself (assuming its per-user)?  

    I've always thought that you should always let asp.net handle the cache stuff, freeing memory etc.. then build the logic into your app to repopulate the cache (or retrieve the data from elsewhere) if it can't find it in the cache.   The idea being if you use the cache and let asp.net free memory then its theoretically hard to run out of memory (well harder).  But if you use the cache and disallow asp.net memory management (for cached items) you're basically locking that memory away and making it unavailable to the rest of the app.
  • Anonymous
    August 09, 2006
    I would not use the cache to store session data since the cache lives in the scope of the application.

    I believe that there is just one cache per application and that Page.Cache and Cache points to that same instance of System.Web.Caching.Cache which is created when the application is started

    I have to disagree with Scott. I would say that it is ok to use the NotRemovable priority. After all I am caching objects and not just making a weak reference to them telling the GC it ok to collect it in shortage of memory. Hopefully I know what I am doing :)
  • Anonymous
    August 09, 2006
    Isn't the Page.Cache an application scope type of storage while the HttpContext.Current.Cache is per worker process?
  • Anonymous
    August 09, 2006
    Tess,
    Sorry for introducing the confusion.
    You asked: "...what is the difference between Page.Cache and Cache?"

    Well, I was talking about the Global cache ..ie. HttpContext.Current.Cache (and I know which is not this thread is about so sorry again).

    In my project (asp.net 1.1), I've set certain items (xmlfile, one small dataset, etc) in HttpContext.Current.Cache because they need to be accessed (same values) by different users (sessions). Nothing else. We use sessions, but they don't store any big size values...so don't know why it consumes that much RAM.

    On that note, why would somebody stores something in HttpContext.Current.Cache and not in Application object? Which one to choose when?

    BTW, this thread is becoming a good source for great info, no?
  • Anonymous
    August 09, 2006
    I thought the Page.Cache and HttpContext.Cache both point to the same Cache instance for the application.

    I also thought adding with the Cache["some key"] syntax just adds the item to the cache with the default settings (no expiration), while Cache.Add lets you specify the settings you want to add the cache object with.
  • Anonymous
    August 09, 2006
    I look it up in the MSDN documentation and found this under the remark section for the Cache class:

    One instance of this class is created per application domain, and it remains valid as long as the application domain remains active. Information about an instance of this class is available through the Cache property of the HttpContext object or the Cache property of the Page object.
  • Anonymous
    August 09, 2006
    I just love this discussion, lots of good comments and yes, the Page.Cache is the same as the "regular cache" (HttpContext.Current.Cache or Cache[".."]) so that it all applies.

    Good question about Cache vs. Application, I'll bring that up too in my summary later.  

  • Anonymous
    August 10, 2006
    If you look in Reflector, Page.Cache points to HttpContext.Current.Cache, which points to HttpRuntime.Cache....
  • Anonymous
    August 10, 2006
    I thought the Application variable concept was only there from ASP days and not really "recommended" for use in ASP.NET?
  • Anonymous
    August 10, 2006
    Gabe,

    Although, I'm not aware of what you mentioned that Application object is not recommended. If it's true, I would like to know why?
    Also, main question is what is so different in Application object that Cache has? Considering they provide similar functionality.

    Thx.
  • Anonymous
    August 10, 2006
    The comment has been removed
  • Anonymous
    August 10, 2006
    I thought I had read that somewhere but it took me a little bit to find it again.  Here is the quote and a link to the page that I found it on.

    "The HttpApplicationState collection used above is primarily meant for backward-compatibility with classic ASP and will be familiar to ASP developers. However, the use of static fields in ASP.NET is generally preferred over the use of HttpApplicationState."

    Near the bottom of the page:
    http://www.asp.net/QuickStart/aspnet/doc/applications/default.aspx

    Thanks
  • Anonymous
    August 10, 2006
    PingBack from http://blogs.msdn.com/tess/archive/2006/08/11/695268.aspx
  • Anonymous
    August 10, 2006
    RE Tess: <i>The NotRemovable, is not non-removable forever, only until the cache items expire, but that brings up an interesting question i think... when do these cache items expire using the code above?</i>

    Never? - When the OnRemoved callback gets called, the entry must still exist in the cache (no?), otherwise  OnRemoved could not give you access to it. By setting it to null, does this 'touch' the cached object reference, resetting the sliding timeout? If so (and please correct me), this explains a couple of things -
    1. The item is never removed from the cache, as the callback keeps firing and resetting the <b>sliding</b> timeout.
    2. The cache entry has a ref to the instance callback method, so we effectively 'cache' each instance of the method.

    Anyway, my brain hurts...
  • Anonymous
    August 14, 2006

     Announcing
     the Windows Mobile Virtual User Group Meeting [Via: trobbins ]
     Refactoring
    ...
  • Anonymous
    September 06, 2006
    In the Alps they mighty call this salt of the density drip. or the need for a higher latene cache coming off the hard drive.
  • Anonymous
    September 12, 2006
    The comment has been removed