Skip to content

Commit bbdc696

Browse files
committed
Bug 1940906 - Introduce SpeechRecognitionBackend - Part 1 - Static methods. r=nika,pehrsons
This patch starts filling in the blanks in SpeechRecognitionBackend. This object sits behind the SpeechRecogntion DOM object, and implements some light audio processing, and IPC communication. Audio processing is left for the next patch. Beyond the static methods (`available` and `install`, which call the IPDL methods of the same name), this patch also implements the threading architecture, the IPC-thread lifecycle, and the transient-vs-long-running session management described below. More importantly this patch implement a threading architecture that works for real-time audio, and DOM API, static and instance methods. The constraints are the following: - We're going to send method calls (available, install, start, stop, etc.) to the HWInference process, those come from the main thread (Web Speech API) - We're also going to need to send real-time audio to the same process (with a graduingly lower and lower packet size, to reduce end-to-end latency). This come from a hard-real-time thread (from our MediaTrackGraph) - Our IPC system requires all method calls to be on a single thread - The real-time audio thread can't do any system calls or unbounded operations (can't do IPC, locking, allocations, etc.) - We really don't want to bounce the real-time audio via the main thread for IPC or make it more busy if we can avoid it (it's already a nightmare in a typical media processing web app) - We can't affort scheduling hazards associated with using a shared thread (e.g. the shared background thread), because we want to have low and constant latency - There are both static and instance methods to implement The design is therefore the following: - A dedicated SpeechIPC thread is created lazily and kept as a static member on the class. It is a LazyIdleThread: its serial event target is stable for the process lifetime, so an actor is never handed a thread instance that later goes away underneath it, but its backing OS thread is released once idle. The OS thread is only ever idle while no connection is open (an active session streams audio continuously), so it is never reclaimed mid-session. - IPC only ever runs on this dedicated thread - Static method calls are dispatched to that thread, and the result is dispatched back to the main thread. - In the next patch, processed and packetized audio is dispatched to that thread as well - Whenever a Speech Recognition session is active, or while an available or install call is in flight, the HWInference connection is held open (refcounted) - Otherwise the connection is closed as soon as we can (session stop, result of `available` or `install`), so the utility process isn't kept alive by an idle connection; the now-idle OS thread is then reclaimed by the LazyIdleThread The logic to manage those transient sessions (vs. long running ones) is encapsulated in a dedicated class. Differential Revision: https://phabricator.services.mozilla.com/D275653
1 parent 3031ea1 commit bbdc696

3 files changed

Lines changed: 361 additions & 9 deletions

File tree

‎build/clang-plugin/ThreadAllows.txt‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ UpdateProcessor
2525
BkgrndClipboard
2626
# Blocking thread calling into the parakeet.cpp library.
2727
Parakeet
28+
# Used by `SpeechRecognitionBackend` to send audio chunks and other commands to
29+
# the HWInference process. Needs to be a dedicated thread because some
30+
# scheduling guarantees are necessary.
31+
SpeechIPC
2832

2933
######
3034
# Thunderbird-only thread names

‎dom/media/webspeech/recognition/SpeechRecognitionBackend.cpp‎

Lines changed: 273 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,24 +6,82 @@
66

77
#include "SpeechRecognitionBackend.h"
88

9+
#include <utility>
10+
911
#include "SpeechRecognition.h"
1012
#include "SpeechTrackListener.h"
1113
#include "mozilla/AbstractThread.h"
1214
#include "mozilla/Assertions.h"
15+
#include "mozilla/ClearOnShutdown.h"
1316
#include "mozilla/dom/AudioStreamTrack.h"
1417
#include "mozilla/dom/ContentChild.h"
1518
#include "mozilla/dom/Promise.h"
19+
#include "mozilla/dom/SpeechRecognitionBinding.h"
20+
#include "mozilla/hwinference/PSpeechRecognition.h"
21+
#include "mozilla/hwinference/SpeechRecognitionChild.h"
22+
#include "mozilla/ipc/MessageChannel.h"
23+
#include "mozilla/ipc/ProtocolUtils.h"
1624

