Bug 732820 - Part 2: Cap the amount of discardable image data we'll willingly keep around. r=joe

This commit is contained in:
Justin Lebar 2012-03-09 17:21:01 -05:00
Родитель 982fb7faa6
Коммит f3e4dc7456
7 изменённых файлов: 272 добавлений и 233 удалений

Просмотреть файл

@ -1,39 +1,7 @@
/* -*- 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"
@ -44,77 +12,95 @@
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()
{
sDiscardRunnablePending = false;
DiscardTracker::DiscardNow();
return NS_OK;
}
int
DiscardTimeoutChangedCallback(const char* aPref, void *aClosure)
{
DiscardTracker::ReloadTimeout();
return 0;
}
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.
nsresult rv;
#ifdef DEBUG
bool isSentinel = (node == &sSentinel);
// 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!");
// 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
// Initialize the first time through
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;
}
/*
@ -126,31 +112,23 @@ 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();
}
/**
@ -159,82 +137,57 @@ DiscardTimeoutChangedCallback(const char* aPref, void *aClosure)
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);
Preferences::AddUintVarCache(&sMaxDecodedImageKB,
"image.mem.max_decoded_image_kb",
50 * 1024);
// Create the timer.
sTimer = do_CreateInstance("@mozilla.org/timer;1");
// Read the timeout pref and start the timer.
ReloadTimeout();
// 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);
// 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)
@ -252,7 +205,7 @@ DiscardTracker::TimerOn()
* 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)
@ -264,29 +217,59 @@ DiscardTracker::TimerOff()
}
/**
* 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();
}
// 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();
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.
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

Просмотреть файл

@ -1,87 +1,113 @@
/* -*- 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

Просмотреть файл

@ -214,8 +214,7 @@ RasterImage::RasterImage(imgStatusTracker* aStatusTracker) :
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
@ -2221,7 +2220,7 @@ RasterImage::CanForciblyDiscard() {
// 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

Просмотреть файл

@ -634,7 +634,7 @@ private: // data
// Discard members
PRUint32 mLockCount;
DiscardTrackerNode mDiscardTrackerNode;
DiscardTracker::Node mDiscardTrackerNode;
// Source data members
FallibleTArray<char> mSourceData;

Просмотреть файл

@ -37,6 +37,7 @@
* ***** END LICENSE BLOCK ***** */
#include "imgFrame.h"
#include "DiscardTracker.h"
#include <limits.h>
@ -67,6 +68,8 @@ static PRUint32 gTotalDDBSize = 0;
#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)
{
@ -147,6 +150,7 @@ imgFrame::imgFrame() :
, mIsDDBSurface(false)
#endif
, mLocked(false)
, mInformedDiscardTracker(false)
{
static bool hasCheckedOptimize = false;
if (!hasCheckedOptimize) {
@ -166,6 +170,10 @@ imgFrame::~imgFrame()
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,
@ -227,6 +235,14 @@ nsresult imgFrame::Init(PRInt32 aX, PRInt32 aY, PRInt32 aWidth, PRInt32 aHeight,
#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;
}
@ -271,6 +287,14 @@ nsresult imgFrame::Optimize()
#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;
}
}

Просмотреть файл

@ -195,6 +195,9 @@ private: // data
/** Indicates if the image data is currently locked */
bool mLocked;
/** Have we called DiscardTracker::InformAllocation()? */
bool mInformedDiscardTracker;
#ifdef XP_WIN
bool mIsDDBSurface;
#endif

Просмотреть файл

@ -3333,6 +3333,10 @@ 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);