Bug 863852 - Part 2 - Handle reentrance in the state machine. r=smaug

This commit is contained in:
Guilherme Gonçalves 2013-04-24 10:40:55 -07:00 коммит произвёл John Schoenick
Родитель 7f1573cedd
Коммит 597f33c342
6 изменённых файлов: 276 добавлений и 110 удалений

Просмотреть файл

@ -38,10 +38,6 @@ static const uint32_t kSPEECH_DETECTION_TIMEOUT_MS = 10000;
// 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()
@ -68,8 +64,7 @@ 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))
{
@ -89,7 +84,21 @@ SpeechRecognition::SpeechRecognition()
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*
@ -125,17 +134,22 @@ SpeechRecognition::ProcessEvent(SpeechEvent* aEvent)
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:
@ -143,112 +157,145 @@ SpeechRecognition::TransitionAndGetNextState(SpeechEvent* aEvent)
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));
@ -256,12 +303,15 @@ SpeechRecognition::TransitionAndGetNextState(SpeechEvent* aEvent)
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;
}
/*
@ -311,101 +361,116 @@ SpeechRecognition::GetRecognitionServiceCID(nsACString& aResultCID)
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;
}
/*
* Since the handler for "end" may call
* start(), we want to fully reset before dispatching
* the event.
*/
SpeechRecognition::FSMState
void
SpeechRecognition::ResetAndEnd()
{
mCurrentState = Reset();
Reset();
DispatchTrustedEvent(NS_LITERAL_STRING("end"));
return mCurrentState;
}
SpeechRecognition::FSMState
void
SpeechRecognition::WaitForAudioData(SpeechEvent* aEvent)
{
SetState(STATE_STARTING);
}
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)
{
mEstimationSamples += ProcessAudioSegment(aEvent->mAudioSegment);
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)
{
ProcessAudioSegment(aEvent->mAudioSegment);
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);
}
return STATE_RECOGNIZING;
if (mCurrentState == STATE_RECOGNIZING) {
// FIXME: StopRecordingAndRecognize should only be called for single
// shot services for continuous we should just inform the service
StopRecordingAndRecognize(aEvent);
}
}
}
SpeechRecognition::FSMState
void
SpeechRecognition::NotifyFinalResult(SpeechEvent* aEvent)
{
ResetAndEnd();
nsCOMPtr<nsIDOMEvent> domEvent;
NS_NewDOMSpeechRecognitionEvent(getter_AddRefs(domEvent), nullptr, nullptr, nullptr);
@ -420,35 +485,37 @@ SpeechRecognition::NotifyFinalResult(SpeechEvent* aEvent)
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
@ -506,7 +573,7 @@ SpeechRecognition::Observe(nsISupports* aSubject, const char* aTopic,
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,
@ -638,7 +705,7 @@ SpeechRecognition::SetServiceURI(const nsAString& aArg, ErrorResult& aRv)
void
SpeechRecognition::Start(ErrorResult& aRv)
{
if (!STATE_EQUALS(STATE_IDLE)) {
if (!mCurrentState == STATE_IDLE) {
aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
return;
}
@ -830,7 +897,8 @@ SpeechRecognition::GetName(FSMState aId)
"STATE_ESTIMATING",
"STATE_WAITING_FOR_SPEECH",
"STATE_RECOGNIZING",
"STATE_WAITING_FOR_RESULT"
"STATE_WAITING_FOR_RESULT",
"STATE_ABORTING"
};
MOZ_ASSERT(aId < STATE_COUNT);

Просмотреть файл

@ -167,9 +167,13 @@ private:
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:
@ -219,19 +223,20 @@ private:
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;
@ -240,7 +245,7 @@ private:
void GetRecognitionServiceCID(nsACString& aResultCID);
FSMState mCurrentState;
bool mProcessingEvent;
nsTArray<nsRefPtr<SpeechEvent> > mPriorityEvents;
Endpointer mEndpointer;
uint32_t mEstimationSamples;

Просмотреть файл

@ -23,6 +23,7 @@ MOCHITEST_FILES := \
test_abort.html \
test_call_start_from_end_handler.html \
test_preference_enable.html \
test_nested_eventloop.html \
hello.ogg \
silence.ogg \
$(NULL)

Просмотреть файл

@ -159,7 +159,9 @@ function performTest(options) {
em.doneFunc = function() {
em.requestTestEnd();
options.doneFunc();
if (options.doneFunc) {
options.doneFunc();
}
}
em.audioSampleFile = DEFAULT_AUDIO_SAMPLE_FILE;

Просмотреть файл

@ -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>

Просмотреть файл

@ -119,6 +119,7 @@
"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",