mirror of
				https://github.com/mozilla/gecko-dev.git
				synced 2025-10-31 08:18:54 +02:00 
			
		
		
		
	Bug 1961386 - part 6: Use binary search in Add/RemoveTimerInternal. r=xpcom-reviewers,nika,jlink
The linear scan for the insertion point can be replaced by a binary search. All the other logic of finding empty slots or removing entries remains unchanged by this. Resulting complexity: - Changed: AddTimer becomes minimum O(log[n]) (due to the lower boundary of the binary search) to maximum O(n) depending on the luck we have with finding empty slots. - Changed: RemoveTimer becomes O(log(n)) always (for the binary search) as we postpone the removal of canceled timers to happen in the Run loop or through re-use. - Extracting the next timer to fire on the TimerThread remains always O(n) (but happens on the timer thread, where it might disturb less). - Removing the leading canceled timers remains always O(n) (but happens on the timer thread, where it might disturb less). Differential Revision: https://phabricator.services.mozilla.com/D249902
This commit is contained in:
		
							parent
							
								
									529796aead
								
							
						
					
					
						commit
						0f8df1b32e
					
				
					 2 changed files with 56 additions and 44 deletions
				
			
		|  | @ -670,21 +670,6 @@ struct IntervalComparator { | ||||||
| 
 | 
 | ||||||
| }  // namespace
 | }  // namespace
 | ||||||
| 
 | 
 | ||||||
