Bug 732820 - Part 2: Cap the amount of discardable image data we'll willingly keep around. r=joe
authorJustin Lebar <justin.lebar@gmail.com>
Fri, 09 Mar 2012 17:21:01 -0500
changeset 91935 f7890083256286e88623561df39ac3903a5a48ac
parent 91934 45d2f5e2fe317cff073fdc206cfa5b154501c785
child 91936 6184b50776fc79cf5b76c60734cdf646ceee745f
push id136
push userlsblakk@mozilla.com
push dateFri, 01 Jun 2012 02:39:32 +0000
treeherdermozilla-release@7ebf7352c959 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjoe
bugs732820
milestone13.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 732820 - Part 2: Cap the amount of discardable image data we'll willingly keep around. r=joe
image/src/DiscardTracker.cpp
image/src/DiscardTracker.h
image/src/RasterImage.cpp
image/src/RasterImage.h
image/src/imgFrame.cpp
image/src/imgFrame.h
modules/libpref/src/init/all.js
--- a/image/src/DiscardTracker.cpp
+++ b/image/src/DiscardTracker.cpp
@@ -1,245 +1,198 @@
-/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
-   ***** BEGIN LICENSE BLOCK *****
- * Version: MPL 1.1/GPL 2.0/LGPL 2.1
- *
- * The contents of this file are subject to the Mozilla Public License Version
- * 1.1 (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- * http://www.mozilla.org/MPL/
- *
- * Software distributed under the License is distributed on an "AS IS" basis,
- * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
- * for the specific language governing rights and limitations under the
- * License.
- *
- * The Original Code is mozilla.org code.
- *
- * The Initial Developer of the Original Code is Mozilla Foundation.
- * Portions created by the Initial Developer are Copyright (C) 2010
- * the Initial Developer. All Rights Reserved.
- *
- * Contributor(s):
- *   Bobby Holley <bobbyholley@gmail.com>
- *
- * Alternatively, the contents of this file may be used under the terms of
- * either the GNU General Public License Version 2 or later (the "GPL"), or
- * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
- * in which case the provisions of the GPL or the LGPL are applicable instead
- * of those above. If you wish to allow use of your version of this file only
- * under the terms of either the GPL or the LGPL, and not to allow others to
- * use your version of this file under the terms of the MPL, indicate your
- * decision by deleting the provisions above and replace them with the notice
- * and other provisions required by the GPL or the LGPL. If you do not delete
- * the provisions above, a recipient may use your version of this file under
- * the terms of any one of the MPL, the GPL or the LGPL.
- *
- * ***** END LICENSE BLOCK ***** */
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "nsComponentManagerUtils.h"
 #include "nsITimer.h"
 #include "RasterImage.h"
 #include "DiscardTracker.h"
 #include "mozilla/Preferences.h"
 
 namespace mozilla {
 namespace image {
 
-static bool sInitialized = false;
-static bool sTimerOn = false;
-static PRUint32 sMinDiscardTimeoutMs = 10000; /* Default if pref unreadable. */
-static nsITimer *sTimer = nsnull;
-static struct DiscardTrackerNode sHead, sSentinel, sTail;
+static const char* sDiscardTimeoutPref = "image.mem.min_discard_timeout_ms";
+
+/* static */ LinkedList<DiscardTracker::Node> DiscardTracker::sDiscardableImages;
+/* static */ nsCOMPtr<nsITimer> DiscardTracker::sTimer;
+/* static */ bool DiscardTracker::sInitialized = false;
+/* static */ bool DiscardTracker::sTimerOn = false;
+/* static */ bool DiscardTracker::sDiscardRunnablePending = false;
+/* static */ ssize_t DiscardTracker::sCurrentDecodedImageBytes = 0;
+/* static */ PRUint32 DiscardTracker::sMinDiscardTimeoutMs = 10000;
+/* static */ PRUint32 DiscardTracker::sMaxDecodedImageKB = 42 * 1024;
 
 /*
- * Puts an image in the back of the tracker queue. If the image is already
- * in the tracker, this removes it first.
+ * When we notice we're using too much memory for decoded images, we enqueue a
+ * DiscardRunnable, which runs this code.
  */
-nsresult
-DiscardTracker::Reset(DiscardTrackerNode *node)
+NS_IMETHODIMP
+DiscardTracker::DiscardRunnable::Run()
 {
-  nsresult rv;
-#ifdef DEBUG
-  bool isSentinel = (node == &sSentinel);
+  sDiscardRunnablePending = false;
+  DiscardTracker::DiscardNow();
+  return NS_OK;
+}
 
-  // Sanity check the node.
-  NS_ABORT_IF_FALSE(isSentinel || node->curr, "Node doesn't point to anything!");
-
-  // We should not call this function if we can't discard
-  NS_ABORT_IF_FALSE(isSentinel || node->curr->CanDiscard(),
-                    "trying to reset discarding but can't discard!");
+int
+DiscardTimeoutChangedCallback(const char* aPref, void *aClosure)
+{
+  DiscardTracker::ReloadTimeout();
+  return 0;
+}
 
-  // As soon as an image becomes animated it is set non-discardable
-  NS_ABORT_IF_FALSE(isSentinel || !node->curr->mAnim,
-                    "Trying to reset discarding on animated image!");
-#endif
+nsresult
+DiscardTracker::Reset(Node *node)
+{
+  // We shouldn't call Reset() with a null |img| pointer, on images which can't
+  // be discarded, or on animated images (which should be marked as
+  // non-discardable, anyway).
+  MOZ_ASSERT(node->img);
+  MOZ_ASSERT(node->img->CanDiscard());
+  MOZ_ASSERT(!node->img->mAnim);
 
-  // Initialize the first time through
+  // Initialize the first time through.
+  nsresult rv;
   if (NS_UNLIKELY(!sInitialized)) {
     rv = Initialize();
     NS_ENSURE_SUCCESS(rv, rv);
   }
 
-  // Remove the node if it's in the list.
-  Remove(node);
+  // Insert the node at the front of the list and note when it was inserted.
+  bool wasInList = node->isInList();
+  if (wasInList) {
+    node->remove();
+  }
+  node->timestamp = TimeStamp::Now();
+  sDiscardableImages.insertFront(node);
 
-  // Append it to the list.
-  node->prev = sTail.prev;
-  node->next = &sTail;
-  node->prev->next = sTail.prev = node;
+  // If the node wasn't already in the list of discardable images, then we may
+  // need to discard some images to stay under the sMaxDecodedImageKB limit.
+  // Call MaybeDiscardSoon to do this check.
+  if (!wasInList) {
+    MaybeDiscardSoon();
+  }
 
-  // Make sure the timer is running
-  rv = TimerOn();
+  // Make sure the timer is running.
+  rv = EnableTimer();
   NS_ENSURE_SUCCESS(rv,rv);
 
   return NS_OK;
 }
 
-/*
- * Removes a node from the tracker. No-op if the node is currently untracked.
+void
+DiscardTracker::Remove(Node *node)
+{
+  if (node->isInList())
+    node->remove();
+
+  if (sDiscardableImages.isEmpty())
+    DisableTimer();
+}
+
+/**
+ * Shut down the tracker, deallocating the timer.
  */
 void
-DiscardTracker::Remove(DiscardTrackerNode *node)
+DiscardTracker::Shutdown()
 {
-  NS_ABORT_IF_FALSE(node != nsnull, "Can't pass null node");
-
-  // If we're not in a list, we have nothing to do.
-  if ((node->prev == nsnull) || (node->next == nsnull)) {
-    NS_ABORT_IF_FALSE(node->prev == node->next,
-                      "Node is half in a list!");
-    return;
+  if (sTimer) {
+    sTimer->Cancel();
+    sTimer = NULL;
   }
-
-  // Connect around ourselves
-  node->prev->next = node->next;
-  node->next->prev = node->prev;
-
-  // Clean up the node we removed.
-  node->prev = node->next = nsnull;
 }
 
 /*
  * Discard all the images we're tracking.
  */
 void
 DiscardTracker::DiscardAll()
 {
   if (!sInitialized)
     return;
 
-  // Remove the sentinel from the list so that the only elements in the list
-  // which don't track an image are the head and tail.
-  Remove(&sSentinel);
+  sDiscardableImages.clear();
 
-  // Discard all tracked images.
-  for (DiscardTrackerNode *node = sHead.next;
-       node != &sTail; node = sHead.next) {
-    NS_ABORT_IF_FALSE(node->curr, "empty node!");
-    Remove(node);
-    node->curr->Discard();
-  }
-
-  // Add the sentinel back to the (now empty) list.
-  Reset(&sSentinel);
-
-  // Because the sentinel is the only element in the list, the next timer event
-  // would be a no-op.  Disable the timer as an optimization.
-  TimerOff();
+  // The list is empty, so there's no need to leave the timer on.
+  DisableTimer();
 }
 
-static int
-DiscardTimeoutChangedCallback(const char* aPref, void *aClosure)
+void
+DiscardTracker::InformAllocation(ssize_t bytes)
 {
-  DiscardTracker::ReloadTimeout();
-  return 0;
+  // This function is called back e.g. from RasterImage::Discard(); be careful!
+
+  sCurrentDecodedImageBytes += bytes;
+  MOZ_ASSERT(sCurrentDecodedImageBytes >= 0);
+
+  // If we're using too much memory for decoded images, MaybeDiscardSoon will
+  // enqueue a callback to discard some images.
+  MaybeDiscardSoon();
 }
 
 /**
  * Initialize the tracker.
  */
 nsresult
 DiscardTracker::Initialize()
 {
-  nsresult rv;
-
-  // Set up the list. Head<->Sentinel<->Tail
-  sHead.curr = sTail.curr = sSentinel.curr = nsnull;
-  sHead.prev = sTail.next = nsnull;
-  sHead.next = sTail.prev = &sSentinel;
-  sSentinel.prev = &sHead;
-  sSentinel.next = &sTail;
-
   // Watch the timeout pref for changes.
   Preferences::RegisterCallback(DiscardTimeoutChangedCallback,
-                                DISCARD_TIMEOUT_PREF);
+                                sDiscardTimeoutPref);
 
-  ReloadTimeout();
+  Preferences::AddUintVarCache(&sMaxDecodedImageKB,
+                              "image.mem.max_decoded_image_kb",
+                              50 * 1024);
 
-  // Create and start the timer
-  nsCOMPtr<nsITimer> t = do_CreateInstance("@mozilla.org/timer;1");
-  NS_ENSURE_TRUE(t, NS_ERROR_OUT_OF_MEMORY);
-  t.forget(&sTimer);
-  rv = TimerOn();
-  NS_ENSURE_SUCCESS(rv, rv);
+  // Create the timer.
+  sTimer = do_CreateInstance("@mozilla.org/timer;1");
+
+  // Read the timeout pref and start the timer.
+  ReloadTimeout();
 
   // Mark us as initialized
   sInitialized = true;
 
   return NS_OK;
 }
 
 /**
- * Shut down the tracker, deallocating the timer.
- */
-void
-DiscardTracker::Shutdown()
-{
-  if (sTimer) {
-    sTimer->Cancel();
-    NS_RELEASE(sTimer);
-    sTimer = nsnull;
-  }
-}
-
-/**
  * Read the discard timeout from about:config.
  */
 void
 DiscardTracker::ReloadTimeout()
 {
-  nsresult rv;
-
-  // read the timeout pref
+  // Read the timeout pref.
   PRInt32 discardTimeout;
-  rv = Preferences::GetInt(DISCARD_TIMEOUT_PREF, &discardTimeout);
+  nsresult rv = Preferences::GetInt(sDiscardTimeoutPref, &discardTimeout);
 
-  // If we got something bogus, return
+  // If we got something bogus, return.
   if (!NS_SUCCEEDED(rv) || discardTimeout <= 0)
     return;
 
-  // If the value didn't change, return
+  // If the value didn't change, return.
   if ((PRUint32) discardTimeout == sMinDiscardTimeoutMs)
     return;
 
-  // Update the value
+  // Update the value.
   sMinDiscardTimeoutMs = (PRUint32) discardTimeout;
 
-  // If the timer's on, restart the clock to make changes take effect
-  if (sTimerOn) {
-    TimerOff();
-    TimerOn();
-  }
+  // Restart the timer so the new timeout takes effect.
+  DisableTimer();
+  EnableTimer();
 }
 
 /**
  * Enables the timer. No-op if the timer is already running.
  */
 nsresult
-DiscardTracker::TimerOn()
+DiscardTracker::EnableTimer()
 {
   // Nothing to do if the timer's already on.
   if (sTimerOn)
     return NS_OK;
   sTimerOn = true;
 
   // Activate
   return sTimer->InitWithFuncCallback(TimerCallback,
@@ -247,47 +200,77 @@ DiscardTracker::TimerOn()
                                       sMinDiscardTimeoutMs,
                                       nsITimer::TYPE_REPEATING_SLACK);
 }
 
 /*
  * Disables the timer. No-op if the timer isn't running.
  */
 void
-DiscardTracker::TimerOff()
+DiscardTracker::DisableTimer()
 {
   // Nothing to do if the timer's already off.
   if (!sTimerOn)
     return;
   sTimerOn = false;
 
   // Deactivate
   sTimer->Cancel();
 }
 
 /**
- * Routine activated when the timer fires. This discards everything
- * in front of sentinel, and resets the sentinel to the back of the
- * list.
+ * Routine activated when the timer fires. This discards everything that's
+ * older than sMinDiscardTimeoutMs, and tries to discard enough images so that
+ * we go under sMaxDecodedImageKB.
  */
 void
 DiscardTracker::TimerCallback(nsITimer *aTimer, void *aClosure)
 {
-  DiscardTrackerNode *node;
+  DiscardNow();
+}
+
+void
+DiscardTracker::DiscardNow()
+{
+  // Assuming the list is ordered with oldest discard tracker nodes at the back
+  // and newest ones at the front, iterate from back to front discarding nodes
+  // until we encounter one which is new enough to keep and until we go under
+  // our sMaxDecodedImageKB limit.
 
-  // Remove and discard everything before the sentinel
-  for (node = sSentinel.prev; node != &sHead; node = sSentinel.prev) {
-    NS_ABORT_IF_FALSE(node->curr, "empty node!");
-    Remove(node);
-    node->curr->Discard();
+  TimeStamp now = TimeStamp::Now();
+  Node* node;
+  while ((node = sDiscardableImages.getLast())) {
+    if ((now - node->timestamp).ToMilliseconds() > sMinDiscardTimeoutMs ||
+        sCurrentDecodedImageBytes > sMaxDecodedImageKB * 1024) {
+
+      // Discarding the image should cause sCurrentDecodedImageBytes to
+      // decrease via a call to InformAllocation().
+      node->img->Discard();
+
+      // Careful: Discarding may have caused the node to have been removed
+      // from the list.
+      Remove(node);
+    }
+    else {
+      break;
+    }
   }
 
-  // Append the sentinel to the back of the list
-  Reset(&sSentinel);
+  // If the list is empty, disable the timer.
+  if (sDiscardableImages.isEmpty())
+    DisableTimer();
+}
 
-  // If there's nothing in front of the sentinel, the next callback
-  // is guaranteed to be a no-op. Disable the timer as an optimization.
-  if (sSentinel.prev == &sHead)
-    TimerOff();
+void
+DiscardTracker::MaybeDiscardSoon()
+{
+  // Are we carrying around too much decoded image data?  If so, enqueue an
+  // event to try to get us down under our limit.
+  if (sCurrentDecodedImageBytes > sMaxDecodedImageKB * 1024 &&
+      !sDiscardableImages.isEmpty() && !sDiscardRunnablePending) {
+    sDiscardRunnablePending = true;
+    nsRefPtr<DiscardRunnable> runnable = new DiscardRunnable();
+    NS_DispatchToCurrentThread(runnable);
+  }
 }
 
 } // namespace image
 } // namespace mozilla
--- a/image/src/DiscardTracker.h
+++ b/image/src/DiscardTracker.h
@@ -1,90 +1,116 @@
-/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
-   ***** BEGIN LICENSE BLOCK *****
- * Version: MPL 1.1/GPL 2.0/LGPL 2.1
- *
- * The contents of this file are subject to the Mozilla Public License Version
- * 1.1 (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- * http://www.mozilla.org/MPL/
- *
- * Software distributed under the License is distributed on an "AS IS" basis,
- * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
- * for the specific language governing rights and limitations under the
- * License.
- *
- * The Original Code is mozilla.org code.
- *
- * The Initial Developer of the Original Code is Mozilla Foundation.
- * Portions created by the Initial Developer are Copyright (C) 2010
- * the Initial Developer. All Rights Reserved.
- *
- * Contributor(s):
- *   Bobby Holley <bobbyholley@gmail.com>
- *
- * Alternatively, the contents of this file may be used under the terms of
- * either the GNU General Public License Version 2 or later (the "GPL"), or
- * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
- * in which case the provisions of the GPL or the LGPL are applicable instead
- * of those above. If you wish to allow use of your version of this file only
- * under the terms of either the GPL or the LGPL, and not to allow others to
- * use your version of this file under the terms of the MPL, indicate your
- * decision by deleting the provisions above and replace them with the notice
- * and other provisions required by the GPL or the LGPL. If you do not delete
- * the provisions above, a recipient may use your version of this file under
- * the terms of any one of the MPL, the GPL or the LGPL.
- *
- * ***** END LICENSE BLOCK ***** */
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #ifndef mozilla_imagelib_DiscardTracker_h_
 #define mozilla_imagelib_DiscardTracker_h_
 
-#define DISCARD_TIMEOUT_PREF "image.mem.min_discard_timeout_ms"
+#include "mozilla/LinkedList.h"
+#include "mozilla/TimeStamp.h"
 
 class nsITimer;
 
 namespace mozilla {
 namespace image {
+
 class RasterImage;
 
-// Struct to make a RasterImage insertable into the tracker list. This
-// is embedded within each RasterImage object, and we do 'this->curr = this'
-// on RasterImage construction. Thus, a RasterImage must always call
-// DiscardTracker::Remove() in its destructor to avoid having the tracker
-// point to bogus memory.
-struct DiscardTrackerNode
-{
-  // Pointer to the RasterImage that this node tracks
-  RasterImage *curr;
-
-  // Pointers to the previous and next nodes in the list
-  DiscardTrackerNode *prev, *next;
-};
-
 /**
- * This static class maintains a linked list of RasterImage nodes. When Reset()
- * is called, the node is removed from its position in the list (if it was there
- * before) and appended to the end. When Remove() is called, the node is removed
- * from the list. The timer fires once every MIN_DISCARD_TIMEOUT_MS ms. When it
- * does, it calls Discard() on each container preceding it, and then appends
- * itself to the end of the list. Thus, the discard timeout varies between
- * MIN_DISCARD_TIMEOUT_MS and 2*MIN_DISCARD_TIMEOUT_MS.
+ * This static class maintains a linked list of RasterImage objects which are
+ * eligible for discarding.
+ *
+ * When Reset() is called, the node is removed from its position in the list
+ * (if it was there before) and appended to the beginnings of the list.
+ *
+ * Periodically (on a timer and when we notice that we're using more memory
+ * than we'd like for decoded images), we go through the list and discard
+ * decoded data from images at the end of the list.
  */
 class DiscardTracker
 {
   public:
-    static nsresult Reset(struct DiscardTrackerNode *node);
-    static void Remove(struct DiscardTrackerNode *node);
+    /**
+     * The DiscardTracker keeps a linked list of Node objects.  Each object
+     * points to a RasterImage and contains a timestamp indicating when the
+     * node was inserted into the tracker.
+     *
+     * This structure is embedded within each RasterImage object, and we do
+     * |mDiscardTrackerNode.img = this| on RasterImage construction.  Thus, a
+     * RasterImage must always call DiscardTracker::Remove() in its destructor
+     * to avoid having the tracker point to bogus memory.
+     */
+    struct Node : public LinkedListElement<Node>
+    {
+      RasterImage *img;
+      TimeStamp timestamp;
+    };
+
+    /**
+     * Add an image to the front of the tracker's list, or move it to the front
+     * if it's already in the list.
+     */
+    static nsresult Reset(struct Node* node);
+
+    /**
+     * Remove a node from the tracker; do nothing if the node is currently
+     * untracked.
+     */
+    static void Remove(struct Node* node);
+
+    /**
+     * Shut the discard tracker down.  This should be called on XPCOM shutdown
+     * so we destroy the discard timer's nsITimer.
+     */
     static void Shutdown();
-    static void ReloadTimeout();
+
+    /**
+     * Discard the decoded image data for all images tracked by the discard
+     * tracker.
+     */
     static void DiscardAll();
+
+    /**
+     * Inform the discard tracker that we've allocated or deallocated some
+     * memory for a decoded image.  We use this to determine when we've
+     * allocated too much memory and should discard some images.
+     */
+    static void InformAllocation(ssize_t bytes);
+
   private:
+    /**
+     * This is called when the discard timer fires; it calls into DiscardNow().
+     */
+    friend int DiscardTimeoutChangedCallback(const char* aPref, void *aClosure);
+
+    /**
+     * When run, this runnable sets sDiscardRunnablePending to false and calls
+     * DiscardNow().
+     */
+    class DiscardRunnable : public nsRunnable
+    {
+      NS_IMETHOD Run();
+    };
+
     static nsresult Initialize();
-    static nsresult TimerOn();
-    static void TimerOff();
+    static void ReloadTimeout();
+    static nsresult EnableTimer();
+    static void DisableTimer();
+    static void MaybeDiscardSoon();
     static void TimerCallback(nsITimer *aTimer, void *aClosure);
+    static void DiscardNow();
+
+    static LinkedList<Node> sDiscardableImages;
+    static nsCOMPtr<nsITimer> sTimer;
+    static bool sInitialized;
+    static bool sTimerOn;
+    static bool sDiscardRunnablePending;
+    static ssize_t sCurrentDecodedImageBytes;
+    static PRUint32 sMinDiscardTimeoutMs;
+    static PRUint32 sMaxDecodedImageKB;
 };
 
 } // namespace image
 } // namespace mozilla
 
 #endif /* mozilla_imagelib_DiscardTracker_h_ */
--- a/image/src/RasterImage.cpp
+++ b/image/src/RasterImage.cpp
@@ -209,18 +209,17 @@ RasterImage::RasterImage(imgStatusTracke
   mDiscardable(false),
   mHasSourceData(false),
   mDecoded(false),
   mHasBeenDecoded(false),
   mInDecoder(false),
   mAnimationFinished(false)
 {
   // Set up the discard tracker node.
-  mDiscardTrackerNode.curr = this;
-  mDiscardTrackerNode.prev = mDiscardTrackerNode.next = nsnull;
+  mDiscardTrackerNode.img = this;
   Telemetry::GetHistogramById(Telemetry::IMAGE_DECODE_COUNT)->Add(0);
 
   // Statistics
   num_containers++;
 
   // Register our pref observers if we haven't yet.
   if (NS_UNLIKELY(!gInitializedPrefCaches)) {
     InitPrefCaches();
@@ -2216,17 +2215,17 @@ RasterImage::CanForciblyDiscard() {
   return mDiscardable &&         // ...Enabled at creation time...
          mHasSourceData;         // ...have the source data...
 }
 
 // Helper method to tell us whether the clock is currently running for
 // discarding this image. Mainly for assertions.
 bool
 RasterImage::DiscardingActive() {
-  return !!(mDiscardTrackerNode.prev || mDiscardTrackerNode.next);
+  return mDiscardTrackerNode.isInList();
 }
 
 // Helper method to determine if we're storing the source data in a buffer
 // or just writing it directly to the decoder
 bool
 RasterImage::StoringSourceData() const {
   return (mDecodeOnDraw || mDiscardable);
 }
--- a/image/src/RasterImage.h
+++ b/image/src/RasterImage.h
@@ -629,17 +629,17 @@ private: // data
   //! # loops remaining before animation stops (-1 no stop)
   PRInt32                    mLoopCount;
   
   //! imgIDecoderObserver
   nsWeakPtr                  mObserver;
 
   // Discard members
   PRUint32                   mLockCount;
-  DiscardTrackerNode         mDiscardTrackerNode;
+  DiscardTracker::Node       mDiscardTrackerNode;
 
   // Source data members
   FallibleTArray<char>       mSourceData;
   nsCString                  mSourceDataMimeType;
   nsCString                  mURIString;
 
   friend class DiscardTracker;
 
--- a/image/src/imgFrame.cpp
+++ b/image/src/imgFrame.cpp
@@ -32,16 +32,17 @@
  * decision by deleting the provisions above and replace them with the notice
  * and other provisions required by the GPL or the LGPL. If you do not delete
  * the provisions above, a recipient may use your version of this file under
  * the terms of any one of the MPL, the GPL or the LGPL.
  *
  * ***** END LICENSE BLOCK ***** */
 
 #include "imgFrame.h"
+#include "DiscardTracker.h"
 
 #include <limits.h>
 
 #include "prmem.h"
 #include "prenv.h"
 
 #include "gfxPlatform.h"
 #include "gfxUtils.h"
@@ -62,16 +63,18 @@ static PRUint32 gTotalDDBs = 0;
 static PRUint32 gTotalDDBSize = 0;
 // only use up a maximum of 64MB in DDBs
 #define kMaxDDBSize (64*1024*1024)
 // and don't let anything in that's bigger than 4MB
 #define kMaxSingleDDBSize (4*1024*1024)
 
 #endif
 
+using namespace mozilla::image;
+
 // Returns true if an image of aWidth x aHeight is allowed and legal.
 static bool AllowedImageSize(PRInt32 aWidth, PRInt32 aHeight)
 {
   // reject over-wide or over-tall images
   const PRInt32 k64KLimit = 0x0000FFFF;
   if (NS_UNLIKELY(aWidth > k64KLimit || aHeight > k64KLimit )) {
     NS_WARNING("image too big");
     return false;
@@ -142,16 +145,17 @@ imgFrame::imgFrame() :
   mSinglePixel(false),
   mNeverUseDeviceSurface(false),
   mFormatChanged(false),
   mCompositingFailed(false)
 #ifdef USE_WIN_SURFACE
   , mIsDDBSurface(false)
 #endif
   , mLocked(false)
+  , mInformedDiscardTracker(false)
 {
   static bool hasCheckedOptimize = false;
   if (!hasCheckedOptimize) {
     if (PR_GetEnv("MOZ_DISABLE_IMAGE_OPTIMIZE")) {
       gDisableOptimize = true;
     }
     hasCheckedOptimize = true;
   }
@@ -161,16 +165,20 @@ imgFrame::~imgFrame()
 {
   PR_FREEIF(mPalettedImageData);
 #ifdef USE_WIN_SURFACE
   if (mIsDDBSurface) {
       gTotalDDBs--;
       gTotalDDBSize -= mSize.width * mSize.height * 4;
   }
 #endif
+
+  if (mInformedDiscardTracker) {
+    DiscardTracker::InformAllocation(-4 * mSize.height * mSize.width);
+  }
 }
 
 nsresult imgFrame::Init(PRInt32 aX, PRInt32 aY, PRInt32 aWidth, PRInt32 aHeight, 
                         gfxASurface::gfxImageFormat aFormat, PRUint8 aPaletteDepth /* = 0 */)
 {
   // assert for properties that should be verified by decoders, warn for properties related to bad content
   if (!AllowedImageSize(aWidth, aHeight))
     return NS_ERROR_FAILURE;
@@ -222,16 +230,24 @@ nsresult imgFrame::Init(PRInt32 aX, PRIn
 
 #ifdef XP_MACOSX
     if (!mNeverUseDeviceSurface && !ShouldUseImageSurfaces()) {
       mQuartzSurface = new gfxQuartzImageSurface(mImageSurface);
     }
 #endif
   }
 
+  // Inform the discard tracker that we've allocated some memory, but only if
+  // we're not a paletted image (paletted images are not usually large and are
+  // used only for animated frames, which we don't discard).
+  if (!mPalettedImageData) {
+    DiscardTracker::InformAllocation(4 * mSize.width * mSize.height);
+    mInformedDiscardTracker = true;
+  }
+
   return NS_OK;
 }
 
 nsresult imgFrame::Optimize()
 {
   if (gDisableOptimize)
     return NS_OK;
 
@@ -266,16 +282,24 @@ nsresult imgFrame::Optimize()
         mImageSurface = nsnull;
         mOptSurface = nsnull;
 #ifdef USE_WIN_SURFACE
         mWinSurface = nsnull;
 #endif
 #ifdef XP_MACOSX
         mQuartzSurface = nsnull;
 #endif
+
+        // We just dumped most of our allocated memory, so tell the discard
+        // tracker that we're not using any at all.
+        if (mInformedDiscardTracker) {
+          DiscardTracker::InformAllocation(-4 * mSize.width * mSize.height);
+          mInformedDiscardTracker = false;
+        }
+
         return NS_OK;
       }
     }
 
     // if it's not RGB24/ARGB32, don't optimize, but we never hit this at the moment
   }
 
   // if we're being forced to use image surfaces due to
--- a/image/src/imgFrame.h
+++ b/image/src/imgFrame.h
@@ -190,14 +190,17 @@ private: // data
   PRInt8       mBlendMethod;
   bool mSinglePixel;
   bool mNeverUseDeviceSurface;
   bool mFormatChanged;
   bool mCompositingFailed;
   /** Indicates if the image data is currently locked */
   bool mLocked;
 
+  /** Have we called DiscardTracker::InformAllocation()? */
+  bool mInformedDiscardTracker;
+
 #ifdef XP_WIN
   bool mIsDDBSurface;
 #endif
 };
 
 #endif /* imgFrame_h */
--- a/modules/libpref/src/init/all.js
+++ b/modules/libpref/src/init/all.js
@@ -3328,16 +3328,20 @@ pref("image.mem.min_discard_timeout_ms",
 pref("image.mem.decode_bytes_at_a_time", 4096);
 
 // The longest time we can spend in an iteration of an async decode
 pref("image.mem.max_ms_before_yield", 5);
 
 // The maximum source data size for which we auto sync decode
 pref("image.mem.max_bytes_for_sync_decode", 150000);
 
+// The maximum amount of decoded image data we'll willingly keep around (we
+// might keep around more than this, but we'll try to get down to this value).
+pref("image.mem.max_decoded_image_kb", 50 * 1024);
+
 // WebGL prefs
 pref("webgl.force-enabled", false);
 pref("webgl.disabled", false);
 pref("webgl.shader_validator", true);
 pref("webgl.force_osmesa", false);
 pref("webgl.osmesalib", "");
 pref("webgl.verbose", false);
 pref("webgl.prefer-native-gl", false);