mirror of
https://github.com/mozilla/gecko-dev.git
synced 2025-11-01 00:38:50 +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