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