From 243c31d203449a4b0232bc9fe978e6f4c4dd6cb0 Mon Sep 17 00:00:00 2001 From: Patrick Griffis Date: Sat, 17 Oct 2026 14:54:44 -0500 Subject: [PATCH] [GStreamer] Load Flite when speech synthesis is first used instead of linking it https://bugs.webkit.org/show_bug.cgi?id=327957 Reviewed by NOBODY (OOPS!). Linking Flite put its 13 libraries in every WebKit process. One voice that WebKit doesn't even use, cmu_time_awb, has over 31,000 symbol relocations against itself, about half of all the symbol bindings the dynamic linker does when a WebKit process starts. Each of those lookups searches the global scope of 168 libraries. Load the Flite libraries and the four voices WebKit uses with dlopen() the first time speech synthesis needs them, and call Flite through the resolved functions. Without Flite at runtime, speech synthesis now offers no voices instead of crashing. A bare WPEWebProcess goes from 34 ms to 22 ms from exec to exit, and from 168 to 154 libraries. * Source/WebCore/platform/GStreamer.cmake: * Source/WebCore/platform/gstreamer/GUniquePtrFlite.h: * Source/WebCore/platform/gstreamer/WebKitFliteSourceGStreamer.cpp: (fliteSymbol): (fliteLibrary): (WebCore::fliteDeleteVoice): (WebCore::fliteDeleteWave): (webkitFliteSrcCreate): (fliteVoices): (ensureFliteVoicesInitialized): (webKitFliteSrcSetUtterance): (WTF::toArray): Deleted. --- Source/WebCore/platform/GStreamer.cmake | 4 +- .../platform/gstreamer/GUniquePtrFlite.h | 11 +- .../gstreamer/WebKitFliteSourceGStreamer.cpp | 132 +++++++++++++----- 3 files changed, 112 insertions(+), 35 deletions(-) diff --git a/Source/WebCore/platform/GStreamer.cmake b/Source/WebCore/platform/GStreamer.cmake index 82fc83ed14418..ce1c66972cff2 100644 --- a/Source/WebCore/platform/GStreamer.cmake +++ b/Source/WebCore/platform/GStreamer.cmake @@ -179,11 +179,13 @@ if (ENABLE_SPEECH_SYNTHESIS) LibSpiel::LibSpiel ) elseif (USE_FLITE) + # Flite is loaded with dlopen() when speech synthesis is first used, see + # WebKitFliteSourceGStreamer.cpp. list(APPEND WebCore_SYSTEM_INCLUDE_DIRECTORIES ${Flite_INCLUDE_DIRS} ) list(APPEND WebCore_LIBRARIES - ${Flite_LIBRARIES} + ${CMAKE_DL_LIBS} ) endif () endif () diff --git a/Source/WebCore/platform/gstreamer/GUniquePtrFlite.h b/Source/WebCore/platform/gstreamer/GUniquePtrFlite.h index 9ce87cc18f606..ab398ebffb6ce 100644 --- a/Source/WebCore/platform/gstreamer/GUniquePtrFlite.h +++ b/Source/WebCore/platform/gstreamer/GUniquePtrFlite.h @@ -26,10 +26,17 @@ #include #include +namespace WebCore { + +void fliteDeleteVoice(cst_voice*); +void fliteDeleteWave(cst_wave*); + +} // namespace WebCore + namespace WTF { -WTF_DEFINE_GPTR_DELETER(cst_voice, delete_voice) -WTF_DEFINE_GPTR_DELETER(cst_wave, delete_wave) +WTF_DEFINE_GPTR_DELETER(cst_voice, WebCore::fliteDeleteVoice) +WTF_DEFINE_GPTR_DELETER(cst_wave, WebCore::fliteDeleteWave) } // namespace WTF diff --git a/Source/WebCore/platform/gstreamer/WebKitFliteSourceGStreamer.cpp b/Source/WebCore/platform/gstreamer/WebKitFliteSourceGStreamer.cpp index dc25a0570ffcb..f6610e5a38acd 100644 --- a/Source/WebCore/platform/gstreamer/WebKitFliteSourceGStreamer.cpp +++ b/Source/WebCore/platform/gstreamer/WebKitFliteSourceGStreamer.cpp @@ -24,20 +24,88 @@ #include "GStreamerCommon.h" #include "GUniquePtrFlite.h" #include "PlatformSpeechSynthesisVoice.h" +#include #include #include -extern "C" { -/* There is no header for Flite functions below */ -extern void usenglish_init(cst_voice*); -extern cst_lexicon* cmulex_init(void); -extern cst_voice* register_cmu_us_kal(const char*); -extern cst_voice* register_cmu_us_awb(const char*); -extern cst_voice* register_cmu_us_rms(const char*); -extern cst_voice* register_cmu_us_slt(const char*); +// Flite is loaded when speech synthesis is first used rather than linked. Its voice +// libraries carry tens of thousands of symbol relocations, which every WebKit process +// would otherwise resolve at startup. +struct FliteLibrary { + decltype(&flite_init) init; + decltype(&flite_add_lang) addLang; + decltype(&flite_text_to_wave) textToWave; + decltype(&cst_wave_resample) waveResample; + decltype(&delete_voice) deleteVoice; + decltype(&delete_wave) deleteWave; + void (*usenglishInit)(cst_voice*); + cst_lexicon* (*cmulexInit)(); + Vector voiceRegisterFunctions; +}; + +static void* fliteSymbol(const char* library, const char* symbol) +{ + void* handle = dlopen(library, RTLD_NOW | RTLD_LOCAL); + if (!handle) { + g_warning("Speech synthesis is unavailable: %s", dlerror()); + return nullptr; + } + void* address = dlsym(handle, symbol); + if (!address) + g_warning("Speech synthesis is unavailable: %s", dlerror()); + return address; } -using namespace WebCore; +static const FliteLibrary* fliteLibrary() +{ + static std::optional library; + static std::once_flag onceFlag; + std::call_once(onceFlag, [] { + FliteLibrary loaded { + reinterpret_cast(fliteSymbol("libflite.so.1", "flite_init")), + reinterpret_cast(fliteSymbol("libflite.so.1", "flite_add_lang")), + reinterpret_cast(fliteSymbol("libflite.so.1", "flite_text_to_wave")), + reinterpret_cast(fliteSymbol("libflite.so.1", "cst_wave_resample")), + reinterpret_cast(fliteSymbol("libflite.so.1", "delete_voice")), + reinterpret_cast(fliteSymbol("libflite.so.1", "delete_wave")), + reinterpret_cast(fliteSymbol("libflite_usenglish.so.1", "usenglish_init")), + reinterpret_cast(fliteSymbol("libflite_cmulex.so.1", "cmulex_init")), + { } + }; + if (!loaded.init || !loaded.addLang || !loaded.textToWave || !loaded.waveResample || !loaded.deleteVoice || !loaded.deleteWave || !loaded.usenglishInit || !loaded.cmulexInit) + return; + + static constexpr std::array, 4> voices { { + { "libflite_cmu_us_kal.so.1", "register_cmu_us_kal" }, + { "libflite_cmu_us_slt.so.1", "register_cmu_us_slt" }, + { "libflite_cmu_us_rms.so.1", "register_cmu_us_rms" }, + { "libflite_cmu_us_awb.so.1", "register_cmu_us_awb" }, + } }; + for (auto [voiceLibrary, registerFunction] : voices) { + if (auto function = reinterpret_cast(fliteSymbol(voiceLibrary, registerFunction))) + loaded.voiceRegisterFunctions.append(function); + } + if (loaded.voiceRegisterFunctions.isEmpty()) + return; + + library = WTF::move(loaded); + }); + return library ? &*library : nullptr; +} + +namespace WebCore { + +void fliteDeleteVoice(cst_voice* voice) +{ + fliteLibrary()->deleteVoice(voice); +} + +void fliteDeleteWave(cst_wave* wave) +{ + fliteLibrary()->deleteWave(wave); +} + +} // namespace WebCore typedef struct _WebKitFliteSrcClass WebKitFliteSrcClass; typedef struct _WebKitFliteSrcPrivate WebKitFliteSrcPrivate; @@ -86,14 +154,6 @@ static GstStaticPadTemplate fliteSrcTemplate = GST_STATIC_PAD_TEMPLATE("src", WEBKIT_DEFINE_TYPE_WITH_CODE(WebKitFliteSrc, webkit_flite_src, GST_TYPE_BASE_SRC, GST_DEBUG_CATEGORY_INIT(webkit_flite_src_debug, "webkitflitesrc", 0, "flitesrc element")); -// To add more voices, add voice register functions here. -static auto voiceRegisterFunctions = WTF::toArray({ - register_cmu_us_kal, - register_cmu_us_slt, - register_cmu_us_rms, - register_cmu_us_awb, -}); - static void webkitFliteSrcReset(WebKitFliteSrc* src) { WebKitFliteSrcPrivate* priv = src->priv; @@ -157,12 +217,12 @@ static GstFlowReturn webkitFliteSrcCreate(GstBaseSrc* baseSource, guint64 offset if (!members->didLoadUtterance) { members->didLoadUtterance = true; - GUniquePtr wave(flite_text_to_wave(priv->text.utf8().legacyCStringPointer(), priv->currentVoice)); - cst_wave_resample(wave.get(), priv->info.rate); + GUniquePtr wave(fliteLibrary()->textToWave(priv->text.utf8().legacyCStringPointer(), priv->currentVoice)); + fliteLibrary()->waveResample(wave.get(), priv->info.rate); gsize bufferSize = priv->info.channels * sizeof(gint16) * wave->num_samples; GRefPtr buf = adoptGRef(gst_buffer_new_allocate(nullptr, bufferSize, nullptr)); - GstMappedBuffer map(buf, GST_MAP_WRITE); + WebCore::GstMappedBuffer map(buf, GST_MAP_WRITE); gint16* data = reinterpret_cast(map.data()); auto dataSpan = unsafeMakeSpan(data, bufferSize / sizeof(gint16)); zeroSpan(dataSpan); @@ -219,7 +279,10 @@ static Vector>& fliteVoices() static Vector> voices; static std::once_flag onceFlag; std::call_once(onceFlag, [] { - for (auto& function : voiceRegisterFunctions) { + auto* library = fliteLibrary(); + if (!library) + return; + for (auto& function : library->voiceRegisterFunctions) { GUniquePtr voice(function(nullptr)); voices.append(WTF::move(voice)); } @@ -240,35 +303,40 @@ static cst_voice* fliteVoice(const String& name) return nullptr; } -Vector>& ensureFliteVoicesInitialized() +Vector>& ensureFliteVoicesInitialized() { - static Vector> voiceList; + static Vector> voiceList; static std::once_flag onceFlag; std::call_once(onceFlag, [] { - flite_init(); + auto* library = fliteLibrary(); + if (!library) + return; - flite_add_lang("eng", usenglish_init, cmulex_init); - flite_add_lang("usenglish", usenglish_init, cmulex_init); + library->init(); + + library->addLang("eng", library->usenglishInit, library->cmulexInit); + library->addLang("usenglish", library->usenglishInit, library->cmulexInit); for (auto& voice : fliteVoices()) - voiceList.append(PlatformSpeechSynthesisVoice::create(""_s, String::fromUTF8(voice->name), "en-US"_s, false, false)); - voiceList[0]->setIsDefault(true); + voiceList.append(WebCore::PlatformSpeechSynthesisVoice::create(""_s, String::fromUTF8(voice->name), "en-US"_s, false, false)); + if (!voiceList.isEmpty()) + voiceList[0]->setIsDefault(true); }); return voiceList; } -void webKitFliteSrcSetUtterance(WebKitFliteSrc* src, const PlatformSpeechSynthesisVoice* platformSpeechSynthesisVoice, const String& text) +void webKitFliteSrcSetUtterance(WebKitFliteSrc* src, const WebCore::PlatformSpeechSynthesisVoice* platformSpeechSynthesisVoice, const String& text) { WebKitFliteSrcPrivate* priv = src->priv; - ASSERT(!fliteVoices().isEmpty()); - cst_voice* voice = nullptr; if (platformSpeechSynthesisVoice && !platformSpeechSynthesisVoice->name().isEmpty()) voice = fliteVoice(platformSpeechSynthesisVoice->name()); // We use the first registered voice as default, where no voice is specified. - priv->currentVoice = voice ? voice : fliteVoices()[0].get(); + if (!voice && !fliteVoices().isEmpty()) + voice = fliteVoices()[0].get(); + priv->currentVoice = voice; priv->text = text; }