Quantum/DOM
Goals
The goal of the Quantum DOM project is to eliminate jank from background tabs. One of the main ways we intend to do this is to run each tab in its own cooperatively scheduled thread. If a runnable on a background thread takes too long to run, then we will pause its execution and switch to a different thread. To do this correctly, we need to guarantee that web pages never observe a change in behavior. For example, it would be bad if we paused a runnable R1 and then allowed another runnable R2 from the same page to see that R1 had started but not yet finished.
Concepts
To more precisely specify when one runnable can observe state from another runnable, we need to define some terminology.
First, a TabGroup is the set of tabs that are related by window.opener. In a session with four tabs, where T1 opens T2 and T3 opens T4, the TabGroups are {T1, T2} and {T3, T4}. Once a tab joins a TabGroup, it never leaves it. TabGroups have the property that two tabs from different TabGroups can never observe each other's state. So a runnable from one TabGroup can run while a runnable from a different TabGroup is paused.
A DocGroup is the collection of documents from a given TabGroup that share the same eTLD+1 part of their origins. So if a TabGroup contains tabs with documents {x.a.com, y.a.com, x.b.com, y.b.com}, then these documents will belong to two DocGroups: {x.a.com, y.a.com}, {x.b.com, y.b.com}. DocGroups are essentially a refinement of TabGroups to account for the fact that only same-origin documents can synchronously communicate. (The eTLD+1 part is to account for pages changing their origin by modifying document.domain.) So a runnable from one DocGroup can run while a runnable from a different DocGroup is paused.
A major task for the Quantum DOM project is to classify runnables into DocGroups and TabGroups. Ideally we would assign all runnables a DocGroup. But in some cases the best we can do is to give it a TabGroup.
Implementation
Given a document, you can find its DocGroup via nsIDocument::GetDocGroup. Given an inner or outer window, you can find its TabGroup and DocGroup via nsPIDOMWindow::{TabGroup,GetDocGroup}. These methods should only be called on the main thread.
Dispatching
Both the TabGroup and DocGroup classes have Dispatch methods to dispatch runnables. Runnables dispatched in this way will always run on the main thread. You can call Dispatch from any thread. Both TabGroup and DocGroup are threadsafe refcounted. The Dispatch method requires you to name the runnable and provide a category. For now, these are for debugging purposes, but the category may be used for scheduling purposes later on.
As a convenience, nsIDocument and nsIGlobalObject have a Dispatch method that will dispatch to their DocGroup. The nsIDocument::Dispatch method can be used on any thread (although you must be careful because nsIDocument is not threadsafe refcounted). The nsIGlobalObject::Dispatch method is main thread only.
Example
Suppose you have a runnable that is dispatched to the main thread.
/* virtual */ void nsDocument::PostVisibilityUpdateEvent() { nsCOMPtr<nsIRunnable> event = NewRunnableMethod(this, &nsDocument::UpdateVisibilityState); NS_DispatchToMainThread(event); }
To convert this code, we simply call Dispatch on the document:
/* virtual */ void nsDocument::PostVisibilityUpdateEvent() { nsCOMPtr<nsIRunnable> event = NewRunnableMethod(this, &nsDocument::UpdateVisibilityState); Dispatch("UpdateVisibility", TaskCategory::Other, event.forget()); }
As a more complex example, consider off-thread script parsing. When parsing is done, a NotifyOffThreadScriptLoadCompletedRunnable runnable is posted to the main thread. We can modify this code by saving the DocGroup in the runnable:
class NotifyOffThreadScriptLoadCompletedRunnable : public Runnable { RefPtr<nsScriptLoadRequest> mRequest; RefPtr<nsScriptLoader> mLoader; + RefPtr<DocGroup> mDocGroup; void *mToken; public: NotifyOffThreadScriptLoadCompletedRunnable(nsScriptLoadRequest* aRequest, nsScriptLoader* aLoader) : mRequest(aRequest) , mLoader(aLoader) + , mDocGroup(aLoader->GetDocGroup()) {}
For this to work, we need to instrument the nsScriptLoader with a DocGroup method. That's very easy though:
mozilla::dom::DocGroup* nsScriptLoader::GetDocGroup() const { return mDocument->GetDocGroup(); }
Finally, when dispatching, we use the DocGroup:
static void Dispatch(already_AddRefed<NotifyOffThreadScriptLoadCompletedRunnable>&& aSelf) { RefPtr<NotifyOffThreadScriptLoadCompletedRunnable> self = aSelf; RefPtr<DocGroup> docGroup = self->mDocGroup; docGroup->Dispatch("OffThreadScriptLoader", TaskCategory::Other, self.forget()); }
Event Targets
A lot of existing Gecko code uses an nsIEventTarget to decide where to dispatch runnables. The DocGroup and TabGroup classes expose EventTargetFor(category) methods that return an event target. Using this event target is equivalent to calling Dispatch on the DocGroup or TabGroup. (The one difference is that no name is provided for the runnable.) TabGroup,DocGroup}::EventTargetFor can be called on any thread. As a convenience, you can also use nsIDocument::EventTargetFor (also callable from any thread) or nsIGlobalObject::EventTargetFor (main thread only).
Example
The main-thread XMLHttpRequest class uses several timers that should all be dispatched to the XHR's DocGroup. We can add a SetTimerEventTarget method that dispatches timers to the correct DocGroup:
void XMLHttpRequestMainThread::SetTimerEventTarget(nsITimer* aTimer) { if (nsCOMPtr<nsIGlobalObject> global = GetOwnerGlobal()) { nsCOMPtr<nsIEventTarget> target = global->EventTargetFor(TaskCategory::Other); aTimer->SetTarget(target); } }
When using EventTargetFor, please try to set the name of the runnable as well. For timers, the name of the timer runnable is derived from the name of the nsITimerCallback (via a GetName method). XMLHttpRequestMainThread implements nsITimerCallback, so we just need to add a GetName method:
nsresult XMLHttpRequestMainThread::GetName(nsACString& aName) { aName.AssignLiteral("XMLHttpRequest"); return NS_OK; }
IPC Actors
Many content process runnables are dispatched from IPC. The IPC code allow you to specify an event target for each actor. Any messages received by that actor or its sub-actors will be dispatched to the given event target. You need to specify the event target after the actor is created but before sending the constructor message to the parent process. To do so, call the SetEventTargetForActor on the manager of the new actor. All this must happen only on whichever thread the actor is bound to.
Example
Most networking data comes in via the HttpChannelChild actor. We first create a method that finds the correct event target via the LoadInfo. Note that Dispatcher is the superclass of TabGroup and DocGroup.
void HttpChannelChild::SetEventTarget() { nsCOMPtr<nsILoadInfo> loadInfo; GetLoadInfo(getter_AddRefs(loadInfo)); if (!loadInfo) { return; } nsCOMPtr<nsIDOMDocument> domDoc; loadInfo->GetLoadingDocument(getter_AddRefs(domDoc)); nsCOMPtr<nsIDocument> doc = do_QueryInterface(domDoc); // Dispatcher is the superclass of TabGroup and DocGroup. RefPtr<Dispatcher> dispatcher; if (doc) { dispatcher = doc->GetDocGroup(); } else { // Top-level loads won't have a DocGroup yet. So instead we target them at // the TabGroup, which is the best we can do at this time. uint64_t outerWindowId; if (NS_FAILED(loadInfo->GetOuterWindowID(&outerWindowId))) { return; } RefPtr<nsGlobalWindow> window = nsGlobalWindow::GetOuterWindowWithId(outerWindowId); if (!window) { return; } dispatcher = window->TabGroup(); } if (dispatcher) { nsCOMPtr<nsIEventTarget> target = dispatcher->EventTargetFor(TaskCategory::Network); // gNeckoChild holds the NeckoChild singleton actor. gNeckoChild->SetEventTargetForActor(this, target); } }
We call this method right before sending the constructor message:
nsresult HttpChannelChild::ContinueAsyncOpen() { ... // lots of code to setup the channel ContentChild* cc = static_cast<ContentChild*>(gNeckoChild->Manager()); if (cc->IsShuttingDown()) { return NS_ERROR_FAILURE; SetEventTarget(); // The socket transport in the chrome process now holds a logical ref to us // until OnStopRequest, or we do a redirect, or we hit an IPDL error. AddIPDLReference(); PBrowserOrId browser = cc->GetBrowserOrId(tabChild); if (!gNeckoChild->SendPHttpChannelConstructor(this, browser, IPC::SerializedLoadContext(this), openArgs)) { return NS_ERROR_FAILURE; }
Actors constructed by the parent
If the new actor is created on the parent side, then you must override the GetMessageEventTarget method on ContentChild (or whatever the top-level protocol is). All constructor messages are passed to this method. It can return an event target for the new actor or null if no special event target should be used. Be careful, because this method is called on the Gecko I/O thread!