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