Commit 11470b1d authored by Erik Nordin's avatar Erik Nordin Committed by enordin@mozilla.com
Browse files

Bug 1820252 - Misc. TranslationsDocument refactors r=translations-reviewers,gregtatum

A collection of miscellaneous refactors within the TranslationsDocument
class that achieve equivalent functionality to what existed before.

Differential Revision: https://phabricator.services.mozilla.com/D249970
parent e5f6989a
Loading
Loading
Loading
Loading
+130 −87
Original line number Diff line number Diff line
@@ -776,13 +776,11 @@ export class TranslationsDocument {
        if (!mutation.target) {
          continue;
        }

        const pendingNode = this.#getPendingNodeFromTarget(mutation.target);
        if (pendingNode) {
          const translationId =
            this.#pendingContentTranslations.get(pendingNode);
          if (translationId) {
          if (this.#preventContentTranslation(pendingNode)) {
            // The node was still pending to be translated, cancel it and re-submit.
            this.#preventContentTranslation(pendingNode, translationId);
            this.#markNodeContentMutated(pendingNode);
            if (mutation.type === "childList") {
              // New nodes could have been added, make sure we can follow their shadow roots.
@@ -808,11 +806,7 @@ export class TranslationsDocument {
              if (!removedNode) {
                continue;
              }
              const translationId =
                this.#pendingContentTranslations.get(removedNode);
              if (translationId) {
                this.#preventContentTranslation(removedNode, translationId);
              }
              this.#preventContentTranslation(removedNode);
              this.#preventAttributeTranslations(removedNode);
            }
            break;
@@ -851,8 +845,8 @@ export class TranslationsDocument {

    const addRootElements = () => {
      this.#addRootElement(document.querySelector("title"));
      this.#addRootElement(document.body);
      this.#addRootElement(document.head);
      this.#addRootElement(document.body);
    };

    if (document.body) {
@@ -907,7 +901,11 @@ export class TranslationsDocument {
   * @param {string} attributeName
   */
  #maybeMarkElementAttributeMutated(element, attributeName) {
    if (isAttributeTranslatable(element, attributeName)) {
    if (!isAttributeTranslatable(element, attributeName)) {
      // The given attribute is not translatable for this element.
      return;
    }

    let attributes = this.#elementsWithMutatedAttributes.get(element);
    if (!attributes) {
      attributes = new Set();
@@ -916,22 +914,30 @@ export class TranslationsDocument {
    attributes.add(attributeName);
    this.#ensureMutationUpdateCallbackIsRegistered();
  }
  }

  /**
   * Ensures that all nodes that have been picked up by the mutation observer
   * are processed, prioritized and sent to the scheduler to re translated.
   */
  #ensureMutationUpdateCallbackIsRegistered() {
    if (this.#hasPendingMutatedNodesCallback) {
      // A callback has already been registered to update mutated nodes.
      return;
    }

    if (
      !this.#hasPendingMutatedNodesCallback &&
      (this.#nodesWithMutatedContent.size || this.#queuedAttributeElements)
      this.#nodesWithMutatedContent.size === 0 &&
      this.#elementsWithMutatedAttributes.size === 0
    ) {
      // There are no mutated nodes to update.
      return;
    }

    this.#hasPendingMutatedNodesCallback = true;
    const ownerGlobal = ensureExists(this.#sourceDocument.ownerGlobal);
      // Perform a double requestAnimationFrame to:
      //   1. Reduce the number of invalidation cycles of canceling intermediate translations.
      //   2. Do less work on the main thread when there are many mutations.

    // Nodes can be mutated in a tight loop. To guard against the performance of re-translating nodes too frequently,
    // we will batch the processing of mutated nodes into a double requestAnimationFrame.
    ownerGlobal.requestAnimationFrame(() => {
      ownerGlobal.requestAnimationFrame(() => {
        this.#hasPendingMutatedNodesCallback = false;
@@ -982,10 +988,10 @@ export class TranslationsDocument {
          this.#enqueueElementForAttributeTranslation(node, attributes);
        }
        this.#dispatchQueuedAttributeTranslations();
        this.#elementsWithMutatedAttributes.clear();
      });
    });
  }
  }

  /**
   * If a pending node contains or is the target node, return that pending node.
@@ -1006,10 +1012,18 @@ export class TranslationsDocument {
   * that content has changed, and the previous translation is no longer valid.
   *
   * @param {Node} node
   * @param {number} translationId
   * @returns {boolean}
   */
  #preventContentTranslation(node, translationId) {
  #preventContentTranslation(node) {
    const translationId = this.#pendingContentTranslations.get(node);

    if (!translationId) {
      // No pending content translation was found for this node.
      return false;
    }

    this.translator.cancelSingleTranslation(translationId);

    if (!isNodeDetached(node)) {
      const element = /** @type {HTMLElement} */ (asHTMLElement(node));
      if (element) {
@@ -1024,8 +1038,11 @@ export class TranslationsDocument {
        }
      }
    }

    this.#pendingContentTranslations.delete(node);
    this.#processedContentNodes.delete(node);

    return true;
  }

  /**
@@ -1036,19 +1053,28 @@ export class TranslationsDocument {
   * that content has changed, and the previous translation is no longer valid.
   *
   * @param {Node} node
   * @returns {boolean}
   *   - True if any pending attribute translations were found for this node.
   */
  #preventAttributeTranslations(node) {
    const element = asElement(node);
    if (!element) {
      return;
      // We only translate attributes on Element type nodes.
      return false;
    }

    const attributes = this.#pendingAttributeTranslations.get(element);
    if (attributes) {
    if (!attributes) {
      // No pending attribute translations were found for this element.
      return false;
    }

    for (const translationId of attributes.values()) {
      this.translator.cancelSingleTranslation(translationId);
    }
    this.#pendingAttributeTranslations.delete(element);
    }

    return true;
  }

  /**
@@ -1233,18 +1259,27 @@ export class TranslationsDocument {

    this.#rootNodes.add(element);

    let viewportNodeTranslations =
      this.#subdivideNodeForContentTranslations(element);
    let viewportAttributeTranslations =
      this.#subdivideNodeForAttributeTranslations(element);
    if (element.nodeName === "TITLE") {
      // The <title> node is special, in that it will never intersect with the viewport,
      // so we must explicitly enqueue it for translation here.
      this.#enqueueNodeForContentTranslation(element);
      this.#maybeEnqueueElementForAttributeTranslation(element);
      return;
    }

    if (!this.#viewportTranslated) {
      this.#viewportTranslated = Promise.allSettled([
        ...(viewportNodeTranslations ?? []),
        ...(viewportAttributeTranslations ?? []),
      ]);
    if (element.nodeName !== "HEAD") {
      // We do not consider the <head> element for content translations, only attributes.
      const contentStartTime = Cu.now();
      this.#subdivideNodeForContentTranslations(element);
      ChromeUtils.addProfilerMarker(
        "TranslationsDocument Add Root",
        { startTime: contentStartTime, innerWindowId: this.#innerWindowId },
        `Subdivided new root "${node.nodeName}" for content translations`
      );
    }

    this.#subdivideNodeForAttributeTranslations(element);

    this.#mutationObserver.observe(element, MUTATION_OBSERVER_OPTIONS);
    this.#addShadowRootsToObserver(element);
  }
@@ -1520,8 +1555,10 @@ export class TranslationsDocument {
      visibility = "in-viewport";
    }

    if (!this.#processedContentNodes.has(node)) {
      this.#queuedContentNodes.set(node, visibility);
    }
  }

  /**
   * Submit the translations giving priority to nodes in the viewport.
@@ -2104,9 +2141,12 @@ export class TranslationsDocument {
   */
  #pauseMutationObserverAndThen(callback) {
    this.#stopMutationObserver();
    try {
      callback();
    } finally {
      this.#startMutationObserver();
    }
  }

  /**
   * Ensures that nodes with completed content translation requests will be updated.
@@ -2560,7 +2600,7 @@ function createNodePath(node, root) {
    root = node.ownerDocument.body;
  }
  if (node.parentNode && node.parentNode !== root) {
    path = createNodePath(node.parentNode);
    path = createNodePath(node.parentNode, root);
  }
  path += `/${node.nodeName}`;

@@ -3289,6 +3329,9 @@ function isNodeDetached(node) {
  return (
    // This node is out of the DOM and already garbage collected.
    Cu.isDeadWrapper(node) ||
    // The node is detached, but not yet garbage collected,
    // or it has been re-parented to a parent that itself is not connected.
    !node.isConnected ||
    // Normally you could just check `node.parentElement` to see if an element is
    // part of the DOM, but the Chrome-only flattenedTreeParentNode is used to include
    // Shadow DOM elements, which have a null parentElement.