| size_t TimerThread::ComputeTimerInsertionIndex(const TimeStamp& timeout) const { |  | ||||||
|   mMonitor.AssertCurrentThreadOwns(); |  | ||||||
| 
 |  | ||||||
|   const size_t timerCount = mTimers.Length(); |  | ||||||
| 
 |  | ||||||
|   size_t firstGtIndex = 0; |  | ||||||
|   while (firstGtIndex < timerCount && |  | ||||||
|          (!mTimers[firstGtIndex].mTimerImpl || |  | ||||||
|           mTimers[firstGtIndex].mTimeout <= timeout)) { |  | ||||||
|     ++firstGtIndex; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   return firstGtIndex; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| TimeStamp TimerThread::ComputeWakeupTimeFromTimers() const { | TimeStamp TimerThread::ComputeWakeupTimeFromTimers() const { | ||||||
|   mMonitor.AssertCurrentThreadOwns(); |   mMonitor.AssertCurrentThreadOwns(); | ||||||
| 
 | 
 | ||||||
|  | @ -1126,6 +1111,14 @@ TimeStamp TimerThread::FindNextFireTimeForCurrentThread(TimeStamp aDefault, | ||||||
|   return aDefault; |   return aDefault; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | void TimerThread::AssertTimersSortedAndUnique() { | ||||||
|  |   MOZ_ASSERT(std::is_sorted(mTimers.begin(), mTimers.end()), | ||||||
|  |              "mTimers must be sorted."); | ||||||
|  |   MOZ_ASSERT( | ||||||
|  |       std::adjacent_find(mTimers.begin(), mTimers.end()) == mTimers.end(), | ||||||
|  |       "mTimers must not contain duplicate entries."); | ||||||
|  | } | ||||||
|  | 
 | ||||||
| // This function must be called from within a lock
 | // This function must be called from within a lock
 | ||||||
| // Also: we hold the mutex for the nsTimerImpl.
 | // Also: we hold the mutex for the nsTimerImpl.
 | ||||||
| void TimerThread::AddTimerInternal(nsTimerImpl& aTimer) { | void TimerThread::AddTimerInternal(nsTimerImpl& aTimer) { | ||||||
|  | @ -1134,11 +1127,9 @@ void TimerThread::AddTimerInternal(nsTimerImpl& aTimer) { | ||||||
|   AUTO_TIMERS_STATS(TimerThread_AddTimerInternal); |   AUTO_TIMERS_STATS(TimerThread_AddTimerInternal); | ||||||
|   LogTimerEvent::LogDispatch(&aTimer); |   LogTimerEvent::LogDispatch(&aTimer); | ||||||
| 
 | 
 | ||||||
|   // TODO: Add is_sorted check after changing our book-keeping.
 |  | ||||||
| 
 |  | ||||||
|   // Do the AddRef here.
 |   // Do the AddRef here.
 | ||||||
|   Entry toBeAdded{aTimer}; |   Entry toBeAdded{aTimer}; | ||||||
|   size_t insertAt = ComputeTimerInsertionIndex(aTimer.mTimeout); |   size_t insertAt = mTimers.IndexOfFirstElementGt(toBeAdded); | ||||||
| 
 | 
 | ||||||
|   if (insertAt > 0 && !mTimers[insertAt - 1].mTimerImpl) { |   if (insertAt > 0 && !mTimers[insertAt - 1].mTimerImpl) { | ||||||
|     // Very common scenario in practice: The timer just before the insertion
 |     // Very common scenario in practice: The timer just before the insertion
 | ||||||
|  | @ -1148,6 +1139,7 @@ void TimerThread::AddTimerInternal(nsTimerImpl& aTimer) { | ||||||
|     // our very own canceled slot here, given the order of the array.
 |     // our very own canceled slot here, given the order of the array.
 | ||||||
|     AUTO_TIMERS_STATS(TimerThread_AddTimerInternal_ReuseBefore); |     AUTO_TIMERS_STATS(TimerThread_AddTimerInternal_ReuseBefore); | ||||||
|     mTimers[insertAt - 1] = std::move(toBeAdded); |     mTimers[insertAt - 1] = std::move(toBeAdded); | ||||||
|  |     AssertTimersSortedAndUnique(); | ||||||
|     return; |     return; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  | @ -1173,6 +1165,8 @@ void TimerThread::AddTimerInternal(nsTimerImpl& aTimer) { | ||||||
|     AUTO_TIMERS_STATS(TimerThread_AddTimerInternal_Expand); |     AUTO_TIMERS_STATS(TimerThread_AddTimerInternal_Expand); | ||||||
|     mTimers.AppendElement(std::move(toBeAdded)); |     mTimers.AppendElement(std::move(toBeAdded)); | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   AssertTimersSortedAndUnique(); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // This function must be called from within a lock
 | // This function must be called from within a lock
 | ||||||
|  | @ -1186,15 +1180,15 @@ bool TimerThread::RemoveTimerInternal(nsTimerImpl& aTimer) { | ||||||
|     return false; |     return false; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   // TODO: Add is_sorted check after changing our book-keeping.
 |   size_t removeAt = mTimers.BinaryIndexOf(EntryKey{aTimer}); | ||||||
| 
 |   if (removeAt != nsTArray<Entry>::NoIndex) { | ||||||
|   AUTO_TIMERS_STATS(TimerThread_RemoveTimerInternal_in_list); |     MOZ_ASSERT(mTimers[removeAt].mTimerImpl == &aTimer); | ||||||
|   for (auto& entry : mTimers) { |     // Mark the timer as canceled, defer the removal to the timer thread.
 | ||||||
|     if (entry.mTimerImpl == &aTimer) { |     mTimers[removeAt].mTimerImpl = nullptr; | ||||||
|       entry.mTimerImpl = nullptr; |     AssertTimersSortedAndUnique(); | ||||||
|       return true; |     return true; | ||||||
|     } |  | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|   MOZ_ASSERT_UNREACHABLE("Not found in the list but it should be!?"); |   MOZ_ASSERT_UNREACHABLE("Not found in the list but it should be!?"); | ||||||
|   return false; |   return false; | ||||||
| } | } | ||||||
|  | @ -1203,6 +1197,9 @@ void TimerThread::RemoveLeadingCanceledTimersInternal() { | ||||||
|   mMonitor.AssertCurrentThreadOwns(); |   mMonitor.AssertCurrentThreadOwns(); | ||||||
|   AUTO_TIMERS_STATS(TimerThread_RemoveLeadingCanceledTimersInternal); |   AUTO_TIMERS_STATS(TimerThread_RemoveLeadingCanceledTimersInternal); | ||||||
| 
 | 
 | ||||||
|  |   // Let's check if we are still sorted before removing the canceled timers.
 | ||||||
|  |   AssertTimersSortedAndUnique(); | ||||||
|  | 
 | ||||||
|   size_t toRemove = 0; |   size_t toRemove = 0; | ||||||
|   while (toRemove < mTimers.Length() && !mTimers[toRemove].mTimerImpl) { |   while (toRemove < mTimers.Length() && !mTimers[toRemove].mTimerImpl) { | ||||||
|     ++toRemove; |     ++toRemove; | ||||||
|  |  | ||||||
|  | @ -70,6 +70,7 @@ class TimerThread final : public mozilla::Runnable, public nsIObserver { | ||||||
|       MOZ_REQUIRES(mMonitor, aTimer.mMutex); |       MOZ_REQUIRES(mMonitor, aTimer.mMutex); | ||||||
|   void RemoveLeadingCanceledTimersInternal() MOZ_REQUIRES(mMonitor); |   void RemoveLeadingCanceledTimersInternal() MOZ_REQUIRES(mMonitor); | ||||||
|   nsresult Init() MOZ_REQUIRES(mMonitor); |   nsresult Init() MOZ_REQUIRES(mMonitor); | ||||||
|  |   void AssertTimersSortedAndUnique() MOZ_REQUIRES(mMonitor); | ||||||
| 
 | 
 | ||||||
|   // Using atomic because this value is written to in one place, and read from
 |   // Using atomic because this value is written to in one place, and read from
 | ||||||
|   // in another, and those two locations are likely to be executed from separate
 |   // in another, and those two locations are likely to be executed from separate
 | ||||||
|  | @ -91,11 +92,37 @@ class TimerThread final : public mozilla::Runnable, public nsIObserver { | ||||||
|   bool mNotified MOZ_GUARDED_BY(mMonitor); |   bool mNotified MOZ_GUARDED_BY(mMonitor); | ||||||
|   bool mSleeping MOZ_GUARDED_BY(mMonitor); |   bool mSleeping MOZ_GUARDED_BY(mMonitor); | ||||||
| 
 | 
 | ||||||
|   struct Entry final { |   struct EntryKey { | ||||||
|  |     explicit EntryKey(nsTimerImpl& aTimerImpl) | ||||||
|  |         : mTimeout(aTimerImpl.mTimeout), mTimerSeq(aTimerImpl.mTimerSeq) {} | ||||||
|  | 
 | ||||||
|  |     // The comparison operators must ensure to detect equality only for
 | ||||||
|  |     // equal mTimerImpl except for canceled timers.
 | ||||||
|  |     // This is achieved through the sequence number.
 | ||||||
|  |     // Currently we maintain a FIFO order for timers with equal timeout.
 | ||||||
|  |     // Note that it might make sense to flip the sequence order to favor
 | ||||||
|  |     // timeouts with smaller delay as they are most likely more sensitive
 | ||||||
|  |     // to jitter. But we strictly test for FIFO order in our gtests.
 | ||||||
|  | 
 | ||||||
|  |     bool operator==(const EntryKey& aRhs) const { | ||||||
|  |       return (mTimeout == aRhs.mTimeout && mTimerSeq == aRhs.mTimerSeq); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     bool operator<(const EntryKey& aRhs) const { | ||||||
|  |       if (mTimeout == aRhs.mTimeout) { | ||||||
|  |         return mTimerSeq < aRhs.mTimerSeq; | ||||||
|  |       } | ||||||
|  |       return mTimeout < aRhs.mTimeout; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     TimeStamp mTimeout; | ||||||
|  |     uint64_t mTimerSeq; | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   struct Entry final : EntryKey { | ||||||
|     explicit Entry(nsTimerImpl& aTimerImpl) |     explicit Entry(nsTimerImpl& aTimerImpl) | ||||||
|         : mTimeout(aTimerImpl.mTimeout), |         : EntryKey(aTimerImpl), | ||||||
|           mDelay(aTimerImpl.mDelay), |           mDelay(aTimerImpl.mDelay), | ||||||
|           mTimerSeq(aTimerImpl.mTimerSeq), |  | ||||||
|           mTimerImpl(&aTimerImpl) {} |           mTimerImpl(&aTimerImpl) {} | ||||||
| 
 | 
 | ||||||
|     // No copies to not fiddle with mTimerImpl's ref-count.
 |     // No copies to not fiddle with mTimerImpl's ref-count.
 | ||||||
|  | @ -114,23 +141,12 @@ class TimerThread final : public mozilla::Runnable, public nsIObserver { | ||||||
|     } |     } | ||||||
| #endif | #endif | ||||||
| 
 | 
 | ||||||
|     // These values are simply cached from the timer. Keeping them here is good
 |  | ||||||
|     // for cache usage and allows us to avoid worrying about locking conflicts
 |  | ||||||
|     // with the timer.
 |  | ||||||
|     TimeStamp mTimeout; |  | ||||||
|     TimeDuration mDelay; |     TimeDuration mDelay; | ||||||
|     uint64_t mTimerSeq; |  | ||||||
| 
 |  | ||||||
|     RefPtr<nsTimerImpl> mTimerImpl; |     RefPtr<nsTimerImpl> mTimerImpl; | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   void PostTimerEvent(Entry& aPostMe) MOZ_REQUIRES(mMonitor); |   void PostTimerEvent(Entry& aPostMe) MOZ_REQUIRES(mMonitor); | ||||||
| 
 | 
 | ||||||
|   // Computes and returns the index in mTimers at which a new timer with the
 |  | ||||||
|   // specified timeout should be inserted in order to maintain "sorted" order.
 |  | ||||||
|   size_t ComputeTimerInsertionIndex(const TimeStamp& timeout) const |  | ||||||
|       MOZ_REQUIRES(mMonitor); |  | ||||||
| 
 |  | ||||||
|   // Computes and returns when we should next try to wake up in order to handle
 |   // Computes and returns when we should next try to wake up in order to handle
 | ||||||
|   // the triggering of the timers in mTimers.
 |   // the triggering of the timers in mTimers.
 | ||||||
|   // If mTimers is empty, returns a null TimeStamp. If mTimers is not empty,
 |   // If mTimers is empty, returns a null TimeStamp. If mTimers is not empty,
 | ||||||
|  | @ -160,10 +176,9 @@ class TimerThread final : public mozilla::Runnable, public nsIObserver { | ||||||
|   // clears a few flags before and after.
 |   // clears a few flags before and after.
 | ||||||
|   void Wait(TimeDuration aWaitFor) MOZ_REQUIRES(mMonitor); |   void Wait(TimeDuration aWaitFor) MOZ_REQUIRES(mMonitor); | ||||||
| 
 | 
 | ||||||
|   // mTimers is maintained in a "pseudo-sorted" order wrt the timeouts.
 |   // mTimers is sorted by timeout, followed by a unique sequence number.
 | ||||||
|   // Specifcally, mTimers is sorted according to the timeouts *if you ignore the
 |   // Some entries are for cancelled entries, but remain in sorted order based
 | ||||||
|   // canceled entries* (those whose mTimerImpl is nullptr). Notably this means
 |   // on the timeout and sequence number they were originally created with.
 | ||||||
|   // that you cannot use a binary search on this list.
 |  | ||||||
|   nsTArray<Entry> mTimers MOZ_GUARDED_BY(mMonitor); |   nsTArray<Entry> mTimers MOZ_GUARDED_BY(mMonitor); | ||||||
| 
 | 
 | ||||||
|   // Set only at the start of the thread's Run():
 |   // Set only at the start of the thread's Run():
 | ||||||
|  |  | ||||||
		Loading…
	
		Reference in a new issue
	
	 Jens Stutte
						Jens Stutte