Quantum/DOM: Difference between revisions

From MozillaWiki
Jump to navigation Jump to search
(small changes for language)
(add SystemGroup explanation)
Line 9: Line 9:
To more precisely specify when one runnable can observe state from another runnable, we need to define some terminology.
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.
First, a <b>TabGroup</b> 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 <b>DocGroup</b> 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.


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


A major task for the Quantum DOM project is to label runnables with DocGroups or TabGroups. Ideally we would assign all runnables a DocGroup. But in some cases the best we can do is to give it a TabGroup.
Runnables that do not affect web content can be labeled with the <b>SystemGroup</b>. Only use the system group if you know that the runnable will not run any content JS code or do any content DOM or layout access.
 
A major task for the Quantum DOM project is to label runnables with DocGroups, TabGroups, or the SystemGroup. Using a DocGroup should be preferred over TabGroup since TabGroup is less specific. But for some runnables the best we can do is to give it a TabGroup.


= Labeling =
= Labeling =

Revision as of 23:16, 26 January 2017

Goals

The goal of the Quantum DOM project is to eliminate jank caused by 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.

One of the biggest pieces of the project is to "label" runnables with the page that they're operating on. This page describes how to label runnables.

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.

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.

Runnables that do not affect web content can be labeled with the SystemGroup. Only use the system group if you know that the runnable will not run any content JS code or do any content DOM or layout access.

A major task for the Quantum DOM project is to label runnables with DocGroups, TabGroups, or the SystemGroup. Using a DocGroup should be preferred over TabGroup since TabGroup is less specific. But for some runnables the best we can do is to give it a TabGroup.

Labeling

Based on how it is dispatched, there are multiple ways to label a runnable. The simplest way is to provide the DocGroup or TabGroup when dispatching the runnable. You can help by taking one of the unowned bugs to label a runnable in the following list:

No results.

0 Total; 0 Open (0%); 0 Resolved (0%); 0 Verified (0%);


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 "task 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. To convert this code, we simply call Dispatch on the document. Here is a diff showing the changes:

 /* virtual */ void
 nsDocument::PostVisibilityUpdateEvent()
 {
   nsCOMPtr<nsIRunnable> event =
     NewRunnableMethod(this, &nsDocument::UpdateVisibilityState);
-  NS_DispatchToMainThread(event);
+  Dispatch("UpdateVisibility", TaskCategory::Other, event.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).

Runnable names

One disadvantage of using EventTargetFor is that any runnables dispatched this way are not given a name. However, there are other options for assigning a name to runnables. To have a name, a runnable needs to implement the GetName method of the nsINamed interface. mozilla::Runnable already does this. You can use the SetName method on an existing mozilla::Runnable to change its name.

Usually, though, you'll be using EventTargetFor in cases where you don't have direct access to the runnable. Typically you'll be giving the event target to a sub-system that will dispatch multiple runnables. Timers, the IPC code, and workers are examples of this. In these cases it's best to modify the sub-system to set pass down an appropriate name for the runnable. The IPC code, for example, can set the runnable name to the name of the message being dispatched.

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 timer callback (if it implements nsINamed). XMLHttpRequestMainThread is itself the timer callback, so we just need to add a GetName method:

nsresult
XMLHttpRequestMainThread::GetName(nsACString& aName)
{
  aName.AssignLiteral("XMLHttpRequest");
  return NS_OK;
}

Script loader example

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 to save an event target while still on the main thread, storing it in the runnable, and then dispatching to that event target off the main thread:

 class NotifyOffThreadScriptLoadCompletedRunnable : public Runnable
 {
   RefPtr<nsScriptLoadRequest> mRequest;
   RefPtr<nsScriptLoader> mLoader;
+  nsCOMPtr<nsIEventTarget> mEventTarget;
   void *mToken;

 public:
   NotifyOffThreadScriptLoadCompletedRunnable(nsScriptLoadRequest* aRequest,
                                              nsScriptLoader* aLoader)
     : mRequest(aRequest)
     , mLoader(aLoader)
+    , mEventTarget(aLoader->GetEventTarget())
   { MOZ_ASSERT(NS_IsMainThread(); }
  ...
 }

For this to work, we need to instrument the nsScriptLoader with an EventTarget method. That's very easy though:

nsIEventTarget*
nsScriptLoader::GetEventTarget() const
{
  return mDocument->EventTargetFor(TaskCategory::Other);
}

Finally, when dispatching, we use the event target. Note that we need to set the runnable name manually:

static void Dispatch(already_AddRefed<NotifyOffThreadScriptLoadCompletedRunnable>&& aSelf) {
  RefPtr<NotifyOffThreadScriptLoadCompletedRunnable> self = aSelf;
  nsCOMPtr<nsIEventTarget> target = self->mEventTarget;
  self->SetName("NotifyOffThreadScriptLoadCompletedRunnable");
  target->Dispatch(self.forget(), DISPATCH_NORMAL);
}

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.

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, normally, it inherits its event target from its manager. If the manager has no event target, you must override the GetConstructedEventTarget 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!

AbstractThread::MainThread

AbstractThread::MainThread() is a singleton of the AbstractThread wrapper class for the main thread and is widely used with MozPromise and its Promise-chain. If you use AbstractThread::MainThread() in your code and you have access to a document or a window, you can replace it with AbstractMainThreadFor provided the document or window object (or by TabGroup or DocGroup directly).

Example

  already_AddRefed<Promise>
  WebAuthentication::MakeCredential(JSContext* aCx, const Account& aAccount,
                    const Sequence<ScopedCredentialParameters>& aCryptoParameters,
                    const ArrayBufferViewOrArrayBuffer& aChallenge,
                    const ScopedCredentialOptions& aOptions)
  {
    MOZ_ASSERT(mParent);
    nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(GetParentObject());
    if (!global) {
      return nullptr;
    }
    ... // lots of code to initiate the request

    requestMonitor->CompleteTask();

-   monitorPromise->Then(AbstractThread::MainThread(), __func__,
+   monitorPromise->Then(
+     global->AbstractMainThreadFor(TaskCategory::Other), __func__,
      [promise] (CredentialPtr aInfo) {
        promise->MaybeResolve(aInfo);
      },
      [promise] (nsresult aErrorCode) {
        promise->MaybeReject(aErrorCode);
    });
 
    return promise.forget();
  }