michael@0: /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this file, michael@0: * You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: #include "nsComponentManagerUtils.h" michael@0: #include "nsITimer.h" michael@0: #include "RasterImage.h" michael@0: #include "DiscardTracker.h" michael@0: #include "mozilla/Preferences.h" michael@0: michael@0: namespace mozilla { michael@0: namespace image { michael@0: michael@0: static const char* sDiscardTimeoutPref = "image.mem.min_discard_timeout_ms"; michael@0: michael@0: /* static */ LinkedList DiscardTracker::sDiscardableImages; michael@0: /* static */ nsCOMPtr DiscardTracker::sTimer; michael@0: /* static */ bool DiscardTracker::sInitialized = false; michael@0: /* static */ bool DiscardTracker::sTimerOn = false; michael@0: /* static */ Atomic DiscardTracker::sDiscardRunnablePending(false); michael@0: /* static */ uint64_t DiscardTracker::sCurrentDecodedImageBytes = 0; michael@0: /* static */ uint32_t DiscardTracker::sMinDiscardTimeoutMs = 10000; michael@0: /* static */ uint32_t DiscardTracker::sMaxDecodedImageKB = 42 * 1024; michael@0: /* static */ uint32_t DiscardTracker::sHardLimitDecodedImageKB = 0; michael@0: /* static */ PRLock * DiscardTracker::sAllocationLock = nullptr; michael@0: /* static */ mozilla::Mutex* DiscardTracker::sNodeListMutex = nullptr; michael@0: /* static */ Atomic DiscardTracker::sShutdown(false); michael@0: michael@0: /* michael@0: * When we notice we're using too much memory for decoded images, we enqueue a michael@0: * DiscardRunnable, which runs this code. michael@0: */ michael@0: NS_IMETHODIMP michael@0: DiscardTracker::DiscardRunnable::Run() michael@0: { michael@0: sDiscardRunnablePending = false; michael@0: michael@0: DiscardTracker::DiscardNow(); michael@0: return NS_OK; michael@0: } michael@0: michael@0: void michael@0: DiscardTimeoutChangedCallback(const char* aPref, void *aClosure) michael@0: { michael@0: DiscardTracker::ReloadTimeout(); michael@0: } michael@0: michael@0: nsresult michael@0: DiscardTracker::Reset(Node *node) michael@0: { michael@0: // We shouldn't call Reset() with a null |img| pointer, on images which can't michael@0: // be discarded, or on animated images (which should be marked as michael@0: // non-discardable, anyway). michael@0: MutexAutoLock lock(*sNodeListMutex); michael@0: MOZ_ASSERT(sInitialized); michael@0: MOZ_ASSERT(node->img); michael@0: MOZ_ASSERT(node->img->CanDiscard()); michael@0: MOZ_ASSERT(!node->img->mAnim); michael@0: michael@0: // Insert the node at the front of the list and note when it was inserted. michael@0: bool wasInList = node->isInList(); michael@0: if (wasInList) { michael@0: node->remove(); michael@0: } michael@0: node->timestamp = TimeStamp::Now(); michael@0: sDiscardableImages.insertFront(node); michael@0: michael@0: // If the node wasn't already in the list of discardable images, then we may michael@0: // need to discard some images to stay under the sMaxDecodedImageKB limit. michael@0: // Call MaybeDiscardSoon to do this check. michael@0: if (!wasInList) { michael@0: MaybeDiscardSoon(); michael@0: } michael@0: michael@0: // Make sure the timer is running. michael@0: nsresult rv = EnableTimer(); michael@0: NS_ENSURE_SUCCESS(rv,rv); michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: void michael@0: DiscardTracker::Remove(Node *node) michael@0: { michael@0: if (sShutdown) { michael@0: // Already shutdown. List should be empty, so just return. michael@0: return; michael@0: } michael@0: MutexAutoLock lock(*sNodeListMutex); michael@0: michael@0: if (node->isInList()) michael@0: node->remove(); michael@0: michael@0: if (sDiscardableImages.isEmpty()) michael@0: DisableTimer(); michael@0: } michael@0: michael@0: /** michael@0: * Shut down the tracker, deallocating the timer. michael@0: */ michael@0: void michael@0: DiscardTracker::Shutdown() michael@0: { michael@0: sShutdown = true; michael@0: michael@0: if (sTimer) { michael@0: sTimer->Cancel(); michael@0: sTimer = nullptr; michael@0: } michael@0: michael@0: // Clear the sDiscardableImages linked list so that its destructor michael@0: // (LinkedList.h) finds an empty array, which is required after bug 803688. michael@0: DiscardAll(); michael@0: michael@0: delete sNodeListMutex; michael@0: sNodeListMutex = nullptr; michael@0: } michael@0: michael@0: /* michael@0: * Discard all the images we're tracking. michael@0: */ michael@0: void michael@0: DiscardTracker::DiscardAll() michael@0: { michael@0: MutexAutoLock lock(*sNodeListMutex); michael@0: michael@0: if (!sInitialized) michael@0: return; michael@0: michael@0: // Be careful: Calling Discard() on an image might cause it to be removed michael@0: // from the list! michael@0: Node *n; michael@0: while ((n = sDiscardableImages.popFirst())) { michael@0: n->img->Discard(); michael@0: } michael@0: michael@0: // The list is empty, so there's no need to leave the timer on. michael@0: DisableTimer(); michael@0: } michael@0: michael@0: /* static */ bool michael@0: DiscardTracker::TryAllocation(uint64_t aBytes) michael@0: { michael@0: MOZ_ASSERT(sInitialized); michael@0: michael@0: PR_Lock(sAllocationLock); michael@0: bool enoughSpace = michael@0: !sHardLimitDecodedImageKB || michael@0: (sHardLimitDecodedImageKB * 1024) - sCurrentDecodedImageBytes >= aBytes; michael@0: michael@0: if (enoughSpace) { michael@0: sCurrentDecodedImageBytes += aBytes; michael@0: } michael@0: PR_Unlock(sAllocationLock); michael@0: michael@0: // If we're using too much memory for decoded images, MaybeDiscardSoon will michael@0: // enqueue a callback to discard some images. michael@0: MaybeDiscardSoon(); michael@0: michael@0: return enoughSpace; michael@0: } michael@0: michael@0: /* static */ void michael@0: DiscardTracker::InformDeallocation(uint64_t aBytes) michael@0: { michael@0: // This function is called back e.g. from RasterImage::Discard(); be careful! michael@0: michael@0: MOZ_ASSERT(sInitialized); michael@0: michael@0: PR_Lock(sAllocationLock); michael@0: MOZ_ASSERT(aBytes <= sCurrentDecodedImageBytes); michael@0: sCurrentDecodedImageBytes -= aBytes; michael@0: PR_Unlock(sAllocationLock); michael@0: } michael@0: michael@0: /** michael@0: * Initialize the tracker. michael@0: */ michael@0: nsresult michael@0: DiscardTracker::Initialize() michael@0: { michael@0: // Watch the timeout pref for changes. michael@0: Preferences::RegisterCallback(DiscardTimeoutChangedCallback, michael@0: sDiscardTimeoutPref); michael@0: michael@0: Preferences::AddUintVarCache(&sMaxDecodedImageKB, michael@0: "image.mem.max_decoded_image_kb", michael@0: 50 * 1024); michael@0: michael@0: Preferences::AddUintVarCache(&sHardLimitDecodedImageKB, michael@0: "image.mem.hard_limit_decoded_image_kb", michael@0: 0); michael@0: // Create the timer. michael@0: sTimer = do_CreateInstance("@mozilla.org/timer;1"); michael@0: michael@0: // Create a lock for safegarding the 64-bit sCurrentDecodedImageBytes michael@0: sAllocationLock = PR_NewLock(); michael@0: michael@0: // Create a lock for the node list. michael@0: MOZ_ASSERT(!sNodeListMutex); michael@0: sNodeListMutex = new Mutex("image::DiscardTracker"); michael@0: michael@0: // Mark us as initialized michael@0: sInitialized = true; michael@0: michael@0: // Read the timeout pref and start the timer. michael@0: ReloadTimeout(); michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: /** michael@0: * Read the discard timeout from about:config. michael@0: */ michael@0: void michael@0: DiscardTracker::ReloadTimeout() michael@0: { michael@0: // Read the timeout pref. michael@0: int32_t discardTimeout; michael@0: nsresult rv = Preferences::GetInt(sDiscardTimeoutPref, &discardTimeout); michael@0: michael@0: // If we got something bogus, return. michael@0: if (!NS_SUCCEEDED(rv) || discardTimeout <= 0) michael@0: return; michael@0: michael@0: // If the value didn't change, return. michael@0: if ((uint32_t) discardTimeout == sMinDiscardTimeoutMs) michael@0: return; michael@0: michael@0: // Update the value. michael@0: sMinDiscardTimeoutMs = (uint32_t) discardTimeout; michael@0: michael@0: // Restart the timer so the new timeout takes effect. michael@0: DisableTimer(); michael@0: EnableTimer(); michael@0: } michael@0: michael@0: /** michael@0: * Enables the timer. No-op if the timer is already running. michael@0: */ michael@0: nsresult michael@0: DiscardTracker::EnableTimer() michael@0: { michael@0: // Nothing to do if the timer's already on or we haven't yet been michael@0: // initialized. !sTimer probably means we've shut down, so just ignore that, michael@0: // too. michael@0: if (sTimerOn || !sInitialized || !sTimer) michael@0: return NS_OK; michael@0: michael@0: sTimerOn = true; michael@0: michael@0: // Activate the timer. Have it call us back in (sMinDiscardTimeoutMs / 2) michael@0: // ms, so that an image is discarded between sMinDiscardTimeoutMs and michael@0: // (3/2 * sMinDiscardTimeoutMs) ms after it's unlocked. michael@0: return sTimer->InitWithFuncCallback(TimerCallback, michael@0: nullptr, michael@0: sMinDiscardTimeoutMs / 2, michael@0: nsITimer::TYPE_REPEATING_SLACK); michael@0: } michael@0: michael@0: /* michael@0: * Disables the timer. No-op if the timer isn't running. michael@0: */ michael@0: void michael@0: DiscardTracker::DisableTimer() michael@0: { michael@0: // Nothing to do if the timer's already off. michael@0: if (!sTimerOn || !sTimer) michael@0: return; michael@0: sTimerOn = false; michael@0: michael@0: // Deactivate michael@0: sTimer->Cancel(); michael@0: } michael@0: michael@0: /** michael@0: * Routine activated when the timer fires. This discards everything that's michael@0: * older than sMinDiscardTimeoutMs, and tries to discard enough images so that michael@0: * we go under sMaxDecodedImageKB. michael@0: */ michael@0: void michael@0: DiscardTracker::TimerCallback(nsITimer *aTimer, void *aClosure) michael@0: { michael@0: DiscardNow(); michael@0: } michael@0: michael@0: void michael@0: DiscardTracker::DiscardNow() michael@0: { michael@0: // Assuming the list is ordered with oldest discard tracker nodes at the back michael@0: // and newest ones at the front, iterate from back to front discarding nodes michael@0: // until we encounter one which is new enough to keep and until we go under michael@0: // our sMaxDecodedImageKB limit. michael@0: michael@0: TimeStamp now = TimeStamp::Now(); michael@0: Node* node; michael@0: while ((node = sDiscardableImages.getLast())) { michael@0: if ((now - node->timestamp).ToMilliseconds() > sMinDiscardTimeoutMs || michael@0: sCurrentDecodedImageBytes > sMaxDecodedImageKB * 1024) { michael@0: michael@0: // Discarding the image should cause sCurrentDecodedImageBytes to michael@0: // decrease via a call to InformDeallocation(). michael@0: node->img->Discard(); michael@0: michael@0: // Careful: Discarding may have caused the node to have been removed michael@0: // from the list. michael@0: Remove(node); michael@0: } michael@0: else { michael@0: break; michael@0: } michael@0: } michael@0: michael@0: // If the list is empty, disable the timer. michael@0: if (sDiscardableImages.isEmpty()) michael@0: DisableTimer(); michael@0: } michael@0: michael@0: void michael@0: DiscardTracker::MaybeDiscardSoon() michael@0: { michael@0: // Are we carrying around too much decoded image data? If so, enqueue an michael@0: // event to try to get us down under our limit. michael@0: if (sCurrentDecodedImageBytes > sMaxDecodedImageKB * 1024 && michael@0: !sDiscardableImages.isEmpty()) { michael@0: // Check if the value of sDiscardRunnablePending used to be false michael@0: if (!sDiscardRunnablePending.exchange(true)) { michael@0: nsRefPtr runnable = new DiscardRunnable(); michael@0: NS_DispatchToMainThread(runnable); michael@0: } michael@0: } michael@0: } michael@0: michael@0: } // namespace image michael@0: } // namespace mozilla