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