Bug 863852 - Part 2 - Handle reentrance in the state machine. r=smaug
authorGuilherme Gonçalves <ggp@mozilla.com>
Wed, 24 Apr 2013 10:40:55 -0700
changeset 140727 d73d6d6be727a9921322e41b369da20d045ecfd4
parent 140726 36c5e72589bc8382722807f3da67a22c29f8b21b
child 140728 7190f15f3f686d17d7c7c88115e227da4c51d0c0
push id2579
push userakeybl@mozilla.com
push dateMon, 24 Jun 2013 18:52:47 +0000
treeherdermozilla-beta@b69b7de8a05a [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssmaug
bugs863852
milestone23.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 863852 - Part 2 - Handle reentrance in the state machine. r=smaug
content/media/webspeech/recognition/SpeechRecognition.cpp
content/media/webspeech/recognition/SpeechRecognition.h
content/media/webspeech/recognition/test/Makefile.in
content/media/webspeech/recognition/test/head.js
content/media/webspeech/recognition/test/test_nested_eventloop.html
testing/mochitest/android.json
--- a/content/media/webspeech/recognition/SpeechRecognition.cpp
+++ b/content/media/webspeech/recognition/SpeechRecognition.cpp
@@ -33,20 +33,16 @@ namespace dom {
 static const uint32_t kSAMPLE_RATE = 16000;
 static const uint32_t kSPEECH_DETECTION_TIMEOUT_MS = 10000;
 
 // number of frames corresponding to 300ms of audio to send to endpointer while
 // it's in environment estimation mode
 // kSAMPLE_RATE frames = 1s, kESTIMATION_FRAMES frames = 300ms
 static const uint32_t kESTIMATION_SAMPLES = 300 * kSAMPLE_RATE / 1000;
 
-#define STATE_EQUALS(state) (mCurrentState == state)
-#define STATE_BETWEEN(state1, state2) \
-  (mCurrentState >= (state1) && mCurrentState <= (state2))
-
 #ifdef PR_LOGGING
 PRLogModuleInfo*
 GetSpeechRecognitionLog()
 {
   static PRLogModuleInfo* sLog;
   if (!sLog) {
     sLog = PR_NewLogModule("SpeechRecognition");
   }
@@ -63,18 +59,17 @@ NS_INTERFACE_MAP_BEGIN(SpeechRecognition
 NS_INTERFACE_MAP_END_INHERITING(nsDOMEventTargetHelper)
 
 NS_IMPL_ADDREF_INHERITED(SpeechRecognition, nsDOMEventTargetHelper)
 NS_IMPL_RELEASE_INHERITED(SpeechRecognition, nsDOMEventTargetHelper)
 
 struct SpeechRecognition::TestConfig SpeechRecognition::mTestConfig;
 
 SpeechRecognition::SpeechRecognition()
-  : mProcessingEvent(false)
-  , mEndpointer(kSAMPLE_RATE)
+  : mEndpointer(kSAMPLE_RATE)
   , mAudioSamplesPerChunk(mEndpointer.FrameSize())
   , mSpeechDetectionTimer(do_CreateInstance(NS_TIMER_CONTRACTID))
 {
   SR_LOG("created SpeechRecognition");
   SetIsDOMBinding();
 
   mTestConfig.Init();
   if (mTestConfig.mEnableTests) {
@@ -84,17 +79,31 @@ SpeechRecognition::SpeechRecognition()
   }
 
   mEndpointer.set_speech_input_complete_silence_length(
       Preferences::GetInt(PREFERENCE_ENDPOINTER_SILENCE_LENGTH, 500000));
   mEndpointer.set_long_speech_input_complete_silence_length(
       Preferences::GetInt(PREFERENCE_ENDPOINTER_LONG_SILENCE_LENGTH, 1000000));
   mEndpointer.set_long_speech_length(
       Preferences::GetInt(PREFERENCE_ENDPOINTER_SILENCE_LENGTH, 3 * 1000000));
-  mCurrentState = Reset();
+  Reset();
+}
+
+bool
+SpeechRecognition::StateBetween(FSMState begin, FSMState end)
+{
+  return mCurrentState >= begin && mCurrentState <= end;
+}
+
+void
+SpeechRecognition::SetState(FSMState state)
+{
+  mCurrentState = state;
+  SR_LOG("Transitioned to state %s", GetName(mCurrentState));
+  return;
 }
 
 JSObject*
 SpeechRecognition::WrapObject(JSContext* aCx, JSObject* aScope)
 {
   return SpeechRecognitionBinding::Wrap(aCx, aScope, this);
 }
 
@@ -120,153 +129,194 @@ SpeechRecognition::GetParentObject() con
 
 void
 SpeechRecognition::ProcessEvent(SpeechEvent* aEvent)
 {
   SR_LOG("Processing %s, current state is %s",
          GetName(aEvent),
          GetName(mCurrentState));
 
-  MOZ_ASSERT(!mProcessingEvent, "Event dispatch should be sequential!");
-  mProcessingEvent = true;
+  // Run priority events first
+  for (uint32_t i = 0; i < mPriorityEvents.Length(); ++i) {
+    nsRefPtr<SpeechEvent> event = mPriorityEvents[i];
 
-  mCurrentState = TransitionAndGetNextState(aEvent);
-  SR_LOG("Transitioned to state: %s", GetName(mCurrentState));
+    SR_LOG("Processing priority %s", GetName(event));
+    Transition(event);
+  }
 
-  mProcessingEvent = false;
+  mPriorityEvents.Clear();
+
+  SR_LOG("Processing %s received as argument", GetName(aEvent));
+  Transition(aEvent);
 }
 
-SpeechRecognition::FSMState
-SpeechRecognition::TransitionAndGetNextState(SpeechEvent* aEvent)
+void
+SpeechRecognition::Transition(SpeechEvent* aEvent)
 {
   switch (mCurrentState) {
     case STATE_IDLE:
       switch (aEvent->mType) {
         case EVENT_START:
           // TODO: may want to time out if we wait too long
           // for user to approve
-          return STATE_STARTING;
+          WaitForAudioData(aEvent);
+          break;
         case EVENT_STOP:
         case EVENT_ABORT:
         case EVENT_AUDIO_DATA:
         case EVENT_RECOGNITIONSERVICE_INTERMEDIATE_RESULT:
         case EVENT_RECOGNITIONSERVICE_FINAL_RESULT:
-          return DoNothing(aEvent);
+          DoNothing(aEvent);
+          break;
         case EVENT_AUDIO_ERROR:
         case EVENT_RECOGNITIONSERVICE_ERROR:
-          return AbortError(aEvent);
+          AbortError(aEvent);
+          break;
         case EVENT_COUNT:
           MOZ_NOT_REACHED("Invalid event EVENT_COUNT");
       }
+      break;
     case STATE_STARTING:
       switch (aEvent->mType) {
         case EVENT_AUDIO_DATA:
-          return StartedAudioCapture(aEvent);
+          StartedAudioCapture(aEvent);
+          break;
         case EVENT_AUDIO_ERROR:
         case EVENT_RECOGNITIONSERVICE_ERROR:
-          return AbortError(aEvent);
+          AbortError(aEvent);
+          break;
         case EVENT_ABORT:
-          return AbortSilently(aEvent);
+          AbortSilently(aEvent);
+          break;
         case EVENT_STOP:
-          return Reset();
+          Reset();
+          break;
         case EVENT_RECOGNITIONSERVICE_INTERMEDIATE_RESULT:
         case EVENT_RECOGNITIONSERVICE_FINAL_RESULT:
-          return DoNothing(aEvent);
+          DoNothing(aEvent);
+          break;
         case EVENT_START:
           SR_LOG("STATE_STARTING: Unhandled event %s", GetName(aEvent));
           MOZ_NOT_REACHED("");
         case EVENT_COUNT:
           MOZ_NOT_REACHED("Invalid event EVENT_COUNT");
       }
+      break;
     case STATE_ESTIMATING:
       switch (aEvent->mType) {
         case EVENT_AUDIO_DATA:
-          return WaitForEstimation(aEvent);
+          WaitForEstimation(aEvent);
+          break;
         case EVENT_STOP:
-          return StopRecordingAndRecognize(aEvent);
+          StopRecordingAndRecognize(aEvent);
+          break;
         case EVENT_ABORT:
-          return AbortSilently(aEvent);
+          AbortSilently(aEvent);
+          break;
         case EVENT_RECOGNITIONSERVICE_INTERMEDIATE_RESULT:
         case EVENT_RECOGNITIONSERVICE_FINAL_RESULT:
         case EVENT_RECOGNITIONSERVICE_ERROR:
-          return DoNothing(aEvent);
+          DoNothing(aEvent);
+          break;
         case EVENT_AUDIO_ERROR:
-          return AbortError(aEvent);
+          AbortError(aEvent);
+          break;
         case EVENT_START:
           SR_LOG("STATE_ESTIMATING: Unhandled event %d", aEvent->mType);
           MOZ_NOT_REACHED("");
         case EVENT_COUNT:
           MOZ_NOT_REACHED("Invalid event EVENT_COUNT");
       }
+      break;
     case STATE_WAITING_FOR_SPEECH:
       switch (aEvent->mType) {
         case EVENT_AUDIO_DATA:
-          return DetectSpeech(aEvent);
+          DetectSpeech(aEvent);
+          break;
         case EVENT_STOP:
-          return StopRecordingAndRecognize(aEvent);
+          StopRecordingAndRecognize(aEvent);
+          break;
         case EVENT_ABORT:
-          return AbortSilently(aEvent);
+          AbortSilently(aEvent);
+          break;
         case EVENT_AUDIO_ERROR:
-          return AbortError(aEvent);
+          AbortError(aEvent);
+          break;
         case EVENT_RECOGNITIONSERVICE_INTERMEDIATE_RESULT:
         case EVENT_RECOGNITIONSERVICE_FINAL_RESULT:
         case EVENT_RECOGNITIONSERVICE_ERROR:
-          return DoNothing(aEvent);
+          DoNothing(aEvent);
+          break;
         case EVENT_START:
           SR_LOG("STATE_STARTING: Unhandled event %s", GetName(aEvent));
           MOZ_NOT_REACHED("");
         case EVENT_COUNT:
           MOZ_NOT_REACHED("Invalid event EVENT_COUNT");
       }
+      break;
     case STATE_RECOGNIZING:
       switch (aEvent->mType) {
         case EVENT_AUDIO_DATA:
-          return WaitForSpeechEnd(aEvent);
+          WaitForSpeechEnd(aEvent);
+          break;
         case EVENT_STOP:
-          return StopRecordingAndRecognize(aEvent);
+          StopRecordingAndRecognize(aEvent);
+          break;
         case EVENT_AUDIO_ERROR:
         case EVENT_RECOGNITIONSERVICE_ERROR:
-          return AbortError(aEvent);
+          AbortError(aEvent);
+          break;
         case EVENT_ABORT:
-          return AbortSilently(aEvent);
+          AbortSilently(aEvent);
+          break;
         case EVENT_RECOGNITIONSERVICE_FINAL_RESULT:
         case EVENT_RECOGNITIONSERVICE_INTERMEDIATE_RESULT:
-          return DoNothing(aEvent);
+          DoNothing(aEvent);
+          break;
         case EVENT_START:
           SR_LOG("STATE_RECOGNIZING: Unhandled aEvent %s", GetName(aEvent));
           MOZ_NOT_REACHED("");
         case EVENT_COUNT:
           MOZ_NOT_REACHED("Invalid event EVENT_COUNT");
       }
+      break;
     case STATE_WAITING_FOR_RESULT:
       switch (aEvent->mType) {
         case EVENT_STOP:
-          return DoNothing(aEvent);
+          DoNothing(aEvent);
+          break;
         case EVENT_AUDIO_ERROR:
         case EVENT_RECOGNITIONSERVICE_ERROR:
-          return AbortError(aEvent);
+          AbortError(aEvent);
+          break;
         case EVENT_RECOGNITIONSERVICE_FINAL_RESULT:
-          return NotifyFinalResult(aEvent);
+          NotifyFinalResult(aEvent);
+          break;
         case EVENT_AUDIO_DATA:
-          return DoNothing(aEvent);
+          DoNothing(aEvent);
+          break;
         case EVENT_ABORT:
-          return AbortSilently(aEvent);
+          AbortSilently(aEvent);
+          break;
         case EVENT_START:
         case EVENT_RECOGNITIONSERVICE_INTERMEDIATE_RESULT:
           SR_LOG("STATE_WAITING_FOR_RESULT: Unhandled aEvent %s", GetName(aEvent));
           MOZ_NOT_REACHED("");
         case EVENT_COUNT:
           MOZ_NOT_REACHED("Invalid event EVENT_COUNT");
       }
+      break;
+    case STATE_ABORTING:
+      DoNothing(aEvent);
+      break;
     case STATE_COUNT:
       MOZ_NOT_REACHED("Invalid state STATE_COUNT");
   }
-  SR_LOG("Unhandled state %s", GetName(mCurrentState));
-  MOZ_NOT_REACHED("");
-  return mCurrentState;
+
+  return;
 }
 
 /*
  * Handle a segment of recorded audio data.
  * Returns the number of samples that were processed.
  */
 uint32_t
 SpeechRecognition::ProcessAudioSegment(AudioSegment* aSegment)
@@ -306,154 +356,171 @@ SpeechRecognition::GetRecognitionService
 
   aResultCID =
     NS_LITERAL_CSTRING(NS_SPEECH_RECOGNITION_SERVICE_CONTRACTID_PREFIX) +
     speechRecognitionService;
 
   return;
 }
 
-/****************************
- * FSM Transition functions *
- ****************************/
+/****************************************************************************
+ * FSM Transition functions
+ *
+ * If a transition function may cause a DOM event to be fired,
+ * it may also be re-entered, since the event handler may cause the
+ * event loop to spin and new SpeechEvents to be processed.
+ *
+ * Rules:
+ * 1) These methods should call SetState as soon as possible.
+ * 2) If these methods dispatch DOM events, or call methods that dispatch
+ * DOM events, that should be done as late as possible.
+ * 3) If anything must happen after dispatching a DOM event, make sure
+ * the state is still what the method expected it to be.
+ ****************************************************************************/
 
-SpeechRecognition::FSMState
+void
 SpeechRecognition::Reset()
 {
+  SetState(STATE_IDLE);
   mRecognitionService = nullptr;
   mEstimationSamples = 0;
   mBufferedSamples = 0;
   mSpeechDetectionTimer->Cancel();
+}
 
-  return STATE_IDLE;
+void
+SpeechRecognition::ResetAndEnd()
+{
+  Reset();
+  DispatchTrustedEvent(NS_LITERAL_STRING("end"));
 }
 
-/*
- * Since the handler for "end" may call
- * start(), we want to fully reset before dispatching
- * the event.
- */
-SpeechRecognition::FSMState
-SpeechRecognition::ResetAndEnd()
+void
+SpeechRecognition::WaitForAudioData(SpeechEvent* aEvent)
 {
-  mCurrentState = Reset();
-  DispatchTrustedEvent(NS_LITERAL_STRING("end"));
-  return mCurrentState;
+  SetState(STATE_STARTING);
 }
 
-SpeechRecognition::FSMState
+void
 SpeechRecognition::StartedAudioCapture(SpeechEvent* aEvent)
 {
+  SetState(STATE_ESTIMATING);
+
   mEndpointer.SetEnvironmentEstimationMode();
   mEstimationSamples += ProcessAudioSegment(aEvent->mAudioSegment);
 
-  DispatchTrustedEvent(NS_LITERAL_STRING("start"));
   DispatchTrustedEvent(NS_LITERAL_STRING("audiostart"));
-
-  return STATE_ESTIMATING;
+  if (mCurrentState == STATE_ESTIMATING) {
+    DispatchTrustedEvent(NS_LITERAL_STRING("start"));
+  }
 }
 
-SpeechRecognition::FSMState
+void
 SpeechRecognition::StopRecordingAndRecognize(SpeechEvent* aEvent)
 {
-  StopRecording();
+  SetState(STATE_WAITING_FOR_RESULT);
+
   MOZ_ASSERT(mRecognitionService, "Service deleted before recording done");
   mRecognitionService->SoundEnd();
 
-  return STATE_WAITING_FOR_RESULT;
+  StopRecording();
 }
 
-SpeechRecognition::FSMState
+void
 SpeechRecognition::WaitForEstimation(SpeechEvent* aEvent)
 {
+  SetState(STATE_ESTIMATING);
+
   mEstimationSamples += ProcessAudioSegment(aEvent->mAudioSegment);
-
   if (mEstimationSamples > kESTIMATION_SAMPLES) {
     mEndpointer.SetUserInputMode();
-    return STATE_WAITING_FOR_SPEECH;
+    SetState(STATE_WAITING_FOR_SPEECH);
   }
-
-  return STATE_ESTIMATING;
 }
 
-SpeechRecognition::FSMState
+void
 SpeechRecognition::DetectSpeech(SpeechEvent* aEvent)
 {
+  SetState(STATE_WAITING_FOR_SPEECH);
+
   ProcessAudioSegment(aEvent->mAudioSegment);
-
   if (mEndpointer.DidStartReceivingSpeech()) {
     mSpeechDetectionTimer->Cancel();
+    SetState(STATE_RECOGNIZING);
     DispatchTrustedEvent(NS_LITERAL_STRING("speechstart"));
-    return STATE_RECOGNIZING;
   }
-
-  return STATE_WAITING_FOR_SPEECH;
 }
 
-SpeechRecognition::FSMState
+void
 SpeechRecognition::WaitForSpeechEnd(SpeechEvent* aEvent)
 {
+  SetState(STATE_RECOGNIZING);
+
   ProcessAudioSegment(aEvent->mAudioSegment);
-
   if (mEndpointer.speech_input_complete()) {
-    // FIXME: StopRecordingAndRecognize should only be called for single
-    // shot services for continous we should just inform the service
     DispatchTrustedEvent(NS_LITERAL_STRING("speechend"));
-    return StopRecordingAndRecognize(aEvent);
+
+    if (mCurrentState == STATE_RECOGNIZING) {
+      // FIXME: StopRecordingAndRecognize should only be called for single
+      // shot services for continuous we should just inform the service
+      StopRecordingAndRecognize(aEvent);
+    }
   }
-
-   return STATE_RECOGNIZING;
 }
 
-SpeechRecognition::FSMState
+void
 SpeechRecognition::NotifyFinalResult(SpeechEvent* aEvent)
 {
+  ResetAndEnd();
+
   nsCOMPtr<nsIDOMEvent> domEvent;
   NS_NewDOMSpeechRecognitionEvent(getter_AddRefs(domEvent), nullptr, nullptr, nullptr);
 
   nsCOMPtr<nsIDOMSpeechRecognitionEvent> srEvent = do_QueryInterface(domEvent);
   nsRefPtr<SpeechRecognitionResultList> rlist = aEvent->mRecognitionResultList;
   nsCOMPtr<nsISupports> ilist = do_QueryInterface(rlist);
   srEvent->InitSpeechRecognitionEvent(NS_LITERAL_STRING("result"),
                                       true, false, 0, ilist,
                                       NS_LITERAL_STRING("NOT_IMPLEMENTED"),
                                       NULL);
   domEvent->SetTrusted(true);
 
   bool defaultActionEnabled;
   this->DispatchEvent(domEvent, &defaultActionEnabled);
-  return ResetAndEnd();
 }
 
-SpeechRecognition::FSMState
+void
 SpeechRecognition::DoNothing(SpeechEvent* aEvent)
 {
-  return mCurrentState;
 }
 
-SpeechRecognition::FSMState
+void
 SpeechRecognition::AbortSilently(SpeechEvent* aEvent)
 {
+  bool stopRecording = StateBetween(STATE_ESTIMATING, STATE_RECOGNIZING);
+
+  // prevent reentrancy from DOM events
+  SetState(STATE_ABORTING);
+
   if (mRecognitionService) {
     mRecognitionService->Abort();
   }
 
-  if (STATE_BETWEEN(STATE_ESTIMATING, STATE_RECOGNIZING)) {
+  if (stopRecording) {
     StopRecording();
   }
 
-  return ResetAndEnd();
+  ResetAndEnd();
 }
 
-SpeechRecognition::FSMState
+void
 SpeechRecognition::AbortError(SpeechEvent* aEvent)
 {
-  FSMState nextState = AbortSilently(aEvent);
+  AbortSilently(aEvent);
   NotifyError(aEvent);
-  return nextState;
 }
 
 void
 SpeechRecognition::NotifyError(SpeechEvent* aEvent)
 {
   nsCOMPtr<nsIDOMEvent> domEvent = do_QueryInterface(aEvent->mError);
   domEvent->SetTrusted(true);
 
@@ -501,17 +568,17 @@ SpeechRecognition::StopRecording()
 
 NS_IMETHODIMP
 SpeechRecognition::Observe(nsISupports* aSubject, const char* aTopic,
                            const PRUnichar* aData)
 {
   MOZ_ASSERT(NS_IsMainThread(), "Observer invoked off the main thread");
 
   if (!strcmp(aTopic, NS_TIMER_CALLBACK_TOPIC) &&
-      STATE_BETWEEN(STATE_IDLE, STATE_WAITING_FOR_SPEECH)) {
+      StateBetween(STATE_IDLE, STATE_WAITING_FOR_SPEECH)) {
 
     DispatchError(SpeechRecognition::EVENT_AUDIO_ERROR,
                   nsIDOMSpeechRecognitionError::NO_SPEECH,
                   NS_LITERAL_STRING("No speech detected (timeout)"));
   } else if (!strcmp(aTopic, SPEECH_RECOGNITION_TEST_END_TOPIC)) {
     nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
     obs->RemoveObserver(this, SPEECH_RECOGNITION_TEST_EVENT_REQUEST_TOPIC);
     obs->RemoveObserver(this, SPEECH_RECOGNITION_TEST_END_TOPIC);
@@ -633,17 +700,17 @@ SpeechRecognition::SetServiceURI(const n
 {
   aRv.Throw(NS_ERROR_NOT_IMPLEMENTED);
   return;
 }
 
 void
 SpeechRecognition::Start(ErrorResult& aRv)
 {
-  if (!STATE_EQUALS(STATE_IDLE)) {
+  if (!mCurrentState == STATE_IDLE) {
     aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
     return;
   }
 
   nsAutoCString speechRecognitionServiceCID;
   GetRecognitionServiceCID(speechRecognitionServiceCID);
 
   nsresult rv;
@@ -825,17 +892,18 @@ const char*
 SpeechRecognition::GetName(FSMState aId)
 {
   static const char* names[] = {
     "STATE_IDLE",
     "STATE_STARTING",
     "STATE_ESTIMATING",
     "STATE_WAITING_FOR_SPEECH",
     "STATE_RECOGNIZING",
-    "STATE_WAITING_FOR_RESULT"
+    "STATE_WAITING_FOR_RESULT",
+    "STATE_ABORTING"
   };
 
   MOZ_ASSERT(aId < STATE_COUNT);
   MOZ_ASSERT(ArrayLength(names) == STATE_COUNT);
   return names[aId];
 }
 
 const char*
--- a/content/media/webspeech/recognition/SpeechRecognition.h
+++ b/content/media/webspeech/recognition/SpeechRecognition.h
@@ -162,19 +162,23 @@ public:
 private:
   enum FSMState {
     STATE_IDLE,
     STATE_STARTING,
     STATE_ESTIMATING,
     STATE_WAITING_FOR_SPEECH,
     STATE_RECOGNIZING,
     STATE_WAITING_FOR_RESULT,
+    STATE_ABORTING,
     STATE_COUNT
   };
 
+  void SetState(FSMState state);
+  bool StateBetween(FSMState begin, FSMState end);
+
   class GetUserMediaStreamOptions : public nsIMediaStreamOptions
   {
   public:
     NS_DECL_ISUPPORTS
     NS_DECL_NSIMEDIASTREAMOPTIONS
 
     GetUserMediaStreamOptions() {}
     virtual ~GetUserMediaStreamOptions() {}
@@ -214,38 +218,39 @@ private:
 
   NS_IMETHOD StartRecording(DOMMediaStream* aDOMStream);
   NS_IMETHOD StopRecording();
 
   uint32_t ProcessAudioSegment(AudioSegment* aSegment);
   void NotifyError(SpeechEvent* aEvent);
 
   void ProcessEvent(SpeechEvent* aEvent);
-  FSMState TransitionAndGetNextState(SpeechEvent* aEvent);
+  void Transition(SpeechEvent* aEvent);
 
-  FSMState Reset();
-  FSMState ResetAndEnd();
-  FSMState StartedAudioCapture(SpeechEvent* aEvent);
-  FSMState StopRecordingAndRecognize(SpeechEvent* aEvent);
-  FSMState WaitForEstimation(SpeechEvent* aEvent);
-  FSMState DetectSpeech(SpeechEvent* aEvent);
-  FSMState WaitForSpeechEnd(SpeechEvent* aEvent);
-  FSMState NotifyFinalResult(SpeechEvent* aEvent);
-  FSMState DoNothing(SpeechEvent* aEvent);
-  FSMState AbortSilently(SpeechEvent* aEvent);
-  FSMState AbortError(SpeechEvent* aEvent);
+  void Reset();
+  void ResetAndEnd();
+  void WaitForAudioData(SpeechEvent* aEvent);
+  void StartedAudioCapture(SpeechEvent* aEvent);
+  void StopRecordingAndRecognize(SpeechEvent* aEvent);
+  void WaitForEstimation(SpeechEvent* aEvent);
+  void DetectSpeech(SpeechEvent* aEvent);
+  void WaitForSpeechEnd(SpeechEvent* aEvent);
+  void NotifyFinalResult(SpeechEvent* aEvent);
+  void DoNothing(SpeechEvent* aEvent);
+  void AbortSilently(SpeechEvent* aEvent);
+  void AbortError(SpeechEvent* aEvent);
 
   nsRefPtr<DOMMediaStream> mDOMStream;
   nsRefPtr<SpeechStreamListener> mSpeechListener;
   nsCOMPtr<nsISpeechRecognitionService> mRecognitionService;
 
   void GetRecognitionServiceCID(nsACString& aResultCID);
 
   FSMState mCurrentState;
-  bool mProcessingEvent;
+  nsTArray<nsRefPtr<SpeechEvent> > mPriorityEvents;
 
   Endpointer mEndpointer;
   uint32_t mEstimationSamples;
 
   uint32_t mAudioSamplesPerChunk;
 
   // buffer holds one chunk of mAudioSamplesPerChunk
   // samples before feeding it to mEndpointer
--- a/content/media/webspeech/recognition/test/Makefile.in
+++ b/content/media/webspeech/recognition/test/Makefile.in
@@ -18,13 +18,14 @@ MOCHITEST_FILES := \
   head.js \
   test_success_without_recognition_service.html \
   test_timeout.html \
   test_recognition_service_error.html \
   test_audio_capture_error.html \
   test_abort.html \
   test_call_start_from_end_handler.html \
   test_preference_enable.html \
+  test_nested_eventloop.html \
   hello.ogg \
   silence.ogg \
   $(NULL)
 
 include $(topsrcdir)/config/rules.mk
--- a/content/media/webspeech/recognition/test/head.js
+++ b/content/media/webspeech/recognition/test/head.js
@@ -154,17 +154,19 @@ function performTest(options) {
 
     for (var eventName in options.expectedEvents) {
       var cb = options.expectedEvents[eventName];
       em.expect(eventName, cb);
     }
 
     em.doneFunc = function() {
       em.requestTestEnd();
-      options.doneFunc();
+      if (options.doneFunc) {
+        options.doneFunc();
+      }
     }
 
     em.audioSampleFile = DEFAULT_AUDIO_SAMPLE_FILE;
     if (options.audioSampleFile) {
       em.audioSampleFile = options.audioSampleFile;
     }
 
     for (var i = 0; i < options.eventsToRequest.length; i++) {
new file mode 100644
--- /dev/null
+++ b/content/media/webspeech/recognition/test/test_nested_eventloop.html
@@ -0,0 +1,89 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=650295
+-->
+<head>
+  <meta charset="utf-8">
+  <title>Test for Bug 650295 -- Spin the event loop from inside a callback</title>
+  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+  <script type="application/javascript" src="head.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=650295">Mozilla Bug 650295</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="text/javascript">
+  SimpleTest.waitForExplicitFinish();
+
+  /*
+   * window.showModalDialog() can be used to spin the event loop, causing
+   * queued SpeechEvents (such as those created by calls to start(), stop()
+   * or abort()) to be processed immediately.
+   * When this is done from inside DOM event handlers, it is possible to
+   * cause reentrancy in our C++ code, which we should be able to withstand.
+   */
+
+  // Garbage collecting the windows created in this test can
+  // cause assertions (Bug 600703).
+  if (!navigator.platform.startsWith("Win")) {
+    SimpleTest.expectAssertions(2);
+  }
+
+  function abortAndSpinEventLoop(evt, sr) {
+    sr.abort();
+    window.showModalDialog("javascript:window.close()");
+  }
+
+  function doneFunc() {
+    // Trigger gc now and wait some time to make sure this test gets the blame
+    // for any assertions caused by showModalDialog
+    var count = 0, GC_COUNT = 4;
+
+    function triggerGCOrFinish() {
+      SpecialPowers.gc();
+      count++;
+
+      if (count == GC_COUNT) {
+        SimpleTest.finish();
+      }
+    }
+
+    for (var i = 0; i < GC_COUNT; i++) {
+      setTimeout(triggerGCOrFinish, 0);
+    }
+  }
+
+  /*
+   * We start by performing a normal start, then abort from the audiostart
+   * callback and force the EVENT_ABORT to be processed while still inside
+   * the event handler. This causes the recording to stop, which raises
+   * the audioend and (later on) end events.
+   * Then, we abort (once again spinning the event loop) from the audioend
+   * handler, attempting to cause a re-entry into the abort code. This second
+   * call should be ignored, and we get the end callback and finish.
+   */
+
+  performTest({
+    eventsToRequest: [
+      "EVENT_START",
+      "EVENT_AUDIO_DATA",
+    ],
+    expectedEvents: {
+      "audiostart": abortAndSpinEventLoop,
+      "audioend": abortAndSpinEventLoop,
+      "end": null
+    },
+    doneFunc: doneFunc,
+    prefs: [["media.webspeech.test.fake_fsm_events", true],
+            ["media.webspeech.test.fake_recognition_service", true]]
+  });
+
+</script>
+</pre>
+</body>
+</html>
--- a/testing/mochitest/android.json
+++ b/testing/mochitest/android.json
@@ -114,16 +114,17 @@
  "content/media/test/test_media_selection.html": "",
  "content/media/test/test_playback.html": "",
  "content/media/test/test_seekLies.html": "TIMED_OUT",
  "content/media/test/test_seekable2.html": "",
  "content/media/test/test_too_many_elements.html": "bug 775227",
  "content/media/test/test_wave_data_s16.html": "TIMED_OUT",
  "content/media/test/test_wave_data_u8.html": "TIMED_OUT",
  "content/media/webspeech/synth/ipc/test/test_ipc.html": "bug 857673",
+ "content/media/webspeech/recognition/test/test_nested_eventloop.html": "",
  "content/smil/test/test_smilRepeatTiming.xhtml": "TIMED_OUT",
  "content/smil/test/test_smilExtDoc.xhtml": "",
  "content/xul/content/test/test_bug486990.xul": "TIMED_OUT",
  "docshell/test/navigation/test_bug13871.html": "RANDOM",
  "docshell/test/navigation/test_bug430723.html": "TIMED_OUT",
  "docshell/test/navigation/test_popup-navigates-children.html": "bug 783589",
  "docshell/test/navigation/test_sessionhistory.html": "RANDOM",
  "docshell/test/navigation/test_bug344861.html": "",