Over lunch last week Mikhail Naganov (creator of the DevTools Heap Profiler) and I were discussing how invaluable it has been to have the same insight into JavaScript memory usage that we have into applications written in languages like C++ or Java. But the heap profiler doesn't seem to get as much attention from developers as I think it deserves. There could be two explanations: either leaking memory isn't a big problem for web sites or there is a problem but developers aren't aware of it.
Are memory leaks a problem?
For traditional pages where the user is encouraged to navigate from page to page, memory leaks should almost never be a problem. However, for any page that encourages interaction, memory management must be considered. Most realize that ultimately if too much memory is consumed the page will be killed, forcing the user to reload it. However, even before all memory is exhausted performance problems arise:
- A large JavaScript heap means garbage collections may take longer.
- Greater system memory pressure means fewer things can be cached (both in the browser and the OS).
- The OS may start paging or thrashing which can make the whole system feel sluggish.
A real world walkthrough
So, in order to demonstrate this is a real world problem and how easily the heap profiler can diagnose it, I set out to find a memory leak in the wild. A peak at the task manager (Wrench > Tools > Task Manager
) for my open tabs showed a good candidate for investigation: Facebook is consuming 410MB!!
Pinpoint the leaky action
The first step in finding a memory leak is to isolate the action that leaks. So I loaded facebook.com in a new tab. The fresh instance used only 49MB -- another indicator the 410MB might have been due to a leak. To observe memory use over time, I opened the Inspector's Timeline
panel, selected the Memory
tab and pressed the record button. At rest, the page displays a typical pattern of allocation and garbage collection. This is not a leak.
While keeping an eye on the graph, I began navigating around the site. I eventually noticed that each time I clicked the Events link on the left side, memory usage would rise but never be collected. This is how the usage grows as I repeatedly click the link. A quintessential leak.
As an aside, this leak isn't a browser bug. The OS task manager shows similar memory growth when performing the same action in Firefox.
Find the leaked memory
Now that we know we have a leak, the obvious next question is what is leaking. The heap profiler's ability to compare heap snapshots is the perfect tool to answer it. To use it, I reloaded a new instance and took a snapshot by clicking the heap snapshot button at the bottom of the Profiles
panel. Next, I performed the leaky action a prime number of times in hopes that it might be easy to spot. So I clicked Events 13 times and immediately took a second snapshot. To compare before and after, I highlighted the second snapshot and selected Comparison
view.
The comparison view displays the difference between any two snapshots. I sorted by delta to look for any objects that grew by the same number of times I clicked: 13. Sure enough, there were 13 more UIPagelets
on the heap after my clicks than before.
Expanding the UIPagelet
shows us each instance. Let's look at the first.
Each instance has an _element
property that points to a DOM node. Expanding that node, we can see that it is part of a detached DOM tree of 136 nodes. This means that 136 nodes are no longer visible in the page, but are being held alive by a JavaScript reference. There are legitimate reasons to do this, but it is also easy and common to do it by accident.
Note that all memory statistics reported by the tool reflect only the memory allocated in the JavaScript heap. This does not include native memory used by the DOM objects. So we cannot readily determine how much memory those 136 nodes are using. It all depends on their content -- for example leaking images can burn through memory very quickly.
Determine what prevents collection
After finding the leaked memory the last question is what is preventing it from being collected. To answer this we simply highlight any node and the retaining path will be shown (I typically change it to show paths to window objects instead of paths to GC roots). Here we see a very simple path. The UIPagelet
s are stored in a __UIControllerRegistry
object.
At first I wondered if this might intentionally keep DOM nodes alive as a cache. However, that doesn't seem to be the case. A search of the source JS shows several places where items are added to the __UIControllerRegistry
, but I couldn't find anywhere where they are cleaned up. So this appears to be a case where retaining the DOM nodes is purely accidental. The fix is to remove references to these nodes so they may be collected.
Takeaway
The point of the post is not that facebook has a leak. Facebook is an extremely well engineered site and large apps all deal with memory leaks from time to time. The point is to demonstrate how readily leaks can be diagnosed even with no knowledge of the source.
For anyone with an interactive web site, I highly recommend using your site for a few minutes with the memory timeline enabled to watch for any suspicious growth. If you have to solve any issues, the manual has excellent tutorials.
36 comments:
Great article! Thanks. :)
This is one of the advantages of Chrome (and IE9+) running every site as a separate process. If a site starts using a large amount of RAM, you can just close the tab and open it again. With other browsers, often you need to restart the whole browser.
Super article. Love the step-by-step example. I think most developers aren't looking at memory consumption. It'd be great to do a survey of top sites and see how many have memory issues.
Thanks for sharing this, especially for going through things step by step. It's made the process easy to follow and to apply elsewhere.
Very helpful article. This is a technique I will be using in the future with my own sites.
Hmmm, when I switch to "Comparison" view of my snapshots all I get is an empty list. That doesn't seem right.
awesome post man
"Greater system memory pressure means fewer things can be cached (both in the browser and the OS)."
But if the memory is being leaked, doesn't it mean its not being accessed by the program execution flow, and hence will exit the cache?
typo: "Registery"; good article, nevertheless
Excellent article, this is beyond useful and quite an interesting tool to be aware of. Thanks
@alexander I'm assuming you have (1) taken a snapshot (2) manually performed some action on the page (3) taken a second snapshot (4) highlighted the second snapshot and (5) clicked comparison. If that didn't work there could be a couple explanations:
- Either no memory was allocated and retained by the action in #2.
- There is a bug in the version of heap profiler you used. In the article I used the dev channel (14.0.835.29). If you are using an older version, try switching to dev channel. If you are using a newer version, please file a bug at http://new.crbug.com/.
@anonymous In garbage collected environments like JavaScript or Java the definition of a memory leak is generally considered slightly different from native environments like C++. In managed environments a leak is usually taken to mean memory that is still referenced (preventing it from being collected), but is no longer useful to the program. The good news is the reference makes them much easier to track down and fix than native memory leaks. Regarding my statement about cache eviction: many caches in browsers and operating systems will reduce their size when less system memory is available. If a page uses memory, it takes precedent and makes less memory available for other purposes like caching and other pages.
@anonymous Good spot on the typo. Fixed in post. Too bad spell check doesn't understand CamelCase.
Great ,thanks for sharing this article. We have trouble for javascript for a long time ,now we
can dig into the problem by your method ,thanks again.
I love it that we finally have tools to deal with this kind of heap problem. Firefox nithtlies' about:memory with compartments comes close. I shall have to try this walkthrough on my usual suspects: new twitter, google reader's infinite scrolling, new deviantArt.
Great Article!
Good to finally see a MAT like tool coming to javascript and the browser.
Is the source code of the tool available?
@Steve (Souders) detecting leaks is usually based on heuristics. As described in the article you typically redo the same interaction several times to get the leak to be large enough to be detected. One would have to automate this for each of the top sites.
That would complex and the results would not really be comparable.
*But* it think to could be possible to detect certain leaks such as " objects retained only be listener objects" and it should also be possible to compute certain Key performance indicators for the top sites.
For example memory usage, nr. of objects. redundancy (drop me a note if you are interested how this could work)
Regards,
Markus
Very nice post thanks for sharing
@Markus Kohler It is open source. The code is all in WebKit. You can find a pointer to the general areas in this bug: https://bugs.webkit.org/show_bug.cgi?id=53659
Great suggestion about automatically flagging any detached DOM trees held alive only by event listeners. As you seem to know, that is quite common and almost always an error. Perhaps it could be added to the Inspector's Audit panel or to Page Speed. I'll suggest it to Bryan McQuade.
By the way, I'm a huge fan of MAT! The WebKit heap profiler doesn't match its features yet, but it's a great start.
The frontend code is in WebKit, but the backend part that generates snapshots is in V8: http://code.google.com/p/v8/source/browse/branches/bleeding_edge
The main part is in 'src/profile-generator*' files.
@Markus Kohler I just opened a feature request https://bugs.webkit.org/show_bug.cgi?id=65992
@tony - Upgrading to the dev channel resolved the issue with blank results during a comparison. Thanks!
<3 Such a great post!
@Tony and @Mikhail,
Thanks for the hint!
I may get someone to work on javascript memory topics within the near future.
If so you will certainly hear from me again :-)
Regards,
Markus
@Tony - Awesome post. I've never really dug into the Chrome tools as much as I need to.
Out of curiousity I tried your memory profiling on a game we developed (http://entanglement.gopherwoodstudios.com). We've been having some issues where the game gets 'stutter-y' while playing it and I've never been able to figure out what what was behind it. Anyway, I ran the memory profile and initially it had the steady gain and drop that you describe of a normal website. After continuing to play the game, however, it started to have a very different memory graph. While the average height was still the same over time, the graph was much more volatile with memory increasing and decreasing in much shorter spans of time. Any ideas what would cause a graph like this?
Thanks,
Todd
sorry that deleaker does not work with xcode....
Great article, just adding one more link for chrome: http://code.google.com/chrome/devtools/docs/heap-profiling.html
You can actually get leaks that stay even across web pages in IE with a circular reference. Even in IE9 if you create an activeX document, create a pointer to it like window.namespace.doc, then assign doc.namespace = window.namespace, the whole JavaScript object namespace will stay in memory even as you leave the page
Thanks for this amazing article! It's also quite useful to start debugging chrome 'native' applications.
One thing that I notice with them is that when I hit reload, it doesn't actually "flush" all the memory. Is that intentional?
Thanks for this great article,
it really helps !
Great article! Help me a lot.
But I can't find the retaining path like 'DOMWindow@1235.listeners[34].handler["on click event"]'.
Great article! Help me a lot.
But I can't find the retaining path like 'DOMWindow@1235.listeners[34].handler["on click event"]'.
This post is great, it helped me a lot !
Thank your very much
The very reason why there's a continuous development happening in the cyberosphere. There are still applications and software which needs proper placement and finer furnishing to be used in the future times.
That is a great observation of memory leaks of web pages. Now I know why my favourite website loads slowly.
Great article.
Hi,
The images in the article is not visible
Is there any way I can dump the heap from the the c++?
Is there any way I can dump the heap from the the c++?
Post a Comment