/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=8 sts=2 et sw=2 tw=80: */ /* 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 NSEXPIRATIONTRACKER_H_ #define NSEXPIRATIONTRACKER_H_ #include "mozilla/Logging.h" #include "nsTArray.h" #include "nsITimer.h" #include "nsCOMPtr.h" #include "nsAutoPtr.h" #include "nsComponentManagerUtils.h" #include "nsIEventTarget.h" #include "nsIObserver.h" #include "nsIObserverService.h" #include "nsThreadUtils.h" #include "mozilla/Attributes.h" #include "mozilla/Services.h" /** * Data used to track the expiration state of an object. We promise that this * is 32 bits so that objects that includes this as a field can pad and align * efficiently. */ struct nsExpirationState { enum { NOT_TRACKED = (1U << 4) - 1, MAX_INDEX_IN_GENERATION = (1U << 28) - 1 }; nsExpirationState() : mGeneration(NOT_TRACKED) {} bool IsTracked() { return mGeneration != NOT_TRACKED; } /** * The generation that this object belongs to, or NOT_TRACKED. */ uint32_t mGeneration:4; uint32_t mIndexInGeneration:28; }; /** * ExpirationTracker classes: * - ExpirationTrackerImpl (Thread-safe class) * - nsExpirationTracker (Main-thread only class) * * These classes can track the lifetimes and usage of a large number of * objects, and send a notification some window of time after a live object was * last used. This is very useful when you manage a large number of objects * and want to flush some after they haven't been used for a while. * nsExpirationTracker is designed to be very space and time efficient. * * The type parameter T is the object type that we will track pointers to. T * must include an accessible method GetExpirationState() that returns a * pointer to an nsExpirationState associated with the object (preferably, * stored in a field of the object). * * The parameter K is the number of generations that will be used. Increasing * the number of generations narrows the window within which we promise * to fire notifications, at a slight increase in space cost for the tracker. * We require 2 <= K <= nsExpirationState::NOT_TRACKED (currently 15). * * To use this class, you need to inherit from it and override the * NotifyExpired() method. * * The approach is to track objects in K generations. When an object is accessed * it moves from its current generation to the newest generation. Generations * are stored in a cyclic array; when a timer interrupt fires, we advance * the current generation pointer to effectively age all objects very efficiently. * By storing information in each object about its generation and index within its * generation array, we make removal of objects from a generation very cheap. * * Future work: * -- Add a method to change the timer period? */ /** * Base class for ExiprationTracker implementations. * * nsExpirationTracker class below is a specialized class to be inherited by the * instances to be accessed only on main-thread. * * For creating a thread-safe tracker, you can define a subclass inheriting this * base class and specialize the Mutex and AutoLock to be used. * */ template class ExpirationTrackerImpl { public: /** * Initialize the tracker. * @param aTimerPeriod the timer period in milliseconds. The guarantees * provided by the tracker are defined in terms of this period. If the * period is zero, then we don't use a timer and rely on someone calling * AgeOneGenerationLocked explicitly. * @param aName the name of the subclass for telemetry. * @param aEventTarget the optional event target on main thread to label the * runnable of the asynchronous invocation to NotifyExpired(). */ ExpirationTrackerImpl(uint32_t aTimerPeriod, const char* aName, nsIEventTarget* aEventTarget = nullptr) : mTimerPeriod(aTimerPeriod) , mNewestGeneration(0) , mInAgeOneGeneration(false) , mName(aName) , mEventTarget(aEventTarget) { static_assert(K >= 2 && K <= nsExpirationState::NOT_TRACKED, "Unsupported number of generations (must be 2 <= K <= 15)"); MOZ_ASSERT(NS_IsMainThread()); if (mEventTarget) { bool current = false; // NOTE: The following check+crash could be condensed into a // MOZ_RELEASE_ASSERT, but that triggers a segfault during compilation in // clang 3.8. Once we don't have to care about clang 3.8 anymore, though, // we can convert to MOZ_RELEASE_ASSERT here. if (MOZ_UNLIKELY(NS_FAILED(mEventTarget->IsOnCurrentThread(¤t)) || !current)) { MOZ_CRASH("Provided event target must be on the main thread"); } } mObserver = new ExpirationTrackerObserver(); mObserver->Init(this); } virtual ~ExpirationTrackerImpl() { MOZ_ASSERT(NS_IsMainThread()); if (mTimer) { mTimer->Cancel(); } mObserver->Destroy(); } /** * Add an object to be tracked. It must not already be tracked. It will * be added to the newest generation, i.e., as if it was just used. * @return an error on out-of-memory */ nsresult AddObjectLocked(T* aObj, const AutoLock& aAutoLock) { if (NS_WARN_IF(!aObj)) { MOZ_DIAGNOSTIC_ASSERT(false, "Invalid object to add"); return NS_ERROR_UNEXPECTED; } nsExpirationState* state = aObj->GetExpirationState(); if (NS_WARN_IF(state->IsTracked())) { MOZ_DIAGNOSTIC_ASSERT(false, "Tried to add an object that's already tracked"); return NS_ERROR_UNEXPECTED; } nsTArray& generation = mGenerations[mNewestGeneration]; uint32_t index = generation.Length(); if (index > nsExpirationState::MAX_INDEX_IN_GENERATION) { NS_WARNING("More than 256M elements tracked, this is probably a problem"); return NS_ERROR_OUT_OF_MEMORY; } if (index == 0) { // We might need to start the timer nsresult rv = CheckStartTimerLocked(aAutoLock); if (NS_FAILED(rv)) { return rv; } } if (!generation.AppendElement(aObj)) { return NS_ERROR_OUT_OF_MEMORY; } state->mGeneration = mNewestGeneration; state->mIndexInGeneration = index; return NS_OK; } /** * Remove an object from the tracker. It must currently be tracked. */ void RemoveObjectLocked(T* aObj, const AutoLock& aAutoLock) { if (NS_WARN_IF(!aObj)) { MOZ_DIAGNOSTIC_ASSERT(false, "Invalid object to remove"); return; } nsExpirationState* state = aObj->GetExpirationState(); if (NS_WARN_IF(!state->IsTracked())) { MOZ_DIAGNOSTIC_ASSERT(false, "Tried to remove an object that's not tracked"); return; } nsTArray& generation = mGenerations[state->mGeneration]; uint32_t index = state->mIndexInGeneration; MOZ_ASSERT(generation.Length() > index && generation[index] == aObj, "Object is lying about its index"); // Move the last object to fill the hole created by removing aObj uint32_t last = generation.Length() - 1; T* lastObj = generation[last]; generation[index] = lastObj; lastObj->GetExpirationState()->mIndexInGeneration = index; generation.RemoveElementAt(last); MOZ_ASSERT(generation.Length() == last); state->mGeneration = nsExpirationState::NOT_TRACKED; // We do not check whether we need to stop the timer here. The timer // will check that itself next time it fires. Checking here would not // be efficient since we'd need to track all generations. Also we could // thrash by incessantly creating and destroying timers if someone // kept adding and removing an object from the tracker. } /** * Notify that an object has been used. * @return an error if we lost the object from the tracker... */ nsresult MarkUsedLocked(T* aObj, const AutoLock& aAutoLock) { nsExpirationState* state = aObj->GetExpirationState(); if (mNewestGeneration == state->mGeneration) { return NS_OK; } RemoveObjectLocked(aObj, aAutoLock); return AddObjectLocked(aObj, aAutoLock); } /** * The timer calls this, but it can also be manually called if you want * to age objects "artifically". This can result in calls to NotifyExpiredLocked. */ void AgeOneGenerationLocked(const AutoLock& aAutoLock) { if (mInAgeOneGeneration) { NS_WARNING("Can't reenter AgeOneGeneration from NotifyExpired"); return; } mInAgeOneGeneration = true; uint32_t reapGeneration = mNewestGeneration > 0 ? mNewestGeneration - 1 : K - 1; nsTArray& generation = mGenerations[reapGeneration]; // The following is rather tricky. We have to cope with objects being // removed from this generation either because of a call to RemoveObject // (or indirectly via MarkUsedLocked) inside NotifyExpiredLocked. Fortunately // no objects can be added to this generation because it's not the newest // generation. We depend on the fact that RemoveObject can only cause // the indexes of objects in this generation to *decrease*, not increase. // So if we start from the end and work our way backwards we are guaranteed // to see each object at least once. size_t index = generation.Length(); for (;;) { // Objects could have been removed so index could be outside // the array index = XPCOM_MIN(index, generation.Length()); if (index == 0) { break; } --index; NotifyExpiredLocked(generation[index], aAutoLock); } // Any leftover objects from reapGeneration just end up in the new // newest-generation. This is bad form, though, so warn if there are any. if (!generation.IsEmpty()) { NS_WARNING("Expired objects were not removed or marked used"); } // Free excess memory used by the generation array, since we probably // just removed most or all of its elements. generation.Compact(); mNewestGeneration = reapGeneration; mInAgeOneGeneration = false; } /** * This just calls AgeOneGenerationLocked K times. Under normal circumstances * this will result in all objects getting NotifyExpiredLocked called on them, * but if NotifyExpiredLocked itself marks some objects as used, then those * objects might not expire. This would be a good thing to call if we get into * a critically-low memory situation. */ void AgeAllGenerationsLocked(const AutoLock& aAutoLock) { uint32_t i; for (i = 0; i < K; ++i) { AgeOneGenerationLocked(aAutoLock); } } class Iterator { private: ExpirationTrackerImpl* mTracker; uint32_t mGeneration; uint32_t mIndex; public: Iterator(ExpirationTrackerImpl* aTracker, AutoLock& aAutoLock) : mTracker(aTracker) , mGeneration(0) , mIndex(0) { } T* Next() { while (mGeneration < K) { nsTArray* generation = &mTracker->mGenerations[mGeneration]; if (mIndex < generation->Length()) { ++mIndex; return (*generation)[mIndex - 1]; } ++mGeneration; mIndex = 0; } return nullptr; } }; friend class Iterator; bool IsEmptyLocked(const AutoLock& aAutoLock) { for (uint32_t i = 0; i < K; ++i) { if (!mGenerations[i].IsEmpty()) { return false; } } return true; } protected: /** * This must be overridden to catch notifications. It is called whenever * we detect that an object has not been used for at least (K-1)*mTimerPeriod * milliseconds. If timer events are not delayed, it will be called within * roughly K*mTimerPeriod milliseconds after the last use. * (Unless AgeOneGenerationLocked or AgeAllGenerationsLocked have been called * to accelerate the aging process.) * * NOTE: These bounds ignore delays in timer firings due to actual work being * performed by the browser. We use a slack timer so there is always at least * mTimerPeriod milliseconds between firings, which gives us (K-1)*mTimerPeriod * as a pretty solid lower bound. The upper bound is rather loose, however. * If the maximum amount by which any given timer firing is delayed is D, then * the upper bound before NotifyExpiredLocked is called is K*(mTimerPeriod + D). * * The NotifyExpiredLocked call is expected to remove the object from the tracker, * but it need not. The object (or other objects) could be "resurrected" * by calling MarkUsedLocked() on them, or they might just not be removed. * Any objects left over that have not been resurrected or removed * are placed in the new newest-generation, but this is considered "bad form" * and should be avoided (we'll issue a warning). (This recycling counts * as "a use" for the purposes of the expiry guarantee above...) * * For robustness and simplicity, we allow objects to be notified more than * once here in the same timer tick. */ virtual void NotifyExpiredLocked(T*, const AutoLock&) = 0; /** * This may be overridden to perform any post-aging work that needs to be * done while still holding the lock. It will be called once after each timer * event, and each low memory event has been handled. */ virtual void NotifyHandlerEndLocked(const AutoLock&) { }; /** * This may be overridden to perform any post-aging work that needs to be * done outside the lock. It will be called once after each * NotifyEndTransactionLocked call. */ virtual void NotifyHandlerEnd() { }; virtual Mutex& GetMutex() = 0; private: class ExpirationTrackerObserver; RefPtr mObserver; nsTArray mGenerations[K]; nsCOMPtr mTimer; uint32_t mTimerPeriod; uint32_t mNewestGeneration; bool mInAgeOneGeneration; const char* const mName; // Used for timer firing profiling. const nsCOMPtr mEventTarget; /** * Whenever "memory-pressure" is observed, it calls AgeAllGenerationsLocked() * to minimize memory usage. */ class ExpirationTrackerObserver final : public nsIObserver { public: void Init(ExpirationTrackerImpl* aObj) { mOwner = aObj; nsCOMPtr obs = mozilla::services::GetObserverService(); if (obs) { obs->AddObserver(this, "memory-pressure", false); } } void Destroy() { mOwner = nullptr; nsCOMPtr obs = mozilla::services::GetObserverService(); if (obs) { obs->RemoveObserver(this, "memory-pressure"); } } NS_DECL_ISUPPORTS NS_DECL_NSIOBSERVER private: ExpirationTrackerImpl* mOwner; }; void HandleLowMemory() { { AutoLock lock(GetMutex()); AgeAllGenerationsLocked(lock); NotifyHandlerEndLocked(lock); } NotifyHandlerEnd(); } void HandleTimeout() { { AutoLock lock(GetMutex()); AgeOneGenerationLocked(lock); // Cancel the timer if we have no objects to track if (IsEmptyLocked(lock)) { mTimer->Cancel(); mTimer = nullptr; } NotifyHandlerEndLocked(lock); } NotifyHandlerEnd(); } static void TimerCallback(nsITimer* aTimer, void* aThis) { ExpirationTrackerImpl* tracker = static_cast(aThis); tracker->HandleTimeout(); } nsresult CheckStartTimerLocked(const AutoLock& aAutoLock) { if (mTimer || !mTimerPeriod) { return NS_OK; } nsCOMPtr target = mEventTarget; if (!target && !NS_IsMainThread()) { // TimerCallback should always be run on the main thread to prevent races // to the destruction of the tracker. target = do_GetMainThread(); NS_ENSURE_STATE(target); } return NS_NewTimerWithFuncCallback( getter_AddRefs(mTimer), TimerCallback, this, mTimerPeriod, nsITimer::TYPE_REPEATING_SLACK_LOW_PRIORITY, mName, target); } }; namespace detail { class PlaceholderLock { public: void Lock() {} void Unlock() {} }; class PlaceholderAutoLock { public: explicit PlaceholderAutoLock(PlaceholderLock&) { } ~PlaceholderAutoLock() = default; }; template using SingleThreadedExpirationTracker = ExpirationTrackerImpl; } // namespace detail template class nsExpirationTracker : protected ::detail::SingleThreadedExpirationTracker { typedef ::detail::PlaceholderLock Lock; typedef ::detail::PlaceholderAutoLock AutoLock; Lock mLock; AutoLock FakeLock() { MOZ_DIAGNOSTIC_ASSERT(NS_IsMainThread()); return AutoLock(mLock); } Lock& GetMutex() override { MOZ_DIAGNOSTIC_ASSERT(NS_IsMainThread()); return mLock; } void NotifyExpiredLocked(T* aObject, const AutoLock&) override { NotifyExpired(aObject); } /** * Since there are no users of these callbacks in the single threaded case, * we mark them as final with the hope that the compiler can optimize the * method calls out entirely. */ void NotifyHandlerEndLocked(const AutoLock&) final override { } void NotifyHandlerEnd() final override { } protected: virtual void NotifyExpired(T* aObj) = 0; public: nsExpirationTracker(uint32_t aTimerPeriod, const char* aName, nsIEventTarget* aEventTarget = nullptr) : ::detail::SingleThreadedExpirationTracker(aTimerPeriod, aName, aEventTarget) { } virtual ~nsExpirationTracker() { } nsresult AddObject(T* aObj) { return this->AddObjectLocked(aObj, FakeLock()); } void RemoveObject(T* aObj) { this->RemoveObjectLocked(aObj, FakeLock()); } nsresult MarkUsed(T* aObj) { return this->MarkUsedLocked(aObj, FakeLock()); } void AgeOneGeneration() { this->AgeOneGenerationLocked(FakeLock()); } void AgeAllGenerations() { this->AgeAllGenerationsLocked(FakeLock()); } class Iterator { private: AutoLock mAutoLock; typename ExpirationTrackerImpl::Iterator mIterator; public: explicit Iterator(nsExpirationTracker* aTracker) : mAutoLock(aTracker->GetMutex()) , mIterator(aTracker, mAutoLock) { } T* Next() { return mIterator.Next(); } }; friend class Iterator; bool IsEmpty() { return this->IsEmptyLocked(FakeLock()); } }; template NS_IMETHODIMP ExpirationTrackerImpl:: ExpirationTrackerObserver::Observe( nsISupports* aSubject, const char* aTopic, const char16_t* aData) { if (!strcmp(aTopic, "memory-pressure") && mOwner) { mOwner->HandleLowMemory(); } return NS_OK; } template NS_IMETHODIMP_(MozExternalRefCountType) ExpirationTrackerImpl:: ExpirationTrackerObserver::AddRef(void) { MOZ_ASSERT(int32_t(mRefCnt) >= 0, "illegal refcnt"); NS_ASSERT_OWNINGTHREAD(ExpirationTrackerObserver); ++mRefCnt; NS_LOG_ADDREF(this, mRefCnt, "ExpirationTrackerObserver", sizeof(*this)); return mRefCnt; } template NS_IMETHODIMP_(MozExternalRefCountType) ExpirationTrackerImpl:: ExpirationTrackerObserver::Release(void) { MOZ_ASSERT(int32_t(mRefCnt) > 0, "dup release"); NS_ASSERT_OWNINGTHREAD(ExpirationTrackerObserver); --mRefCnt; NS_LOG_RELEASE(this, mRefCnt, "ExpirationTrackerObserver"); if (mRefCnt == 0) { NS_ASSERT_OWNINGTHREAD(ExpirationTrackerObserver); mRefCnt = 1; /* stabilize */ delete (this); return 0; } return mRefCnt; } template NS_IMETHODIMP ExpirationTrackerImpl:: ExpirationTrackerObserver::QueryInterface( REFNSIID aIID, void** aInstancePtr) { NS_ASSERTION(aInstancePtr, "QueryInterface requires a non-NULL destination!"); nsresult rv = NS_ERROR_FAILURE; NS_INTERFACE_TABLE(ExpirationTrackerObserver, nsIObserver) return rv; } #endif /*NSEXPIRATIONTRACKER_H_*/