xpcom/ds/nsExpirationTracker.h

Thu, 22 Jan 2015 13:21:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 22 Jan 2015 13:21:57 +0100
branch
TOR_BUG_9701
changeset 15
b8a032363ba2
permissions
-rw-r--r--

Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6

michael@0 1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
michael@0 2 /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0 3 * License, v. 2.0. If a copy of the MPL was not distributed with this
michael@0 4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 5
michael@0 6 #ifndef NSEXPIRATIONTRACKER_H_
michael@0 7 #define NSEXPIRATIONTRACKER_H_
michael@0 8
michael@0 9 #include "mozilla/Attributes.h"
michael@0 10
michael@0 11 #include "prlog.h"
michael@0 12 #include "nsTArray.h"
michael@0 13 #include "nsITimer.h"
michael@0 14 #include "nsCOMPtr.h"
michael@0 15 #include "nsAutoPtr.h"
michael@0 16 #include "nsComponentManagerUtils.h"
michael@0 17 #include "nsIObserver.h"
michael@0 18 #include "nsIObserverService.h"
michael@0 19 #include "mozilla/Services.h"
michael@0 20 #include "mozilla/Attributes.h"
michael@0 21
michael@0 22 /**
michael@0 23 * Data used to track the expiration state of an object. We promise that this
michael@0 24 * is 32 bits so that objects that includes this as a field can pad and align
michael@0 25 * efficiently.
michael@0 26 */
michael@0 27 struct nsExpirationState {
michael@0 28 enum { NOT_TRACKED = (1U << 4) - 1,
michael@0 29 MAX_INDEX_IN_GENERATION = (1U << 28) - 1 };
michael@0 30
michael@0 31 nsExpirationState() : mGeneration(NOT_TRACKED) {}
michael@0 32 bool IsTracked() { return mGeneration != NOT_TRACKED; }
michael@0 33
michael@0 34 /**
michael@0 35 * The generation that this object belongs to, or NOT_TRACKED.
michael@0 36 */
michael@0 37 uint32_t mGeneration:4;
michael@0 38 uint32_t mIndexInGeneration:28;
michael@0 39 };
michael@0 40
michael@0 41 /**
michael@0 42 * nsExpirationTracker can track the lifetimes and usage of a large number of
michael@0 43 * objects, and send a notification some window of time after a live object was
michael@0 44 * last used. This is very useful when you manage a large number of objects
michael@0 45 * and want to flush some after they haven't been used for a while.
michael@0 46 * nsExpirationTracker is designed to be very space and time efficient.
michael@0 47 *
michael@0 48 * The type parameter T is the object type that we will track pointers to. T
michael@0 49 * must include an accessible method GetExpirationState() that returns a
michael@0 50 * pointer to an nsExpirationState associated with the object (preferably,
michael@0 51 * stored in a field of the object).
michael@0 52 *
michael@0 53 * The parameter K is the number of generations that will be used. Increasing
michael@0 54 * the number of generations narrows the window within which we promise
michael@0 55 * to fire notifications, at a slight increase in space cost for the tracker.
michael@0 56 * We require 2 <= K <= nsExpirationState::NOT_TRACKED (currently 15).
michael@0 57 *
michael@0 58 * To use this class, you need to inherit from it and override the
michael@0 59 * NotifyExpired() method.
michael@0 60 *
michael@0 61 * The approach is to track objects in K generations. When an object is accessed
michael@0 62 * it moves from its current generation to the newest generation. Generations
michael@0 63 * are stored in a cyclic array; when a timer interrupt fires, we advance
michael@0 64 * the current generation pointer to effectively age all objects very efficiently.
michael@0 65 * By storing information in each object about its generation and index within its
michael@0 66 * generation array, we make removal of objects from a generation very cheap.
michael@0 67 *
michael@0 68 * Future work:
michael@0 69 * -- Add a method to change the timer period?
michael@0 70 */
michael@0 71 template <class T, uint32_t K> class nsExpirationTracker {
michael@0 72 public:
michael@0 73 /**
michael@0 74 * Initialize the tracker.
michael@0 75 * @param aTimerPeriod the timer period in milliseconds. The guarantees
michael@0 76 * provided by the tracker are defined in terms of this period. If the
michael@0 77 * period is zero, then we don't use a timer and rely on someone calling
michael@0 78 * AgeOneGeneration explicitly.
michael@0 79 */
michael@0 80 nsExpirationTracker(uint32_t aTimerPeriod)
michael@0 81 : mTimerPeriod(aTimerPeriod), mNewestGeneration(0),
michael@0 82 mInAgeOneGeneration(false) {
michael@0 83 static_assert(K >= 2 && K <= nsExpirationState::NOT_TRACKED,
michael@0 84 "Unsupported number of generations (must be 2 <= K <= 15)");
michael@0 85 mObserver = new ExpirationTrackerObserver();
michael@0 86 mObserver->Init(this);
michael@0 87 }
michael@0 88 ~nsExpirationTracker() {
michael@0 89 if (mTimer) {
michael@0 90 mTimer->Cancel();
michael@0 91 }
michael@0 92 mObserver->Destroy();
michael@0 93 }
michael@0 94
michael@0 95 /**
michael@0 96 * Add an object to be tracked. It must not already be tracked. It will
michael@0 97 * be added to the newest generation, i.e., as if it was just used.
michael@0 98 * @return an error on out-of-memory
michael@0 99 */
michael@0 100 nsresult AddObject(T* aObj) {
michael@0 101 nsExpirationState* state = aObj->GetExpirationState();
michael@0 102 NS_ASSERTION(!state->IsTracked(), "Tried to add an object that's already tracked");
michael@0 103 nsTArray<T*>& generation = mGenerations[mNewestGeneration];
michael@0 104 uint32_t index = generation.Length();
michael@0 105 if (index > nsExpirationState::MAX_INDEX_IN_GENERATION) {
michael@0 106 NS_WARNING("More than 256M elements tracked, this is probably a problem");
michael@0 107 return NS_ERROR_OUT_OF_MEMORY;
michael@0 108 }
michael@0 109 if (index == 0) {
michael@0 110 // We might need to start the timer
michael@0 111 nsresult rv = CheckStartTimer();
michael@0 112 if (NS_FAILED(rv))
michael@0 113 return rv;
michael@0 114 }
michael@0 115 if (!generation.AppendElement(aObj))
michael@0 116 return NS_ERROR_OUT_OF_MEMORY;
michael@0 117 state->mGeneration = mNewestGeneration;
michael@0 118 state->mIndexInGeneration = index;
michael@0 119 return NS_OK;
michael@0 120 }
michael@0 121
michael@0 122 /**
michael@0 123 * Remove an object from the tracker. It must currently be tracked.
michael@0 124 */
michael@0 125 void RemoveObject(T* aObj) {
michael@0 126 nsExpirationState* state = aObj->GetExpirationState();
michael@0 127 NS_ASSERTION(state->IsTracked(), "Tried to remove an object that's not tracked");
michael@0 128 nsTArray<T*>& generation = mGenerations[state->mGeneration];
michael@0 129 uint32_t index = state->mIndexInGeneration;
michael@0 130 NS_ASSERTION(generation.Length() > index &&
michael@0 131 generation[index] == aObj, "Object is lying about its index");
michael@0 132 // Move the last object to fill the hole created by removing aObj
michael@0 133 uint32_t last = generation.Length() - 1;
michael@0 134 T* lastObj = generation[last];
michael@0 135 generation[index] = lastObj;
michael@0 136 lastObj->GetExpirationState()->mIndexInGeneration = index;
michael@0 137 generation.RemoveElementAt(last);
michael@0 138 state->mGeneration = nsExpirationState::NOT_TRACKED;
michael@0 139 // We do not check whether we need to stop the timer here. The timer
michael@0 140 // will check that itself next time it fires. Checking here would not
michael@0 141 // be efficient since we'd need to track all generations. Also we could
michael@0 142 // thrash by incessantly creating and destroying timers if someone
michael@0 143 // kept adding and removing an object from the tracker.
michael@0 144 }
michael@0 145
michael@0 146 /**
michael@0 147 * Notify that an object has been used.
michael@0 148 * @return an error if we lost the object from the tracker...
michael@0 149 */
michael@0 150 nsresult MarkUsed(T* aObj) {
michael@0 151 nsExpirationState* state = aObj->GetExpirationState();
michael@0 152 if (mNewestGeneration == state->mGeneration)
michael@0 153 return NS_OK;
michael@0 154 RemoveObject(aObj);
michael@0 155 return AddObject(aObj);
michael@0 156 }
michael@0 157
michael@0 158 /**
michael@0 159 * The timer calls this, but it can also be manually called if you want
michael@0 160 * to age objects "artifically". This can result in calls to NotifyExpired.
michael@0 161 */
michael@0 162 void AgeOneGeneration() {
michael@0 163 if (mInAgeOneGeneration) {
michael@0 164 NS_WARNING("Can't reenter AgeOneGeneration from NotifyExpired");
michael@0 165 return;
michael@0 166 }
michael@0 167
michael@0 168 mInAgeOneGeneration = true;
michael@0 169 uint32_t reapGeneration =
michael@0 170 mNewestGeneration > 0 ? mNewestGeneration - 1 : K - 1;
michael@0 171 nsTArray<T*>& generation = mGenerations[reapGeneration];
michael@0 172 // The following is rather tricky. We have to cope with objects being
michael@0 173 // removed from this generation either because of a call to RemoveObject
michael@0 174 // (or indirectly via MarkUsed) inside NotifyExpired. Fortunately no
michael@0 175 // objects can be added to this generation because it's not the newest
michael@0 176 // generation. We depend on the fact that RemoveObject can only cause
michael@0 177 // the indexes of objects in this generation to *decrease*, not increase.
michael@0 178 // So if we start from the end and work our way backwards we are guaranteed
michael@0 179 // to see each object at least once.
michael@0 180 uint32_t index = generation.Length();
michael@0 181 for (;;) {
michael@0 182 // Objects could have been removed so index could be outside
michael@0 183 // the array
michael@0 184 index = XPCOM_MIN(index, generation.Length());
michael@0 185 if (index == 0)
michael@0 186 break;
michael@0 187 --index;
michael@0 188 NotifyExpired(generation[index]);
michael@0 189 }
michael@0 190 // Any leftover objects from reapGeneration just end up in the new
michael@0 191 // newest-generation. This is bad form, though, so warn if there are any.
michael@0 192 if (!generation.IsEmpty()) {
michael@0 193 NS_WARNING("Expired objects were not removed or marked used");
michael@0 194 }
michael@0 195 // Free excess memory used by the generation array, since we probably
michael@0 196 // just removed most or all of its elements.
michael@0 197 generation.Compact();
michael@0 198 mNewestGeneration = reapGeneration;
michael@0 199 mInAgeOneGeneration = false;
michael@0 200 }
michael@0 201
michael@0 202 /**
michael@0 203 * This just calls AgeOneGeneration K times. Under normal circumstances this
michael@0 204 * will result in all objects getting NotifyExpired called on them, but
michael@0 205 * if NotifyExpired itself marks some objects as used, then those objects
michael@0 206 * might not expire. This would be a good thing to call if we get into
michael@0 207 * a critically-low memory situation.
michael@0 208 */
michael@0 209 void AgeAllGenerations() {
michael@0 210 uint32_t i;
michael@0 211 for (i = 0; i < K; ++i) {
michael@0 212 AgeOneGeneration();
michael@0 213 }
michael@0 214 }
michael@0 215
michael@0 216 class Iterator {
michael@0 217 private:
michael@0 218 nsExpirationTracker<T,K>* mTracker;
michael@0 219 uint32_t mGeneration;
michael@0 220 uint32_t mIndex;
michael@0 221 public:
michael@0 222 Iterator(nsExpirationTracker<T,K>* aTracker)
michael@0 223 : mTracker(aTracker), mGeneration(0), mIndex(0) {}
michael@0 224 T* Next() {
michael@0 225 while (mGeneration < K) {
michael@0 226 nsTArray<T*>* generation = &mTracker->mGenerations[mGeneration];
michael@0 227 if (mIndex < generation->Length()) {
michael@0 228 ++mIndex;
michael@0 229 return (*generation)[mIndex - 1];
michael@0 230 }
michael@0 231 ++mGeneration;
michael@0 232 mIndex = 0;
michael@0 233 }
michael@0 234 return nullptr;
michael@0 235 }
michael@0 236 };
michael@0 237
michael@0 238 friend class Iterator;
michael@0 239
michael@0 240 bool IsEmpty() {
michael@0 241 for (uint32_t i = 0; i < K; ++i) {
michael@0 242 if (!mGenerations[i].IsEmpty())
michael@0 243 return false;
michael@0 244 }
michael@0 245 return true;
michael@0 246 }
michael@0 247
michael@0 248 protected:
michael@0 249 /**
michael@0 250 * This must be overridden to catch notifications. It is called whenever
michael@0 251 * we detect that an object has not been used for at least (K-1)*mTimerPeriod
michael@0 252 * milliseconds. If timer events are not delayed, it will be called within
michael@0 253 * roughly K*mTimerPeriod milliseconds after the last use. (Unless AgeOneGeneration
michael@0 254 * or AgeAllGenerations have been called to accelerate the aging process.)
michael@0 255 *
michael@0 256 * NOTE: These bounds ignore delays in timer firings due to actual work being
michael@0 257 * performed by the browser. We use a slack timer so there is always at least
michael@0 258 * mTimerPeriod milliseconds between firings, which gives us (K-1)*mTimerPeriod
michael@0 259 * as a pretty solid lower bound. The upper bound is rather loose, however.
michael@0 260 * If the maximum amount by which any given timer firing is delayed is D, then
michael@0 261 * the upper bound before NotifyExpired is called is K*(mTimerPeriod + D).
michael@0 262 *
michael@0 263 * The NotifyExpired call is expected to remove the object from the tracker,
michael@0 264 * but it need not. The object (or other objects) could be "resurrected"
michael@0 265 * by calling MarkUsed() on them, or they might just not be removed.
michael@0 266 * Any objects left over that have not been resurrected or removed
michael@0 267 * are placed in the new newest-generation, but this is considered "bad form"
michael@0 268 * and should be avoided (we'll issue a warning). (This recycling counts
michael@0 269 * as "a use" for the purposes of the expiry guarantee above...)
michael@0 270 *
michael@0 271 * For robustness and simplicity, we allow objects to be notified more than
michael@0 272 * once here in the same timer tick.
michael@0 273 */
michael@0 274 virtual void NotifyExpired(T* aObj) = 0;
michael@0 275
michael@0 276 private:
michael@0 277 class ExpirationTrackerObserver;
michael@0 278 nsRefPtr<ExpirationTrackerObserver> mObserver;
michael@0 279 nsTArray<T*> mGenerations[K];
michael@0 280 nsCOMPtr<nsITimer> mTimer;
michael@0 281 uint32_t mTimerPeriod;
michael@0 282 uint32_t mNewestGeneration;
michael@0 283 bool mInAgeOneGeneration;
michael@0 284
michael@0 285 /**
michael@0 286 * Whenever "memory-pressure" is observed, it calls AgeAllGenerations()
michael@0 287 * to minimize memory usage.
michael@0 288 */
michael@0 289 class ExpirationTrackerObserver MOZ_FINAL : public nsIObserver {
michael@0 290 public:
michael@0 291 void Init(nsExpirationTracker<T,K> *obj) {
michael@0 292 mOwner = obj;
michael@0 293 nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
michael@0 294 if (obs) {
michael@0 295 obs->AddObserver(this, "memory-pressure", false);
michael@0 296 }
michael@0 297 }
michael@0 298 void Destroy() {
michael@0 299 mOwner = nullptr;
michael@0 300 nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
michael@0 301 if (obs)
michael@0 302 obs->RemoveObserver(this, "memory-pressure");
michael@0 303 }
michael@0 304 NS_DECL_ISUPPORTS
michael@0 305 NS_DECL_NSIOBSERVER
michael@0 306 private:
michael@0 307 nsExpirationTracker<T,K> *mOwner;
michael@0 308 };
michael@0 309
michael@0 310 static void TimerCallback(nsITimer* aTimer, void* aThis) {
michael@0 311 nsExpirationTracker* tracker = static_cast<nsExpirationTracker*>(aThis);
michael@0 312 tracker->AgeOneGeneration();
michael@0 313 // Cancel the timer if we have no objects to track
michael@0 314 if (tracker->IsEmpty()) {
michael@0 315 tracker->mTimer->Cancel();
michael@0 316 tracker->mTimer = nullptr;
michael@0 317 }
michael@0 318 }
michael@0 319
michael@0 320 nsresult CheckStartTimer() {
michael@0 321 if (mTimer || !mTimerPeriod)
michael@0 322 return NS_OK;
michael@0 323 mTimer = do_CreateInstance("@mozilla.org/timer;1");
michael@0 324 if (!mTimer)
michael@0 325 return NS_ERROR_OUT_OF_MEMORY;
michael@0 326 mTimer->InitWithFuncCallback(TimerCallback, this, mTimerPeriod,
michael@0 327 nsITimer::TYPE_REPEATING_SLACK);
michael@0 328 return NS_OK;
michael@0 329 }
michael@0 330 };
michael@0 331
michael@0 332 template<class T, uint32_t K>
michael@0 333 NS_IMETHODIMP
michael@0 334 nsExpirationTracker<T, K>::ExpirationTrackerObserver::Observe(nsISupports *aSubject,
michael@0 335 const char *aTopic,
michael@0 336 const char16_t *aData)
michael@0 337 {
michael@0 338 if (!strcmp(aTopic, "memory-pressure") && mOwner)
michael@0 339 mOwner->AgeAllGenerations();
michael@0 340 return NS_OK;
michael@0 341 }
michael@0 342
michael@0 343 template <class T, uint32_t K>
michael@0 344 NS_IMETHODIMP_(MozExternalRefCountType)
michael@0 345 nsExpirationTracker<T,K>::ExpirationTrackerObserver::AddRef(void)
michael@0 346 {
michael@0 347 MOZ_ASSERT(int32_t(mRefCnt) >= 0, "illegal refcnt");
michael@0 348 NS_ASSERT_OWNINGTHREAD(ExpirationTrackerObserver);
michael@0 349 ++mRefCnt;
michael@0 350 NS_LOG_ADDREF(this, mRefCnt, "ExpirationTrackerObserver", sizeof(*this));
michael@0 351 return mRefCnt;
michael@0 352 }
michael@0 353
michael@0 354 template <class T, uint32_t K>
michael@0 355 NS_IMETHODIMP_(MozExternalRefCountType)
michael@0 356 nsExpirationTracker<T,K>::ExpirationTrackerObserver::Release(void)
michael@0 357 {
michael@0 358 MOZ_ASSERT(int32_t(mRefCnt) > 0, "dup release");
michael@0 359 NS_ASSERT_OWNINGTHREAD(ExpirationTrackerObserver);
michael@0 360 --mRefCnt;
michael@0 361 NS_LOG_RELEASE(this, mRefCnt, "ExpirationTrackerObserver");
michael@0 362 if (mRefCnt == 0) {
michael@0 363 NS_ASSERT_OWNINGTHREAD(ExpirationTrackerObserver);
michael@0 364 mRefCnt = 1; /* stabilize */
michael@0 365 delete (this);
michael@0 366 return 0;
michael@0 367 }
michael@0 368 return mRefCnt;
michael@0 369 }
michael@0 370
michael@0 371 template <class T, uint32_t K>
michael@0 372 NS_IMETHODIMP
michael@0 373 nsExpirationTracker<T,K>::ExpirationTrackerObserver::QueryInterface(REFNSIID aIID,
michael@0 374 void** aInstancePtr)
michael@0 375 {
michael@0 376 NS_ASSERTION(aInstancePtr,
michael@0 377 "QueryInterface requires a non-NULL destination!");
michael@0 378 nsresult rv = NS_ERROR_FAILURE;
michael@0 379 NS_INTERFACE_TABLE(ExpirationTrackerObserver, nsIObserver)
michael@0 380 return rv;
michael@0 381 }
michael@0 382
michael@0 383 #endif /*NSEXPIRATIONTRACKER_H_*/

mercurial