Line data Source code
1 : /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 : /* vim: set ts=8 sts=2 et sw=2 tw=80: */
3 : /* This Source Code Form is subject to the terms of the Mozilla Public
4 : * License, v. 2.0. If a copy of the MPL was not distributed with this
5 : * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 :
7 : #ifndef NSEXPIRATIONTRACKER_H_
8 : #define NSEXPIRATIONTRACKER_H_
9 :
10 : #include "mozilla/Logging.h"
11 : #include "nsTArray.h"
12 : #include "nsITimer.h"
13 : #include "nsCOMPtr.h"
14 : #include "nsAutoPtr.h"
15 : #include "nsComponentManagerUtils.h"
16 : #include "nsIEventTarget.h"
17 : #include "nsIObserver.h"
18 : #include "nsIObserverService.h"
19 : #include "nsThreadUtils.h"
20 : #include "mozilla/Attributes.h"
21 : #include "mozilla/Services.h"
22 :
23 : /**
24 : * Data used to track the expiration state of an object. We promise that this
25 : * is 32 bits so that objects that includes this as a field can pad and align
26 : * efficiently.
27 : */
28 : struct nsExpirationState
29 : {
30 : enum
31 : {
32 : NOT_TRACKED = (1U << 4) - 1,
33 : MAX_INDEX_IN_GENERATION = (1U << 28) - 1
34 : };
35 :
36 216 : nsExpirationState() : mGeneration(NOT_TRACKED) {}
37 460 : bool IsTracked() { return mGeneration != NOT_TRACKED; }
38 :
39 : /**
40 : * The generation that this object belongs to, or NOT_TRACKED.
41 : */
42 : uint32_t mGeneration:4;
43 : uint32_t mIndexInGeneration:28;
44 : };
45 :
46 : /**
47 : * ExpirationTracker classes:
48 : * - ExpirationTrackerImpl (Thread-safe class)
49 : * - nsExpirationTracker (Main-thread only class)
50 : *
51 : * These classes can track the lifetimes and usage of a large number of
52 : * objects, and send a notification some window of time after a live object was
53 : * last used. This is very useful when you manage a large number of objects
54 : * and want to flush some after they haven't been used for a while.
55 : * nsExpirationTracker is designed to be very space and time efficient.
56 : *
57 : * The type parameter T is the object type that we will track pointers to. T
58 : * must include an accessible method GetExpirationState() that returns a
59 : * pointer to an nsExpirationState associated with the object (preferably,
60 : * stored in a field of the object).
61 : *
62 : * The parameter K is the number of generations that will be used. Increasing
63 : * the number of generations narrows the window within which we promise
64 : * to fire notifications, at a slight increase in space cost for the tracker.
65 : * We require 2 <= K <= nsExpirationState::NOT_TRACKED (currently 15).
66 : *
67 : * To use this class, you need to inherit from it and override the
68 : * NotifyExpired() method.
69 : *
70 : * The approach is to track objects in K generations. When an object is accessed
71 : * it moves from its current generation to the newest generation. Generations
72 : * are stored in a cyclic array; when a timer interrupt fires, we advance
73 : * the current generation pointer to effectively age all objects very efficiently.
74 : * By storing information in each object about its generation and index within its
75 : * generation array, we make removal of objects from a generation very cheap.
76 : *
77 : * Future work:
78 : * -- Add a method to change the timer period?
79 : */
80 :
81 : /**
82 : * Base class for ExiprationTracker implementations.
83 : *
84 : * nsExpirationTracker class below is a specialized class to be inherited by the
85 : * instances to be accessed only on main-thread.
86 : *
87 : * For creating a thread-safe tracker, you can define a subclass inheriting this
88 : * base class and specialize the Mutex and AutoLock to be used.
89 : *
90 : */
91 : template<typename T, uint32_t K, typename Mutex, typename AutoLock>
92 : class ExpirationTrackerImpl
93 : {
94 : public:
95 : /**
96 : * Initialize the tracker.
97 : * @param aTimerPeriod the timer period in milliseconds. The guarantees
98 : * provided by the tracker are defined in terms of this period. If the
99 : * period is zero, then we don't use a timer and rely on someone calling
100 : * AgeOneGenerationLocked explicitly.
101 : * @param aName the name of the subclass for telemetry.
102 : * @param aEventTarget the optional event target on main thread to label the
103 : * runnable of the asynchronous invocation to NotifyExpired().
104 :
105 : */
106 19 : ExpirationTrackerImpl(uint32_t aTimerPeriod,
107 : const char* aName,
108 : nsIEventTarget* aEventTarget = nullptr)
109 : : mTimerPeriod(aTimerPeriod)
110 : , mNewestGeneration(0)
111 : , mInAgeOneGeneration(false)
112 : , mName(aName)
113 19 : , mEventTarget(aEventTarget)
114 : {
115 : static_assert(K >= 2 && K <= nsExpirationState::NOT_TRACKED,
116 : "Unsupported number of generations (must be 2 <= K <= 15)");
117 19 : MOZ_ASSERT(NS_IsMainThread());
118 19 : if (mEventTarget) {
119 13 : bool current = false;
120 : // NOTE: The following check+crash could be condensed into a
121 : // MOZ_RELEASE_ASSERT, but that triggers a segfault during compilation in
122 : // clang 3.8. Once we don't have to care about clang 3.8 anymore, though,
123 : // we can convert to MOZ_RELEASE_ASSERT here.
124 13 : if (MOZ_UNLIKELY(NS_FAILED(mEventTarget->IsOnCurrentThread(¤t)) ||
125 : !current)) {
126 0 : MOZ_CRASH("Provided event target must be on the main thread");
127 : }
128 : }
129 19 : mObserver = new ExpirationTrackerObserver();
130 19 : mObserver->Init(this);
131 19 : }
132 :
133 0 : virtual ~ExpirationTrackerImpl()
134 : {
135 0 : MOZ_ASSERT(NS_IsMainThread());
136 0 : if (mTimer) {
137 0 : mTimer->Cancel();
138 : }
139 0 : mObserver->Destroy();
140 0 : }
141 :
142 : /**
143 : * Add an object to be tracked. It must not already be tracked. It will
144 : * be added to the newest generation, i.e., as if it was just used.
145 : * @return an error on out-of-memory
146 : */
147 98 : nsresult AddObjectLocked(T* aObj, const AutoLock& aAutoLock)
148 : {
149 98 : nsExpirationState* state = aObj->GetExpirationState();
150 98 : MOZ_ASSERT(!state->IsTracked(),
151 : "Tried to add an object that's already tracked");
152 98 : nsTArray<T*>& generation = mGenerations[mNewestGeneration];
153 98 : uint32_t index = generation.Length();
154 98 : if (index > nsExpirationState::MAX_INDEX_IN_GENERATION) {
155 0 : NS_WARNING("More than 256M elements tracked, this is probably a problem");
156 0 : return NS_ERROR_OUT_OF_MEMORY;
157 : }
158 98 : if (index == 0) {
159 : // We might need to start the timer
160 69 : nsresult rv = CheckStartTimerLocked(aAutoLock);
161 69 : if (NS_FAILED(rv)) {
162 0 : return rv;
163 : }
164 : }
165 98 : if (!generation.AppendElement(aObj)) {
166 0 : return NS_ERROR_OUT_OF_MEMORY;
167 : }
168 98 : state->mGeneration = mNewestGeneration;
169 98 : state->mIndexInGeneration = index;
170 98 : return NS_OK;
171 : }
172 :
173 : /**
174 : * Remove an object from the tracker. It must currently be tracked.
175 : */
176 75 : void RemoveObjectLocked(T* aObj, const AutoLock& aAutoLock)
177 : {
178 75 : nsExpirationState* state = aObj->GetExpirationState();
179 75 : MOZ_ASSERT(state->IsTracked(), "Tried to remove an object that's not tracked");
180 75 : nsTArray<T*>& generation = mGenerations[state->mGeneration];
181 75 : uint32_t index = state->mIndexInGeneration;
182 75 : MOZ_ASSERT(generation.Length() > index &&
183 : generation[index] == aObj, "Object is lying about its index");
184 : // Move the last object to fill the hole created by removing aObj
185 75 : uint32_t last = generation.Length() - 1;
186 75 : T* lastObj = generation[last];
187 75 : generation[index] = lastObj;
188 75 : lastObj->GetExpirationState()->mIndexInGeneration = index;
189 75 : generation.RemoveElementAt(last);
190 75 : MOZ_ASSERT(generation.Length() == last);
191 75 : state->mGeneration = nsExpirationState::NOT_TRACKED;
192 : // We do not check whether we need to stop the timer here. The timer
193 : // will check that itself next time it fires. Checking here would not
194 : // be efficient since we'd need to track all generations. Also we could
195 : // thrash by incessantly creating and destroying timers if someone
196 : // kept adding and removing an object from the tracker.
197 75 : }
198 :
199 : /**
200 : * Notify that an object has been used.
201 : * @return an error if we lost the object from the tracker...
202 : */
203 40 : nsresult MarkUsedLocked(T* aObj, const AutoLock& aAutoLock)
204 : {
205 40 : nsExpirationState* state = aObj->GetExpirationState();
206 40 : if (mNewestGeneration == state->mGeneration) {
207 35 : return NS_OK;
208 : }
209 5 : RemoveObjectLocked(aObj, aAutoLock);
210 5 : return AddObjectLocked(aObj, aAutoLock);
211 : }
212 :
213 : /**
214 : * The timer calls this, but it can also be manually called if you want
215 : * to age objects "artifically". This can result in calls to NotifyExpiredLocked.
216 : */
217 19 : void AgeOneGenerationLocked(const AutoLock& aAutoLock)
218 : {
219 19 : if (mInAgeOneGeneration) {
220 0 : NS_WARNING("Can't reenter AgeOneGeneration from NotifyExpired");
221 0 : return;
222 : }
223 :
224 19 : mInAgeOneGeneration = true;
225 : uint32_t reapGeneration =
226 19 : mNewestGeneration > 0 ? mNewestGeneration - 1 : K - 1;
227 19 : nsTArray<T*>& generation = mGenerations[reapGeneration];
228 : // The following is rather tricky. We have to cope with objects being
229 : // removed from this generation either because of a call to RemoveObject
230 : // (or indirectly via MarkUsedLocked) inside NotifyExpiredLocked. Fortunately
231 : // no objects can be added to this generation because it's not the newest
232 : // generation. We depend on the fact that RemoveObject can only cause
233 : // the indexes of objects in this generation to *decrease*, not increase.
234 : // So if we start from the end and work our way backwards we are guaranteed
235 : // to see each object at least once.
236 19 : size_t index = generation.Length();
237 : for (;;) {
238 : // Objects could have been removed so index could be outside
239 : // the array
240 33 : index = XPCOM_MIN(index, generation.Length());
241 26 : if (index == 0) {
242 19 : break;
243 : }
244 7 : --index;
245 7 : NotifyExpiredLocked(generation[index], aAutoLock);
246 : }
247 : // Any leftover objects from reapGeneration just end up in the new
248 : // newest-generation. This is bad form, though, so warn if there are any.
249 19 : if (!generation.IsEmpty()) {
250 0 : NS_WARNING("Expired objects were not removed or marked used");
251 : }
252 : // Free excess memory used by the generation array, since we probably
253 : // just removed most or all of its elements.
254 19 : generation.Compact();
255 19 : mNewestGeneration = reapGeneration;
256 19 : mInAgeOneGeneration = false;
257 : }
258 :
259 : /**
260 : * This just calls AgeOneGenerationLocked K times. Under normal circumstances
261 : * this will result in all objects getting NotifyExpiredLocked called on them,
262 : * but if NotifyExpiredLocked itself marks some objects as used, then those
263 : * objects might not expire. This would be a good thing to call if we get into
264 : * a critically-low memory situation.
265 : */
266 0 : void AgeAllGenerationsLocked(const AutoLock& aAutoLock)
267 : {
268 : uint32_t i;
269 0 : for (i = 0; i < K; ++i) {
270 0 : AgeOneGenerationLocked(aAutoLock);
271 : }
272 0 : }
273 :
274 : class Iterator
275 : {
276 : private:
277 : ExpirationTrackerImpl<T, K, Mutex, AutoLock>* mTracker;
278 : uint32_t mGeneration;
279 : uint32_t mIndex;
280 : public:
281 86 : Iterator(ExpirationTrackerImpl<T, K, Mutex, AutoLock>* aTracker,
282 : AutoLock& aAutoLock)
283 : : mTracker(aTracker)
284 : , mGeneration(0)
285 86 : , mIndex(0)
286 : {
287 86 : }
288 :
289 385 : T* Next()
290 : {
291 643 : while (mGeneration < K) {
292 299 : nsTArray<T*>* generation = &mTracker->mGenerations[mGeneration];
293 299 : if (mIndex < generation->Length()) {
294 41 : ++mIndex;
295 41 : return (*generation)[mIndex - 1];
296 : }
297 258 : ++mGeneration;
298 258 : mIndex = 0;
299 : }
300 86 : return nullptr;
301 : }
302 : };
303 :
304 : friend class Iterator;
305 :
306 19 : bool IsEmptyLocked(const AutoLock& aAutoLock)
307 : {
308 46 : for (uint32_t i = 0; i < K; ++i) {
309 45 : if (!mGenerations[i].IsEmpty()) {
310 18 : return false;
311 : }
312 : }
313 1 : return true;
314 : }
315 :
316 : protected:
317 : /**
318 : * This must be overridden to catch notifications. It is called whenever
319 : * we detect that an object has not been used for at least (K-1)*mTimerPeriod
320 : * milliseconds. If timer events are not delayed, it will be called within
321 : * roughly K*mTimerPeriod milliseconds after the last use.
322 : * (Unless AgeOneGenerationLocked or AgeAllGenerationsLocked have been called
323 : * to accelerate the aging process.)
324 : *
325 : * NOTE: These bounds ignore delays in timer firings due to actual work being
326 : * performed by the browser. We use a slack timer so there is always at least
327 : * mTimerPeriod milliseconds between firings, which gives us (K-1)*mTimerPeriod
328 : * as a pretty solid lower bound. The upper bound is rather loose, however.
329 : * If the maximum amount by which any given timer firing is delayed is D, then
330 : * the upper bound before NotifyExpiredLocked is called is K*(mTimerPeriod + D).
331 : *
332 : * The NotifyExpiredLocked call is expected to remove the object from the tracker,
333 : * but it need not. The object (or other objects) could be "resurrected"
334 : * by calling MarkUsedLocked() on them, or they might just not be removed.
335 : * Any objects left over that have not been resurrected or removed
336 : * are placed in the new newest-generation, but this is considered "bad form"
337 : * and should be avoided (we'll issue a warning). (This recycling counts
338 : * as "a use" for the purposes of the expiry guarantee above...)
339 : *
340 : * For robustness and simplicity, we allow objects to be notified more than
341 : * once here in the same timer tick.
342 : */
343 : virtual void NotifyExpiredLocked(T*, const AutoLock&) = 0;
344 :
345 : virtual Mutex& GetMutex() = 0;
346 :
347 : private:
348 : class ExpirationTrackerObserver;
349 : RefPtr<ExpirationTrackerObserver> mObserver;
350 : nsTArray<T*> mGenerations[K];
351 : nsCOMPtr<nsITimer> mTimer;
352 : uint32_t mTimerPeriod;
353 : uint32_t mNewestGeneration;
354 : bool mInAgeOneGeneration;
355 : const char* const mName; // Used for timer firing profiling.
356 : const nsCOMPtr<nsIEventTarget> mEventTarget;
357 :
358 : /**
359 : * Whenever "memory-pressure" is observed, it calls AgeAllGenerationsLocked()
360 : * to minimize memory usage.
361 : */
362 19 : class ExpirationTrackerObserver final : public nsIObserver
363 : {
364 : public:
365 19 : void Init(ExpirationTrackerImpl<T, K, Mutex, AutoLock>* aObj)
366 : {
367 19 : mOwner = aObj;
368 38 : nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
369 19 : if (obs) {
370 19 : obs->AddObserver(this, "memory-pressure", false);
371 : }
372 19 : }
373 0 : void Destroy()
374 : {
375 0 : mOwner = nullptr;
376 0 : nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
377 0 : if (obs) {
378 0 : obs->RemoveObserver(this, "memory-pressure");
379 : }
380 0 : }
381 : NS_DECL_ISUPPORTS
382 : NS_DECL_NSIOBSERVER
383 : private:
384 : ExpirationTrackerImpl<T, K, Mutex, AutoLock>* mOwner;
385 : };
386 :
387 0 : void HandleLowMemory() {
388 0 : AutoLock lock(GetMutex());
389 0 : AgeAllGenerationsLocked(lock);
390 0 : }
391 :
392 19 : void HandleTimeout() {
393 19 : AutoLock lock(GetMutex());
394 19 : AgeOneGenerationLocked(lock);
395 : // Cancel the timer if we have no objects to track
396 19 : if (IsEmptyLocked(lock)) {
397 1 : mTimer->Cancel();
398 1 : mTimer = nullptr;
399 : }
400 19 : }
401 :
402 19 : static void TimerCallback(nsITimer* aTimer, void* aThis)
403 : {
404 19 : ExpirationTrackerImpl* tracker = static_cast<ExpirationTrackerImpl*>(aThis);
405 19 : tracker->HandleTimeout();
406 19 : }
407 :
408 69 : nsresult CheckStartTimerLocked(const AutoLock& aAutoLock)
409 : {
410 69 : if (mTimer || !mTimerPeriod) {
411 57 : return NS_OK;
412 : }
413 12 : mTimer = do_CreateInstance("@mozilla.org/timer;1");
414 12 : if (!mTimer) {
415 0 : return NS_ERROR_OUT_OF_MEMORY;
416 : }
417 12 : if (mEventTarget) {
418 10 : mTimer->SetTarget(mEventTarget);
419 2 : } else if (!NS_IsMainThread()) {
420 : // TimerCallback should always be run on the main thread to prevent races
421 : // to the destruction of the tracker.
422 0 : nsCOMPtr<nsIEventTarget> target = do_GetMainThread();
423 0 : NS_ENSURE_STATE(target);
424 0 : mTimer->SetTarget(target);
425 : }
426 24 : mTimer->InitWithNamedFuncCallback(
427 : TimerCallback,
428 : this,
429 : mTimerPeriod,
430 : nsITimer::TYPE_REPEATING_SLACK_LOW_PRIORITY,
431 12 : mName);
432 12 : return NS_OK;
433 : }
434 : };
435 :
436 : namespace detail {
437 :
438 : class PlaceholderLock {
439 : public:
440 : void Lock() {}
441 : void Unlock() {}
442 : };
443 :
444 : class PlaceholderAutoLock {
445 : public:
446 280 : explicit PlaceholderAutoLock(PlaceholderLock&) { }
447 : ~PlaceholderAutoLock() = default;
448 :
449 : };
450 :
451 : template<typename T, uint32_t K>
452 : using SingleThreadedExpirationTracker =
453 : ExpirationTrackerImpl<T, K, PlaceholderLock, PlaceholderAutoLock>;
454 :
455 : } // namespace detail
456 :
457 : template<typename T, uint32_t K>
458 : class nsExpirationTracker : protected ::detail::SingleThreadedExpirationTracker<T, K>
459 : {
460 : typedef ::detail::PlaceholderLock Lock;
461 : typedef ::detail::PlaceholderAutoLock AutoLock;
462 :
463 : Lock mLock;
464 :
465 175 : AutoLock FakeLock() {
466 175 : return AutoLock(mLock);
467 : }
468 :
469 105 : Lock& GetMutex() override
470 : {
471 105 : return mLock;
472 : }
473 :
474 7 : void NotifyExpiredLocked(T* aObject, const AutoLock&) override
475 : {
476 7 : NotifyExpired(aObject);
477 7 : }
478 :
479 : protected:
480 : virtual void NotifyExpired(T* aObj) = 0;
481 :
482 : public:
483 16 : nsExpirationTracker(uint32_t aTimerPeriod,
484 : const char* aName,
485 : nsIEventTarget* aEventTarget = nullptr)
486 : : ::detail::SingleThreadedExpirationTracker<T, K>(aTimerPeriod,
487 : aName,
488 16 : aEventTarget)
489 16 : { }
490 :
491 0 : virtual ~nsExpirationTracker()
492 0 : { }
493 :
494 79 : nsresult AddObject(T* aObj)
495 : {
496 79 : return this->AddObjectLocked(aObj, FakeLock());
497 : }
498 :
499 56 : void RemoveObject(T* aObj)
500 : {
501 56 : this->RemoveObjectLocked(aObj, FakeLock());
502 56 : }
503 :
504 40 : nsresult MarkUsed(T* aObj)
505 : {
506 40 : return this->MarkUsedLocked(aObj, FakeLock());
507 : }
508 :
509 0 : void AgeOneGeneration()
510 : {
511 0 : this->AgeOneGenerationLocked(FakeLock());
512 0 : }
513 :
514 0 : void AgeAllGenerations()
515 : {
516 0 : this->AgeAllGenerationsLocked(FakeLock());
517 0 : }
518 :
519 : class Iterator
520 : {
521 : private:
522 : AutoLock mAutoLock;
523 : typename ExpirationTrackerImpl<T, K, Lock, AutoLock>::Iterator mIterator;
524 : public:
525 86 : explicit Iterator(nsExpirationTracker<T, K>* aTracker)
526 : : mAutoLock(aTracker->GetMutex())
527 86 : , mIterator(aTracker, mAutoLock)
528 : {
529 86 : }
530 :
531 127 : T* Next()
532 : {
533 127 : return mIterator.Next();
534 : }
535 : };
536 :
537 : friend class Iterator;
538 :
539 0 : bool IsEmpty()
540 : {
541 0 : return this->IsEmptyLocked(FakeLock());
542 : }
543 : };
544 :
545 : template<typename T, uint32_t K, typename Mutex, typename AutoLock>
546 : NS_IMETHODIMP
547 0 : ExpirationTrackerImpl<T, K, Mutex, AutoLock>::
548 : ExpirationTrackerObserver::Observe(
549 : nsISupports* aSubject, const char* aTopic, const char16_t* aData)
550 : {
551 0 : if (!strcmp(aTopic, "memory-pressure") && mOwner) {
552 0 : mOwner->HandleLowMemory();
553 : }
554 0 : return NS_OK;
555 : }
556 :
557 : template<class T, uint32_t K, typename Mutex, typename AutoLock>
558 : NS_IMETHODIMP_(MozExternalRefCountType)
559 38 : ExpirationTrackerImpl<T, K, Mutex, AutoLock>::
560 : ExpirationTrackerObserver::AddRef(void)
561 : {
562 38 : MOZ_ASSERT(int32_t(mRefCnt) >= 0, "illegal refcnt");
563 38 : NS_ASSERT_OWNINGTHREAD(ExpirationTrackerObserver);
564 38 : ++mRefCnt;
565 38 : NS_LOG_ADDREF(this, mRefCnt, "ExpirationTrackerObserver", sizeof(*this));
566 38 : return mRefCnt;
567 : }
568 :
569 : template<class T, uint32_t K, typename Mutex, typename AutoLock>
570 : NS_IMETHODIMP_(MozExternalRefCountType)
571 0 : ExpirationTrackerImpl<T, K, Mutex, AutoLock>::
572 : ExpirationTrackerObserver::Release(void)
573 : {
574 0 : MOZ_ASSERT(int32_t(mRefCnt) > 0, "dup release");
575 0 : NS_ASSERT_OWNINGTHREAD(ExpirationTrackerObserver);
576 0 : --mRefCnt;
577 0 : NS_LOG_RELEASE(this, mRefCnt, "ExpirationTrackerObserver");
578 0 : if (mRefCnt == 0) {
579 0 : NS_ASSERT_OWNINGTHREAD(ExpirationTrackerObserver);
580 0 : mRefCnt = 1; /* stabilize */
581 : delete (this);
582 0 : return 0;
583 : }
584 0 : return mRefCnt;
585 : }
586 :
587 : template<class T, uint32_t K, typename Mutex, typename AutoLock>
588 : NS_IMETHODIMP
589 0 : ExpirationTrackerImpl<T, K, Mutex, AutoLock>::
590 : ExpirationTrackerObserver::QueryInterface(
591 : REFNSIID aIID, void** aInstancePtr)
592 : {
593 0 : NS_ASSERTION(aInstancePtr,
594 : "QueryInterface requires a non-NULL destination!");
595 0 : nsresult rv = NS_ERROR_FAILURE;
596 0 : NS_INTERFACE_TABLE(ExpirationTrackerObserver, nsIObserver)
597 0 : return rv;
598 : }
599 :
600 : #endif /*NSEXPIRATIONTRACKER_H_*/
|