Bug 544945, part 1: Detect nested glib event loops in the plugin subprocess. r=karlt
--- a/dom/plugins/PluginModuleChild.cpp
+++ b/dom/plugins/PluginModuleChild.cpp
@@ -69,18 +69,19 @@ PluginModuleChild* gInstance = nsnull;
PluginModuleChild::PluginModuleChild() :
mLibrary(0),
mInitializeFunc(0),
mShutdownFunc(0)
#ifdef OS_WIN
, mGetEntryPointsFunc(0)
+#elif defined(MOZ_WIDGET_GTK2)
+ , mNestedLoopTimerId(0)
#endif
-// ,mNextInstanceId(0)
{
NS_ASSERTION(!gInstance, "Something terribly wrong here!");
memset(&mFunctions, 0, sizeof(mFunctions));
memset(&mSavedData, 0, sizeof(mSavedData));
gInstance = this;
}
PluginModuleChild::~PluginModuleChild()
@@ -224,22 +225,98 @@ wrap_gtk_plug_embedded(GtkPlug* plug) {
// https://bugzilla.gnome.org/show_bug.cgi?id=607061
g_object_ref(socket_window);
}
if (*real_gtk_plug_embedded) {
(*real_gtk_plug_embedded)(plug);
}
}
+
+//
+// The next four constants are knobs that can be tuned. They trade
+// off potential UI lag from delayed event processing with CPU time.
+//
+static const gint kNestedLoopDetectorPriority = G_PRIORITY_HIGH_IDLE;
+// 90ms so that we can hopefully break livelocks before the user
+// notices UI lag (100ms)
+static const guint kNestedLoopDetectorIntervalMs = 90;
+
+static const gint kBrowserEventPriority = G_PRIORITY_HIGH_IDLE;
+static const guint kBrowserEventIntervalMs = 10;
+
+// static
+gboolean
+PluginModuleChild::DetectNestedEventLoop(gpointer data)
+{
+ PluginModuleChild* pmc = static_cast<PluginModuleChild*>(data);
+
+ NS_ABORT_IF_FALSE(0 != pmc->mNestedLoopTimerId,
+ "callback after descheduling");
+ NS_ABORT_IF_FALSE(1 < g_main_depth(),
+ "not canceled before returning to main event loop!");
+
+ PLUGIN_LOG_DEBUG(("Detected nested glib event loop"));
+
+ // just detected a nested loop; start a timer that will
+ // periodically rpc-call back into the browser and process some
+ // events
+ pmc->mNestedLoopTimerId =
+ g_timeout_add_full(kBrowserEventPriority,
+ kBrowserEventIntervalMs,
+ PluginModuleChild::ProcessBrowserEvents,
+ data,
+ NULL);
+ // cancel the nested-loop detection timer
+ return FALSE;
+}
+
+// static
+gboolean
+PluginModuleChild::ProcessBrowserEvents(gpointer data)
+{
+ NS_ABORT_IF_FALSE(1 < g_main_depth(),
+ "not canceled before returning to main event loop!");
+
+ PluginModuleChild* pmc = static_cast<PluginModuleChild*>(data);
+
+ PLUGIN_LOG_DEBUG(("FIXME/bug 544945: rpc-call to browser to process a few events"));
+
+ return TRUE;
+}
+
+void
+PluginModuleChild::EnteredCxxStack()
+{
+ NS_ABORT_IF_FALSE(0 == mNestedLoopTimerId,
+ "previous timer not descheduled");
+
+ mNestedLoopTimerId =
+ g_timeout_add_full(kNestedLoopDetectorPriority,
+ kNestedLoopDetectorIntervalMs,
+ PluginModuleChild::DetectNestedEventLoop,
+ this,
+ NULL);
+}
+
+void
+PluginModuleChild::ExitedCxxStack()
+{
+ NS_ABORT_IF_FALSE(0 < mNestedLoopTimerId,
+ "nested loop timeout not scheduled");
+
+ g_source_remove(mNestedLoopTimerId);
+ mNestedLoopTimerId = 0;
+}
+
#endif
bool
PluginModuleChild::InitGraphics()
{
- // FIXME/cjones: is this the place for this?
#if defined(MOZ_WIDGET_GTK2)
// Work around plugins that don't interact well with GDK
// client-side windows.
PR_SetEnv("GDK_NATIVE_WINDOWS=1");
gtk_init(0, 0);
// GtkPlug is a static class so will leak anyway but this ref makes sure.
--- a/dom/plugins/PluginModuleChild.h
+++ b/dom/plugins/PluginModuleChild.h
@@ -161,32 +161,75 @@ public:
static NPObject* NP_CALLBACK NPN_RetainObject(NPObject* aNPObj);
/**
* The child implementation of NPN_ReleaseObject.
*/
static void NP_CALLBACK NPN_ReleaseObject(NPObject* aNPObj);
private:
bool InitGraphics();
+#if defined(MOZ_WIDGET_GTK2)
+ static gboolean DetectNestedEventLoop(gpointer data);
+ static gboolean ProcessBrowserEvents(gpointer data);
+
+ NS_OVERRIDE
+ virtual void EnteredCxxStack();
+ NS_OVERRIDE
+ virtual void ExitedCxxStack();
+#endif
std::string mPluginFilename;
PRLibrary* mLibrary;
nsCString mUserAgent;
// we get this from the plugin
#ifdef OS_POSIX
NP_PLUGINUNIXINIT mInitializeFunc;
#elif OS_WIN
NP_PLUGININIT mInitializeFunc;
NP_GETENTRYPOINTS mGetEntryPointsFunc;
#endif
+
NP_PLUGINSHUTDOWN mShutdownFunc;
NPPluginFuncs mFunctions;
NPSavedData mSavedData;
+#if defined(MOZ_WIDGET_GTK2)
+ // If a plugin spins a nested glib event loop in response to a
+ // synchronous IPC message from the browser, the loop might break
+ // only after the browser responds to a request sent by the
+ // plugin. This can happen if a plugin uses gtk's synchronous
+ // copy/paste, for example. But because the browser is blocked on
+ // a condvar, it can't respond to the request. This situation
+ // isn't technically a deadlock, but the symptoms are basically
+ // the same from the user's perspective.
+ //
+ // We take two steps to prevent this
+ //
+ // (1) Detect nested event loops spun by the plugin. This is
+ // done by scheduling a glib timer event in the plugin
+ // process whenever the browser might block on the plugin.
+ // If the plugin indeed spins a nested loop, this timer event
+ // will fire "soon" thereafter.
+ //
+ // (2) When a nested loop is detected, deschedule the
+ // nested-loop-detection timer and in its place, schedule
+ // another timer that periodically calls back into the
+ // browser and spins a mini event loop. This mini event loop
+ // processes a handful of pending native events.
+ //
+ // Because only timer (1) or (2) (or neither) may be active at any
+ // point in time, we use the same member variable
+ // |mNestedLoopTimerId| to refer to both.
+ //
+ // When the browser no longer might be blocked on a plugin's IPC
+ // response, we deschedule whichever of (1) or (2) is active.
+ guint mNestedLoopTimerId;
+#endif
+
struct NPObjectData : public nsPtrHashKey<NPObject>
{
NPObjectData(const NPObject* key)
: nsPtrHashKey<NPObject>(key)
, instance(NULL)
, actor(NULL)
{ }