1725
namespace mozilla::dom {
1826

27+
using namespace mozilla::ipc;
28+
29+
StaticAutoPtr<mozilla::EventTargetCapability<nsISerialEventTarget>>
30+
SpeechRecognitionBackend::sIPCCapability;
31+
int32_t SpeechRecognitionBackend::sIPCActorUsers = 0;
32+
1933
static LazyLogModule gSpeechRecognitionBackendLog("SpeechRecognitionBackend");
2034

21-
#define LOG(...) \
22-
MOZ_LOG_FMT(gSpeechRecognitionBackendLog, LogLevel::Debug, __VA_ARGS__)
23-
#define LOGV(...) \
24-
MOZ_LOG_FMT(gSpeechRecognitionBackendLog, LogLevel::Verbose, __VA_ARGS__)
25-
#define LOGE(...) \
26-
MOZ_LOG_FMT(gSpeechRecognitionBackendLog, LogLevel::Error, __VA_ARGS__)
35+
#define LOG(fmt, ...) \
36+
MOZ_LOG_FMT(gSpeechRecognitionBackendLog, mozilla::LogLevel::Debug, fmt, \
37+
##__VA_ARGS__)
38+
#define LOGV(fmt, ...) \
39+
MOZ_LOG_FMT(gSpeechRecognitionBackendLog, mozilla::LogLevel::Verbose, fmt, \
40+
##__VA_ARGS__)
41+
#define LOGE(fmt, ...) \
42+
MOZ_LOG_FMT(gSpeechRecognitionBackendLog, mozilla::LogLevel::Error, fmt, \
43+
##__VA_ARGS__)
44+
45+
// How long the shared IPC thread stays idle before its backing OS thread is
46+
// released. The serial event target itself lives for the process lifetime.
47+
static constexpr uint32_t IPC_THREAD_IDLE_TIMEOUT_MS = 5000;
48+
49+
SpeechRecognitionBackend::SpeechRecognitionIPCActorUserGuard::
50+
~SpeechRecognitionIPCActorUserGuard() {
51+
if (NS_IsMainThread()) {
52+
SpeechRecognitionBackend::ReleaseIPCActorUser();
53+
} else {
54+
NS_DispatchToMainThread(NS_NewRunnableFunction(
55+
"SpeechRecognitionIPCActorUserGuard::Release", [] {
56+
AssertIsOnMainThread();
57+
SpeechRecognitionBackend::ReleaseIPCActorUser();
58+
}));
59+
}
60+
}
61+
62+
/* static */
63+
void SpeechRecognitionBackend::AcquireIPCActorUser() {
64+
AssertIsOnMainThread();
65+
++sIPCActorUsers;
66+
}
67+
68+
/* static */
69+
void SpeechRecognitionBackend::ReleaseIPCActorUser() {
70+
AssertIsOnMainThread();
71+
MOZ_ASSERT(sIPCActorUsers > 0);
72+
if (--sIPCActorUsers || !sIPCCapability) {
73+
return;
74+
}
75+
76+
// Close the HWInference connection once no session needs it, so the utility
77+
// process isn't kept alive by an idle connection. The serial event target is
78+
// kept (its backing OS thread is released on idle by the LazyIdleThread);
79+
// the next EnsureIPC() reopens the connection on that same target.
80+
nsCOMPtr<nsIRunnable> close = NS_NewRunnableFunction(
81+
"SpeechRecognitionBackend::CloseHWInferenceChildIfAny",
82+
[] { CloseHWInferenceChildIfAny(); });
83+
sIPCCapability->Dispatch(close.forget());
84+
}
2785

2886
SpeechRecognitionBackend::SpeechRecognitionBackend(
2987
SpeechRecognition* aParent, uint32_t aGraphRate, const nsString& aLanguage,
@@ -56,14 +114,221 @@ void SpeechRecognitionBackend::DataCallback(TrackTime aTime,
56114

57115
void SpeechRecognitionBackend::NotifyTrackEnded() {}
58116

117+
/* static */
118+
void SpeechRecognitionBackend::EnsureIPCThread() {
119+
AssertIsOnMainThread();
120+
121+
if (!sIPCCapability) {
122+
// IPC actors are bound to the event target they were opened on, so the
123+
// target has to outlive them: a LazyIdleThread keeps a single one for the
124+
// process lifetime, and only releases its backing OS thread when idle.
125+
RefPtr<LazyIdleThread> thread =
126+
new LazyIdleThread(IPC_THREAD_IDLE_TIMEOUT_MS, "SpeechIPC");
127+
sIPCCapability = new EventTargetCapability<nsISerialEventTarget>(thread);
128+
LOG("Created shared IPC thread for speech recognition");
129+
ClearOnShutdown(&sIPCCapability);
130+
}
131+
}
132+
133+
/* static */
134+
void SpeechRecognitionBackend::AssertOnIPCThread() {
135+
sIPCCapability->AssertOnCurrentThread();
136+
}
137+
138+
/* static */
139+
template <typename SendFunc>
140+
auto SpeechRecognitionBackend::RunWithTransientSession(SendFunc&& aSendFunc) {
141+
AssertIsOnMainThread();
142+
143+
using SendPromise = typename decltype(aSendFunc(
144+
std::declval<hwinference::SpeechRecognitionChild*>()))::element_type;
145+
using ResolveValueType = typename SendPromise::ResolveValueType;
146+
using OperationPromise = MozPromise<ResolveValueType, nsresult, true>;
147+
148+
MozPromiseHolder<OperationPromise> holder;
149+
RefPtr<OperationPromise> operation = holder.Ensure(__func__);
150+
CreateSession([holder = std::move(holder),
151+
aSendFunc = std::forward<SendFunc>(aSendFunc)](
152+
hwinference::SpeechRecognitionChild* aChild) mutable {
153+
AssertOnIPCThread();
154+
if (!aChild) {
155+
holder.Reject(NS_ERROR_FAILURE, __func__);
156+
return;
157+
}
158+
RefPtr child = aChild;
159+
aSendFunc(aChild)->Then(
160+
GetCurrentSerialEventTarget(), __func__,
161+
[holder = std::move(holder),
162+
child](typename SendPromise::ResolveOrRejectValue&& aValue) mutable {
163+
AssertOnIPCThread();
164+
child->Close();
165+
if (aValue.IsReject()) {
166+
holder.Reject(NS_ERROR_FAILURE, __func__);
167+
return;
168+
}
169+
holder.Resolve(std::move(aValue.ResolveValue()), __func__);
170+
});
171+
});
172+
return operation;
173+
}
174+
175+
/* static */
59176
already_AddRefed<Promise> SpeechRecognitionBackend::Available(
60177
nsIGlobalObject* aGlobal, const nsTArray<nsCString>& aLanguages) {
61-
return nullptr;
178+
AssertIsOnMainThread();
179+
180+
if (!aGlobal) {
181+
return nullptr;
182+
}
183+
184+
ErrorResult rv;
185+
RefPtr<Promise> promise = Promise::Create(aGlobal, rv);
186+
if (rv.Failed()) {
187+
return nullptr;
188+
}
189+
190+
nsTArray<nsCString> languages = aLanguages.Clone();
191+
if (languages.IsEmpty()) {
192+
languages.AppendElement("en-US"_ns);
193+
}
194+
195+
LOG("SpeechRecognitionBackend::Available - Starting availability check for "
196+
"{} languages",
197+
languages.Length());
198+
199+
if (MOZ_LOG_TEST(gSpeechRecognitionBackendLog, LogLevel::Debug)) {
200+
for (const auto& lang : languages) {
201+
LOG("SpeechRecognitionBackend::Available - Language requested: {}",
202+
lang.get());
203+
}
204+
}
205+
206+
using IsModelInstalledPromise =
207+
hwinference::PSpeechRecognitionChild::IsModelInstalledPromise;
208+
using AvailabilityPromise = MozPromise<AvailabilityStatus, nsresult, true>;
209+
// RunWithTransientSession turns any rejection into NS_ERROR_FAILURE, so the
210+
// IPC reject reason is forwarded verbatim rather than converted at each step.
211+
using SendAvailabilityPromise =
212+
MozPromise<AvailabilityStatus, ResponseRejectReason, true>;
213+
RunWithTransientSession(
214+
[languages = std::move(languages)](
215+
hwinference::SpeechRecognitionChild* aChild) mutable
216+
-> RefPtr<SendAvailabilityPromise> {
217+
RefPtr<hwinference::SpeechRecognitionChild> child = aChild;
218+
return IsModelInstalledNative(child, languages)
219+
->Then(GetCurrentSerialEventTarget(), __func__,
220+
[child, languages = std::move(languages)](
221+
IsModelInstalledPromise::ResolveOrRejectValue&& aValue)
222+
-> RefPtr<SendAvailabilityPromise> {
223+
if (aValue.IsReject()) {
224+
return SendAvailabilityPromise::CreateAndReject(
225+
aValue.RejectValue(), __func__);
226+
}
227+
if (aValue.ResolveValue()) {
228+
return SendAvailabilityPromise::CreateAndResolve(
229+
AvailabilityStatus::Available, __func__);
230+
}
231+
return child->SendIsModelAvailable(languages)
232+
->Map(GetCurrentSerialEventTarget(), __func__,
233+
[](bool aAvailable) {
234+
return aAvailable
235+
? AvailabilityStatus::Downloadable
236+
: AvailabilityStatus::Unavailable;
237+
});
238+
});
239+
})
240+
->Then(GetMainThreadSerialEventTarget(), __func__,
241+
[promise](AvailabilityPromise::ResolveOrRejectValue&& aValue) {
242+
promise->MaybeResolve(aValue.IsResolve()
243+
? aValue.ResolveValue()
244+
: AvailabilityStatus::Unavailable);
245+
});
246+
247+
return promise.forget();
248+
}
249+
250+
/* static */
251+
RefPtr<hwinference::PSpeechRecognitionChild::IsModelInstalledPromise>
252+
SpeechRecognitionBackend::IsModelInstalledNative(
253+
hwinference::SpeechRecognitionChild* aChild,
254+
const nsTArray<nsCString>& aLanguages) {
255+
AssertOnIPCThread();
256+
257+
LOG("SpeechRecognitionBackend::IsModelInstalledNative - Starting installed "
258+
"check for {} languages",
259+
aLanguages.Length());
260+
261+
return aChild->SendIsModelInstalled(aLanguages);
262+
}
263+
264+
/* static */
265+
void SpeechRecognitionBackend::CreateSession(
266+
MoveOnlyFunction<void(hwinference::SpeechRecognitionChild*)> aCallback) {
267+
AssertIsOnMainThread();
268+
269+
RefPtr<SpeechRecognitionIPCActorUserGuard> guard =
270+
MakeRefPtr<SpeechRecognitionIPCActorUserGuard>();
271+
EnsureIPCThread();
272+
273+
Endpoint<hwinference::PSpeechRecognitionParent> parentEndpoint;
274+
Endpoint<hwinference::PSpeechRecognitionChild> childEndpoint;
275+
MOZ_ALWAYS_SUCCEEDS(hwinference::PSpeechRecognition::CreateEndpoints(
276+
&parentEndpoint, &childEndpoint));
277+
ContentChild::GetSingleton()->SendCreateSpeechRecognition(
278+
std::move(parentEndpoint));
279+
280+
sIPCCapability->Dispatch(NS_NewRunnableFunction(
281+
"SpeechRecognitionBackend::CreateSession",
282+
[guard = std::move(guard), endpoint = std::move(childEndpoint),
283+
callback = std::move(aCallback)]() mutable {
284+
AssertOnIPCThread();
285+
RefPtr child = new hwinference::SpeechRecognitionChild(guard.forget());
286+
if (!endpoint.Bind(child)) {
287+
callback(nullptr);
288+
return;
289+
}
290+
callback(child);
291+
}));
62292
}
63293

294+
/* static */
64295
already_AddRefed<Promise> SpeechRecognitionBackend::Install(
65296
nsIGlobalObject* aGlobal, const nsTArray<nsCString>& aLanguages) {
66-
return nullptr;
297+
AssertIsOnMainThread();
298+
299+
if (!aGlobal) {
300+
return nullptr;
301+
}
302+
303+
ErrorResult rv;
304+
RefPtr<Promise> promise = Promise::Create(aGlobal, rv);
305+
if (rv.Failed()) {
306+
return nullptr;
307+
}
308+
309+
if (aLanguages.IsEmpty()) {
310+
promise->MaybeResolve(false);
311+
return promise.forget();
312+
}
313+
314+
LOG("SpeechRecognitionBackend::Install - Starting install for {} languages",
315+
aLanguages.Length());
316+
317+
using InstallPromise = MozPromise<bool, nsresult, true>;
318+
RunWithTransientSession(
319+
[languages = aLanguages.Clone()](
320+
hwinference::SpeechRecognitionChild* aChild) mutable {
321+
return aChild->SendInstallModels(std::move(languages));
322+
})
323+
->Then(GetMainThreadSerialEventTarget(), __func__,
324+
[promise](InstallPromise::ResolveOrRejectValue&& aValue) {
325+
bool success = aValue.IsResolve() && aValue.ResolveValue();
326+
LOG("SpeechRecognitionBackend::Install - Install completed: {}",
327+
success ? "success" : "failed");
328+
promise->MaybeResolve(success);
329+
});
330+
331+
return promise.forget();
67332
}
68333

69334
} // namespace mozilla::dom

0 commit comments

Comments
 (0)