Bug 1059572 - Part 0: Fuzz test for timers. r=nfroyd
--- a/xpcom/tests/TestTimers.cpp
+++ b/xpcom/tests/TestTimers.cpp
@@ -13,16 +13,21 @@
#include "nsServiceManagerUtils.h"
#include "nsThreadUtils.h"
#include "prinrval.h"
#include "prmon.h"
#include "prthread.h"
#include "mozilla/Attributes.h"
#include "mozilla/ReentrantMonitor.h"
+
+#include <list>
+#include <vector>
+#include <stdio.h>
+
using namespace mozilla;
typedef nsresult(*TestFuncPtr)();
class AutoTestThread
{
public:
AutoTestThread() {
@@ -163,24 +168,324 @@ TestTimerWithStoppedTarget()
testThread->Shutdown();
PR_Sleep(400);
return NS_OK;
}
+#define FUZZ_MAX_TIMEOUT 9
+class FuzzTestThreadState final : public nsITimerCallback {
+ public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+
+ explicit FuzzTestThreadState(nsIThread* thread) :
+ mThread(thread),
+ mStopped(false)
+ {}
+
+ class StartRunnable final : public nsRunnable {
+ public:
+ explicit StartRunnable(FuzzTestThreadState* threadState) :
+ mThreadState(threadState)
+ {}
+
+ NS_IMETHOD Run() override
+ {
+ mThreadState->ScheduleOrCancelTimers();
+ return NS_OK;
+ }
+
+ private:
+ nsRefPtr<FuzzTestThreadState> mThreadState;
+ };
+
+ void Start()
+ {
+ nsCOMPtr<nsIRunnable> runnable = new StartRunnable(this);
+ nsresult rv = mThread->Dispatch(runnable, NS_DISPATCH_NORMAL);
+ if (NS_FAILED(rv)) {
+ MOZ_ASSERT(false, "Failed to dispatch StartRunnable.");
+ }
+ }
+
+ void Stop()
+ {
+ mStopped = true;
+ }
+
+ NS_IMETHOD Notify(nsITimer* aTimer) override
+ {
+ bool onCorrectThread;
+ nsresult rv = mThread->IsOnCurrentThread(&onCorrectThread);
+ MOZ_ASSERT(NS_SUCCEEDED(rv), "Failed to perform thread check.");
+ MOZ_ASSERT(onCorrectThread, "Notify invoked on wrong thread.");
+
+ uint32_t delay;
+ rv = aTimer->GetDelay(&delay);
+ if (NS_FAILED(rv)) {
+ MOZ_ASSERT(false, "GetDelay failed.");
+ return rv;
+ }
+
+ if (delay > FUZZ_MAX_TIMEOUT) {
+ MOZ_ASSERT(false, "Delay was an invalid value for this test.");
+ return NS_ERROR_FAILURE;
+ }
+
+ uint32_t type;
+ rv = aTimer->GetType(&type);
+ MOZ_ASSERT(NS_SUCCEEDED(rv), "Failed to get timer type.");
+ MOZ_ASSERT(type <= nsITimer::TYPE_REPEATING_PRECISE_CAN_SKIP);
+
+ if (type == nsITimer::TYPE_ONE_SHOT) {
+ if (mOneShotTimersByDelay[delay].empty()) {
+ MOZ_ASSERT(false, "Unexpected one-shot timer.");
+ return NS_ERROR_FAILURE;
+ }
+
+ if (mOneShotTimersByDelay[delay].front().get() != aTimer) {
+ MOZ_ASSERT(false,
+ "One-shot timers for a given duration have been reordered.");
+ return NS_ERROR_FAILURE;
+ }
+
+ mOneShotTimersByDelay[delay].pop_front();
+ --mTimersOutstanding;
+ } else if (mStopped) {
+ CancelRepeatingTimer(aTimer);
+ }
+
+ ScheduleOrCancelTimers();
+ RescheduleSomeTimers();
+ return NS_OK;
+ }
+
+ bool HasTimersOutstanding() const
+ {
+ return !!mTimersOutstanding;
+ }
+
+ private:
+ ~FuzzTestThreadState()
+ {
+ for (size_t i = 0; i <= FUZZ_MAX_TIMEOUT; ++i) {
+ MOZ_ASSERT(mOneShotTimersByDelay[i].empty(),
+ "Timers remain at end of test.");
+ }
+ }
+
+ uint32_t GetRandomType() const
+ {
+ return rand() % (nsITimer::TYPE_REPEATING_PRECISE_CAN_SKIP + 1);
+ }
+
+ size_t CountOneShotTimers() const
+ {
+ size_t count = 0;
+ for (size_t i = 0; i <= FUZZ_MAX_TIMEOUT; ++i) {
+ count += mOneShotTimersByDelay[i].size();
+ }
+ return count;
+ }
+
+ void ScheduleOrCancelTimers()
+ {
+ if (mStopped) {
+ return;
+ }
+
+ const size_t numTimersDesired = (rand() % 100) + 100;
+ MOZ_ASSERT(numTimersDesired >= 100);
+ MOZ_ASSERT(numTimersDesired < 200);
+ int adjustment = numTimersDesired - mTimersOutstanding;
+
+ while (adjustment > 0) {
+ CreateRandomTimer();
+ --adjustment;
+ }
+
+ while (adjustment < 0) {
+ CancelRandomTimer();
+ ++adjustment;
+ }
+
+ MOZ_ASSERT(numTimersDesired == mTimersOutstanding);
+ }
+
+ void RescheduleSomeTimers()
+ {
+ if (mStopped) {
+ return;
+ }
+
+ static const size_t kNumRescheduled = 40;
+
+ // Reschedule some timers with a Cancel first.
+ for (size_t i = 0; i < kNumRescheduled; ++i) {
+ InitRandomTimer(CancelRandomTimer().get());
+ }
+ // Reschedule some timers without a Cancel first.
+ for (size_t i = 0; i < kNumRescheduled; ++i) {
+ InitRandomTimer(RemoveRandomTimer().get());
+ }
+ }
+
+ void CreateRandomTimer()
+ {
+ nsresult rv;
+ nsCOMPtr<nsITimer> timer = do_CreateInstance(NS_TIMER_CONTRACTID, &rv);
+ if (NS_FAILED(rv)) {
+ MOZ_ASSERT(false, "Failed to create timer.");
+ return;
+ }
+
+ rv = timer->SetTarget(static_cast<nsIEventTarget*>(mThread.get()));
+ if (NS_FAILED(rv)) {
+ MOZ_ASSERT(false, "Failed to set target.");
+ return;
+ }
+
+ InitRandomTimer(timer.get());
+ }
+
+ nsCOMPtr<nsITimer> CancelRandomTimer()
+ {
+ nsCOMPtr<nsITimer> timer(RemoveRandomTimer());
+ timer->Cancel();
+ return timer;
+ }
+
+ nsCOMPtr<nsITimer> RemoveRandomTimer()
+ {
+ MOZ_ASSERT(mTimersOutstanding);
+
+ if ((GetRandomType() == nsITimer::TYPE_ONE_SHOT && CountOneShotTimers())
+ || mRepeatingTimers.empty()) {
+ uint32_t delayToRemove = rand() % (FUZZ_MAX_TIMEOUT + 1);
+ while (mOneShotTimersByDelay[delayToRemove].empty()) {
+ // ++delayToRemove mod FUZZ_MAX_TIMEOUT + 1
+ delayToRemove = (delayToRemove + 1) % (FUZZ_MAX_TIMEOUT + 1);
+ }
+
+ uint32_t indexToRemove =
+ rand() % mOneShotTimersByDelay[delayToRemove].size();
+
+ for (auto it = mOneShotTimersByDelay[delayToRemove].begin();
+ it != mOneShotTimersByDelay[delayToRemove].end();
+ ++it) {
+ if (indexToRemove) {
+ --indexToRemove;
+ continue;
+ }
+
+ nsCOMPtr<nsITimer> removed = *it;
+ mOneShotTimersByDelay[delayToRemove].erase(it);
+ --mTimersOutstanding;
+ return removed;
+ }
+ } else {
+ size_t indexToRemove = rand() % mRepeatingTimers.size();
+ nsCOMPtr<nsITimer> removed(mRepeatingTimers[indexToRemove]);
+ mRepeatingTimers.erase(mRepeatingTimers.begin() + indexToRemove);
+ --mTimersOutstanding;
+ return removed;
+ }
+
+ MOZ_ASSERT_UNREACHABLE("Unable to remove a timer");
+ return nullptr;
+ }
+
+ void InitRandomTimer(nsITimer* aTimer)
+ {
+ // Between 0 and FUZZ_MAX_TIMEOUT
+ uint32_t delay = rand() % (FUZZ_MAX_TIMEOUT + 1);
+ uint32_t type = GetRandomType();
+ nsresult rv = aTimer->InitWithCallback(this, delay, type);
+ if (NS_FAILED(rv)) {
+ MOZ_ASSERT(false, "Failed to set timer.");
+ return;
+ }
+
+ if (type == nsITimer::TYPE_ONE_SHOT) {
+ mOneShotTimersByDelay[delay].push_back(aTimer);
+ } else {
+ mRepeatingTimers.push_back(aTimer);
+ }
+ ++mTimersOutstanding;
+ }
+
+ void CancelRepeatingTimer(nsITimer* aTimer)
+ {
+ for (auto it = mRepeatingTimers.begin();
+ it != mRepeatingTimers.end();
+ ++it) {
+ if (it->get() == aTimer) {
+ mRepeatingTimers.erase(it);
+ aTimer->Cancel();
+ --mTimersOutstanding;
+ return;
+ }
+ }
+ }
+
+ nsCOMPtr<nsIThread> mThread;
+ // Scheduled timers, indexed by delay between 0-9 ms, in lists
+ // with most recently scheduled last.
+ std::list<nsCOMPtr<nsITimer>> mOneShotTimersByDelay[FUZZ_MAX_TIMEOUT + 1];
+ std::vector<nsCOMPtr<nsITimer>> mRepeatingTimers;
+ Atomic<bool> mStopped;
+ Atomic<size_t> mTimersOutstanding;
+};
+
+NS_IMPL_ISUPPORTS(FuzzTestThreadState, nsITimerCallback)
+
+nsresult
+FuzzTestTimers()
+{
+ static const size_t kNumThreads(10);
+ AutoTestThread threads[kNumThreads];
+ nsRefPtr<FuzzTestThreadState> threadStates[kNumThreads];
+
+ for (size_t i = 0; i < kNumThreads; ++i) {
+ threadStates[i] = new FuzzTestThreadState(&*threads[i]);
+ threadStates[i]->Start();
+ }
+
+ PR_Sleep(PR_MillisecondsToInterval(20000));
+
+ for (size_t i = 0; i < kNumThreads; ++i) {
+ threadStates[i]->Stop();
+ }
+
+ // Wait at most 10 seconds for all outstanding timers to pop
+ PRIntervalTime start = PR_IntervalNow();
+ for (auto& threadState : threadStates) {
+ while (threadState->HasTimersOutstanding()) {
+ if (PR_IntervalToMilliseconds(PR_IntervalNow() - start) > 10000) {
+ MOZ_ASSERT(false, "Timed out waiting for all timers to pop");
+ return NS_ERROR_FAILURE;
+ }
+ PR_Sleep(PR_MillisecondsToInterval(10));
+ }
+ }
+
+ return NS_OK;
+}
+
int main(int argc, char** argv)
{
ScopedXPCOM xpcom("TestTimers");
NS_ENSURE_FALSE(xpcom.failed(), 1);
static TestFuncPtr testsToRun[] = {
TestTargetedTimers,
- TestTimerWithStoppedTarget
+ TestTimerWithStoppedTarget,
+ FuzzTestTimers
};
static uint32_t testCount = sizeof(testsToRun) / sizeof(testsToRun[0]);
for (uint32_t i = 0; i < testCount; i++) {
nsresult rv = testsToRun[i]();
NS_ENSURE_SUCCESS(rv, 1);
}